-
Notifications
You must be signed in to change notification settings - Fork 0
Add test suite for input.py with prompt_toolkit mocking #20
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
bd917ae
a70f1a4
b93b7a5
96d38c0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,234 @@ | ||||||||
| import asyncio | ||||||||
| import tempfile | ||||||||
| import pathlib | ||||||||
| from unittest.mock import patch, AsyncMock, MagicMock | ||||||||
| import pytest | ||||||||
| import pytest_asyncio | ||||||||
| from sqlalchemy import select | ||||||||
| from ddss.orm import initialize_database, Facts, Ideas | ||||||||
| from ddss.input import main | ||||||||
|
|
||||||||
|
|
||||||||
| @pytest_asyncio.fixture | ||||||||
| async def temp_db(): | ||||||||
| """Fixture to create a temporary database.""" | ||||||||
| with tempfile.TemporaryDirectory() as tmpdir: | ||||||||
| db_path = pathlib.Path(tmpdir) / "test.db" | ||||||||
| addr = f"sqlite+aiosqlite:///{db_path.as_posix()}" | ||||||||
| engine, session = await initialize_database(addr) | ||||||||
| yield addr, engine, session | ||||||||
| await engine.dispose() | ||||||||
|
|
||||||||
|
|
||||||||
| @pytest.mark.asyncio | ||||||||
| async def test_input_valid_fact(temp_db): | ||||||||
| """Test that valid input is parsed and stored as a Fact in the database.""" | ||||||||
| addr, engine, session = temp_db | ||||||||
|
|
||||||||
| # Mock PromptSession to simulate user input | ||||||||
| mock_prompt_session = MagicMock() | ||||||||
| # First call returns valid input (parse will transform it), second call raises EOFError to exit | ||||||||
| mock_prompt_session.prompt_async = AsyncMock(side_effect=["a => b", EOFError()]) | ||||||||
|
|
||||||||
| with patch("ddss.input.PromptSession", return_value=mock_prompt_session): | ||||||||
| # Run the main function | ||||||||
| task = asyncio.create_task(main(addr, engine, session)) | ||||||||
| try: | ||||||||
| await task | ||||||||
| except asyncio.CancelledError: | ||||||||
| pass | ||||||||
|
|
||||||||
| # Verify data was stored (parse transforms "a => b" to "a\n----\nb\n") | ||||||||
| async with session() as sess: | ||||||||
| facts = await sess.scalars(select(Facts)) | ||||||||
| facts_list = list(facts) | ||||||||
| assert len(facts_list) == 1 | ||||||||
| assert facts_list[0].data == "a\n----\nb\n" | ||||||||
|
|
||||||||
|
|
||||||||
| @pytest.mark.asyncio | ||||||||
| async def test_input_generates_idea(temp_db): | ||||||||
| """Test that input that parses to non-dashed form generates an Idea.""" | ||||||||
| addr, engine, session = temp_db | ||||||||
|
|
||||||||
| # Mock PromptSession to simulate user input | ||||||||
| mock_prompt_session = MagicMock() | ||||||||
| # Input "a => b" parses to "a\n----\nb\n" which doesn't start with "--" | ||||||||
| # so it generates an idea | ||||||||
| mock_prompt_session.prompt_async = AsyncMock(side_effect=["a => b", EOFError()]) | ||||||||
|
|
||||||||
| with patch("ddss.input.PromptSession", return_value=mock_prompt_session): | ||||||||
| # Run the main function | ||||||||
| task = asyncio.create_task(main(addr, engine, session)) | ||||||||
| try: | ||||||||
| await task | ||||||||
| except asyncio.CancelledError: | ||||||||
|
||||||||
| pass | ||||||||
|
|
||||||||
| # Verify both Fact and Idea were stored | ||||||||
| async with session() as sess: | ||||||||
| facts = await sess.scalars(select(Facts)) | ||||||||
| facts_list = list(facts) | ||||||||
| assert len(facts_list) == 1 | ||||||||
| # parse("a => b") returns "a\n----\nb\n" | ||||||||
| assert facts_list[0].data == "a\n----\nb\n" | ||||||||
|
|
||||||||
| ideas = await sess.scalars(select(Ideas)) | ||||||||
| ideas_list = list(ideas) | ||||||||
| assert len(ideas_list) == 1 | ||||||||
| # rule_get_idea extracts first line: "----\na\n" | ||||||||
| assert ideas_list[0].data == "----\na\n" | ||||||||
|
|
||||||||
|
|
||||||||
| @pytest.mark.asyncio | ||||||||
| async def test_input_invalid_input_handling(temp_db, capsys): | ||||||||
| """Test that invalid/malformed input is handled gracefully with error message.""" | ||||||||
| addr, engine, session = temp_db | ||||||||
|
|
||||||||
| # Mock PromptSession to simulate user input | ||||||||
| mock_prompt_session = MagicMock() | ||||||||
| # First input will cause parse error, second exits | ||||||||
| mock_prompt_session.prompt_async = AsyncMock(side_effect=["=>", EOFError()]) | ||||||||
|
|
||||||||
| with patch("ddss.input.PromptSession", return_value=mock_prompt_session): | ||||||||
| # Run the main function | ||||||||
| task = asyncio.create_task(main(addr, engine, session)) | ||||||||
| try: | ||||||||
| await task | ||||||||
| except asyncio.CancelledError: | ||||||||
|
||||||||
| pass | ||||||||
|
|
||||||||
| # Check that error was printed | ||||||||
| captured = capsys.readouterr() | ||||||||
| assert "error:" in captured.out | ||||||||
|
|
||||||||
| # Verify no data was stored | ||||||||
| async with session() as sess: | ||||||||
| facts = await sess.scalars(select(Facts)) | ||||||||
| facts_list = list(facts) | ||||||||
| assert len(facts_list) == 0 | ||||||||
|
|
||||||||
|
|
||||||||
| @pytest.mark.asyncio | ||||||||
| async def test_input_empty_input_continues(temp_db): | ||||||||
| """Test that empty input is skipped and loop continues.""" | ||||||||
| addr, engine, session = temp_db | ||||||||
|
|
||||||||
| # Mock PromptSession to simulate user input | ||||||||
| mock_prompt_session = MagicMock() | ||||||||
| # Empty strings should be skipped, then valid input, then exit | ||||||||
| mock_prompt_session.prompt_async = AsyncMock(side_effect=["", " ", "a => b", EOFError()]) | ||||||||
|
|
||||||||
| with patch("ddss.input.PromptSession", return_value=mock_prompt_session): | ||||||||
| # Run the main function | ||||||||
| task = asyncio.create_task(main(addr, engine, session)) | ||||||||
| try: | ||||||||
| await task | ||||||||
| except asyncio.CancelledError: | ||||||||
|
||||||||
| except asyncio.CancelledError: | |
| except asyncio.CancelledError: | |
| # Expected - main cancels its task when input loop terminates |
Copilot
AI
Dec 22, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
'except' clause does nothing but pass and there is no explanatory comment.
| except asyncio.CancelledError: | |
| except asyncio.CancelledError: | |
| # Task cancellation can occur as part of normal test execution; ignore it. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
'except' clause does nothing but pass and there is no explanatory comment.