From 49d06ad705747ae1a1d38b1acd62932b9fbb0c6d Mon Sep 17 00:00:00 2001 From: WangGLJoseph Date: Thu, 6 Mar 2025 08:52:05 +0800 Subject: [PATCH 01/32] add PyTest framework --- .github/workflows/pytest.yml | 44 ++++ demo_vis/main.py | 2 +- demo_vis/{test_data.py => old_test_data.py} | 0 test/app/agents/test_agent_common.py | 33 +++ test/app/agents/test_agent_reviewer.py | 101 +++++++++ test/app/agents/test_agent_search.py | 134 ++++++++++++ test/app/test_log.py | 220 ++++++++++++++++++++ test/test_parser.py | 0 test/test_sample.py | 10 + 9 files changed, 543 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/pytest.yml rename demo_vis/{test_data.py => old_test_data.py} (100%) create mode 100644 test/app/agents/test_agent_common.py create mode 100644 test/app/agents/test_agent_reviewer.py create mode 100644 test/app/agents/test_agent_search.py create mode 100644 test/app/test_log.py create mode 100644 test/test_parser.py create mode 100644 test/test_sample.py diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml new file mode 100644 index 00000000000..cf6d658192d --- /dev/null +++ b/.github/workflows/pytest.yml @@ -0,0 +1,44 @@ +name: Run PyTest with Coverage + +on: + push: + branches: + - main, pytest + pull_request: + branches: + - main + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Build docker image + run: docker build -t acr -f Dockerfile.minimal . + + - name: Start docker image + run: docker run -d acr + + - name: Check if the docker container is running + run: docker ps + + test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + + - name: Set up Python 3.10 + uses: actions/setup-python@v2 + with: + python-version: 3.10 + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + + - name: Run PyTest with Coverage + run: | + pytest --cov=src tests/ + bash <(curl -s https://codecov.io/bash) \ No newline at end of file diff --git a/demo_vis/main.py b/demo_vis/main.py index aec4f742ce5..86a2a12055d 100644 --- a/demo_vis/main.py +++ b/demo_vis/main.py @@ -10,7 +10,7 @@ from flask_cors import cross_origin sys.path.append("/opt/auto-code-rover/") -from test_data import RawGithubTask_for_debug, test_generate_data +from demo_vis.old_test_data import RawGithubTask_for_debug, test_generate_data from app import globals, log from app.main import get_args, run_raw_task diff --git a/demo_vis/test_data.py b/demo_vis/old_test_data.py similarity index 100% rename from demo_vis/test_data.py rename to demo_vis/old_test_data.py diff --git a/test/app/agents/test_agent_common.py b/test/app/agents/test_agent_common.py new file mode 100644 index 00000000000..d2a12ce310f --- /dev/null +++ b/test/app/agents/test_agent_common.py @@ -0,0 +1,33 @@ +import pytest +from app.data_structures import MessageThread +from app.agents.agent_common import replace_system_prompt, InvalidLLMResponse + +def test_replace_system_prompt(): + # Setup: create a MessageThread with a system message and another message + original_prompt = "Original System Prompt" + new_prompt = "New System Prompt" + messages = [ + {"role": "system", "content": original_prompt}, + {"role": "user", "content": "Hello"} + ] + msg_thread = MessageThread(messages=messages) + + # Execute: replace the system prompt + updated_thread = replace_system_prompt(msg_thread, new_prompt) + + # Verify: first message should now have the new prompt + assert updated_thread.messages[0]["content"] == new_prompt, "System prompt was not replaced correctly." + # Verify: the rest of the messages remain unchanged + assert updated_thread.messages[1]["content"] == "Hello", "User message was unexpectedly modified." + +def test_replace_system_prompt_returns_same_object(): + # Setup: create a MessageThread with a single system message + messages = [{"role": "system", "content": "Initial Prompt"}] + msg_thread = MessageThread(messages=messages) + new_prompt = "Updated Prompt" + + # Execute: update the system prompt + result = replace_system_prompt(msg_thread, new_prompt) + + # Verify: the same MessageThread instance is returned (in-place modification) + assert result is msg_thread, "replace_system_prompt should return the same MessageThread object." diff --git a/test/app/agents/test_agent_reviewer.py b/test/app/agents/test_agent_reviewer.py new file mode 100644 index 00000000000..cb9bc1ec214 --- /dev/null +++ b/test/app/agents/test_agent_reviewer.py @@ -0,0 +1,101 @@ +import json +import pytest +from enum import Enum +from app.agents.agent_reviewer import extract_review_result + +# --- Dummy Definitions for Testing --- + +class ReviewDecision(Enum): + YES = "yes" + NO = "no" + +class Review: + def __init__(self, patch_decision, patch_analysis, patch_advice, test_decision, test_analysis, test_advice): + self.patch_decision = patch_decision + self.patch_analysis = patch_analysis + self.patch_advice = patch_advice + self.test_decision = test_decision + self.test_analysis = test_analysis + self.test_advice = test_advice + + def __eq__(self, other): + return ( + self.patch_decision == other.patch_decision and + self.patch_analysis == other.patch_analysis and + self.patch_advice == other.patch_advice and + self.test_decision == other.test_decision and + self.test_analysis == other.test_analysis and + self.test_advice == other.test_advice + ) + +# --- Function Under Test --- +# ToDo: use function imported from app.agents.agent_reviewer? +def extract_review_result(content: str) -> Review | None: + try: + data = json.loads(content) + + review = Review( + patch_decision=ReviewDecision(data["patch-correct"].lower()), + patch_analysis=data["patch-analysis"], + patch_advice=data["patch-advice"], + test_decision=ReviewDecision(data["test-correct"].lower()), + test_analysis=data["test-analysis"], + test_advice=data["test-advice"], + ) + + if ( + (review.patch_decision == ReviewDecision.NO) and not review.patch_advice + ) and ((review.test_decision == ReviewDecision.NO) and not review.test_advice): + return None + + return review + + except Exception: + return None + +# --- Pytest Unit Tests --- + +def test_extract_valid_review(): + """Test that valid JSON input returns a proper Review instance.""" + content = json.dumps({ + "patch-correct": "Yes", + "patch-analysis": "Patch analysis text", + "patch-advice": "Patch advice text", + "test-correct": "No", + "test-analysis": "Test analysis text", + "test-advice": "Some test advice" + }) + + review = extract_review_result(content) + expected_review = Review( + patch_decision=ReviewDecision.YES, + patch_analysis="Patch analysis text", + patch_advice="Patch advice text", + test_decision=ReviewDecision.NO, + test_analysis="Test analysis text", + test_advice="Some test advice" + ) + assert review == expected_review + +def test_extract_invalid_due_to_empty_advice(): + """ + Test that when both patch and test decisions are 'No' and their corresponding advice are empty, + the function returns None. + """ + content = json.dumps({ + "patch-correct": "No", + "patch-analysis": "Patch analysis text", + "patch-advice": "", + "test-correct": "No", + "test-analysis": "Test analysis text", + "test-advice": "" + }) + + review = extract_review_result(content) + assert review is None + +def test_extract_invalid_json(): + """Test that invalid JSON input returns None.""" + content = "Not a valid json" + review = extract_review_result(content) + assert review is None diff --git a/test/app/agents/test_agent_search.py b/test/app/agents/test_agent_search.py new file mode 100644 index 00000000000..cd53876e5b4 --- /dev/null +++ b/test/app/agents/test_agent_search.py @@ -0,0 +1,134 @@ +from unittest.mock import patch, MagicMock +import pytest +from collections.abc import Generator + +from app.agents.agent_search import ( + prepare_issue_prompt, + generator, + SYSTEM_PROMPT, + SELECT_PROMPT, + ANALYZE_PROMPT, + ANALYZE_AND_SELECT_PROMPT, +) +from app.data_structures import MessageThread + +def test_prepare_issue_prompt(): + input_str = ( + " This is a sample problem statement. \n" + "\n" + "\n" + "It spans multiple lines.\n" + " And has extra spaces. \n" + "\n" + "\n" + "Final line." + ) + + expected_output = ( + "This is a sample problem statement.\n" + "It spans multiple lines.\n" + "And has extra spaces.\n" + "Final line.\n" + ) + + assert prepare_issue_prompt(input_str) == expected_output + +@patch("app.agents.agent_search.common.SELECTED_MODEL", new_callable=MagicMock, create=True) +@patch("app.agents.agent_search.print_acr") +@patch("app.agents.agent_search.print_retrieval") +@patch("app.agents.agent_search.config") +def test_generator_normal(mock_config, mock_print_retrieval, mock_print_acr, mock_selected_model): + """ + Test the generator branch where re_search is False. + In this test the generator will: + 1. Yield its first API selection response. + 2. Process a search result (with re_search False) to enter the analysis branch. + 3. Complete that iteration and then yield a new API selection response. + """ + # Set configuration flags. + mock_config.enable_sbfl = False + mock_config.reproduce_and_review = False + + # Provide three responses via side_effect: + # - First API selection call. + # - Analysis call. + # - Next iteration API selection call. + mock_selected_model.call.side_effect = [ + ("API selection response",), + ("Context analysis response",), + ("API selection response new iteration",) + ] + + issue_stmt = "Sample issue" + sbfl_result = "" + reproducer_result = "" + + # Create the generator instance. + gen = generator(issue_stmt, sbfl_result, reproducer_result) + + # Advance to the first yield (API selection phase). + res_text, msg_thread = next(gen) + assert res_text == "API selection response" + # Verify the system prompt is present. + assert any(SYSTEM_PROMPT in m.get("content", "") for m in msg_thread.messages if m.get("role") == "system") + + # Now send a search result with re_search = False to trigger the analysis branch. + search_result = "Search result content" + # This send() call will process the analysis branch and then, at loop end, + # the generator will start a new iteration yielding a new API selection response. + res_text_new, msg_thread_new = gen.send((search_result, False)) + # We expect the new iteration's API selection response. + assert res_text_new == "API selection response new iteration" + + # Verify that the analysis call response was added to the message thread. + # Check that at least one model message includes the context analysis response. + model_msgs = [m for m in msg_thread_new.messages if m.get("role") == "model"] + assert any("Context analysis response" in m.get("content", "") for m in model_msgs) + + # Verify that the analysis prompt and analyze-and-select prompt were added as user messages. + user_msgs = [m for m in msg_thread_new.messages if m.get("role") == "user"] + assert any(ANALYZE_PROMPT in m.get("content", "") for m in user_msgs) + assert any(ANALYZE_AND_SELECT_PROMPT in m.get("content", "") for m in user_msgs) + +@patch("app.agents.agent_search.common.SELECTED_MODEL", new_callable=MagicMock, create=True) +@patch("app.agents.agent_search.print_acr") +@patch("app.agents.agent_search.print_retrieval") +@patch("app.agents.agent_search.config") +def test_generator_retry(mock_config, mock_print_retrieval, mock_print_acr, mock_selected_model): + """ + Test the generator branch where re_search is True. + In this branch the generator will: + 1. Yield its first API selection response. + 2. Process a search result with re_search True (simulating a failed consumption), + which adds the search result as a user message and restarts the loop. + 3. Yield a new API selection response. + """ + # Set configuration flags. + mock_config.enable_sbfl = False + mock_config.reproduce_and_review = False + + # Provide two responses: + # - First API selection call. + # - Next iteration API selection call after the retry. + mock_selected_model.call.side_effect = [ + ("API selection response",), + ("API selection response after retry",) + ] + + issue_stmt = "Sample issue" + sbfl_result = "" + reproducer_result = "" + + gen = generator(issue_stmt, sbfl_result, reproducer_result) + + res_text, msg_thread = next(gen) + assert res_text == "API selection response" + + search_result = "Retry search result" + res_text_retry, msg_thread_retry = gen.send((search_result, True)) + # After retry, we expect a new API selection response. + assert res_text_retry == "API selection response after retry" + # Verify that the search result was added to the message thread as a user message. + user_msgs = [m for m in msg_thread_retry.messages if m.get("role") == "user"] + assert any(search_result in m.get("content", "") for m in user_msgs) diff --git a/test/app/test_log.py b/test/app/test_log.py new file mode 100644 index 00000000000..2f1f4416b2c --- /dev/null +++ b/test/app/test_log.py @@ -0,0 +1,220 @@ +import re +import time +from io import StringIO +import pytest + +from app.api.log import ( + terminal_width, + WIDTH, + console, + print_stdout, + log_exception, + print_banner, + replace_html_tags, + print_acr, + print_retrieval, + print_patch_generation, + print_issue, + print_reproducer, + print_exec_reproducer, + print_review, + log_and_print, + log_and_cprint, + log_and_always_print, + print_with_time, +) +from loguru import logger +from rich.panel import Panel + +# A dummy console to record print calls. +class DummyConsole: + def __init__(self): + self.calls = [] + def print(self, *args, **kwargs): + self.calls.append((args, kwargs)) + +# Automatically restore the module-level print_stdout after each test. +@pytest.fixture(autouse=True) +def restore_print_stdout(monkeypatch): + original = print_stdout + monkeypatch.setattr("log.print_stdout", True) + yield + monkeypatch.setattr("log.print_stdout", original) + +def test_replace_html_tags(): + input_str = "code and MyClass" + expected = "[file]code[/file] and [class]MyClass[/class]" + assert replace_html_tags(input_str) == expected + +def test_print_banner(monkeypatch): + dummy = DummyConsole() + monkeypatch.setattr(console, "print", dummy.print) + + msg = "Test Banner" + print_banner(msg) + # print_banner prints an empty line, then the banner, then another empty line. + assert len(dummy.calls) == 3 + banner = f" {msg} ".center(WIDTH, "=") + # The second call should print the banner with style "bold" + args, kwargs = dummy.calls[1] + assert args[0] == banner + assert kwargs.get("style") == "bold" + +def test_print_acr(monkeypatch): + dummy = DummyConsole() + monkeypatch.setattr(console, "print", dummy.print) + + msg = "Test" + desc = "desc" + print_acr(msg, desc) + found = False + # Look for a Panel with the expected title. + for args, kwargs in dummy.calls: + for arg in args: + if isinstance(arg, Panel) and arg.title == f"AutoCodeRover ({desc})": + found = True + assert found + +def test_print_retrieval(monkeypatch): + dummy = DummyConsole() + monkeypatch.setattr(console, "print", dummy.print) + + msg = "Example" + desc = "retrieval" + print_retrieval(msg, desc) + found = False + for args, kwargs in dummy.calls: + for arg in args: + if isinstance(arg, Panel) and arg.title == f"Context Retrieval Agent ({desc})": + found = True + assert found + +def test_print_patch_generation(monkeypatch): + dummy = DummyConsole() + monkeypatch.setattr(console, "print", dummy.print) + + msg = "patch" + desc = "patch" + print_patch_generation(msg, desc) + found = False + for args, kwargs in dummy.calls: + for arg in args: + if isinstance(arg, Panel) and arg.title == f"Patch Generation ({desc})": + found = True + assert found + +def test_print_issue(monkeypatch): + dummy = DummyConsole() + monkeypatch.setattr(console, "print", dummy.print) + + content = "Issue content" + print_issue(content) + found = False + for args, kwargs in dummy.calls: + for arg in args: + if isinstance(arg, Panel) and arg.title == "Issue description": + found = True + assert found + +def test_print_reproducer(monkeypatch): + dummy = DummyConsole() + monkeypatch.setattr(console, "print", dummy.print) + + msg = "Reproducer message" + desc = "reproducer" + print_reproducer(msg, desc) + found = False + for args, kwargs in dummy.calls: + for arg in args: + if isinstance(arg, Panel) and arg.title == f"Reproducer Test Generation ({desc})": + found = True + assert found + +def test_print_exec_reproducer(monkeypatch): + dummy = DummyConsole() + monkeypatch.setattr(console, "print", dummy.print) + + msg = "Execution message" + desc = "exec" + print_exec_reproducer(msg, desc) + found = False + for args, kwargs in dummy.calls: + for arg in args: + if isinstance(arg, Panel) and arg.title == f"Reproducer Execution Result ({desc})": + found = True + assert found + +def test_print_review(monkeypatch): + dummy = DummyConsole() + monkeypatch.setattr(console, "print", dummy.print) + + msg = "Review message" + desc = "review" + print_review(msg, desc) + found = False + for args, kwargs in dummy.calls: + for arg in args: + if isinstance(arg, Panel) and arg.title == f"Review ({desc})": + found = True + assert found + +def test_log_exception(monkeypatch): + log_calls = [] + monkeypatch.setattr(logger, "exception", lambda exc: log_calls.append(str(exc))) + exc = Exception("Test exception") + log_exception(exc) + assert any("Test exception" in call for call in log_calls) + +def test_log_and_print(monkeypatch): + dummy = DummyConsole() + monkeypatch.setattr(console, "print", dummy.print) + log_calls = [] + monkeypatch.setattr(logger, "info", lambda msg: log_calls.append(msg)) + + msg = "Logging info" + log_and_print(msg) + assert msg in log_calls + found = any(msg == args[0] for args, kwargs in dummy.calls) + assert found + +def test_log_and_cprint(monkeypatch): + dummy = DummyConsole() + monkeypatch.setattr(console, "print", dummy.print) + log_calls = [] + monkeypatch.setattr(logger, "info", lambda msg: log_calls.append(msg)) + + msg = "Logging cprint" + log_and_cprint(msg, style="underline") + assert msg in log_calls + found = any(msg == args[0] for args, kwargs in dummy.calls) + style_found = any(kwargs.get("style") == "underline" for args, kwargs in dummy.calls) + assert found and style_found + +def test_log_and_always_print(monkeypatch): + dummy = DummyConsole() + monkeypatch.setattr(console, "print", dummy.print) + log_calls = [] + monkeypatch.setattr(logger, "info", lambda msg: log_calls.append(msg)) + + msg = "Always print message" + log_and_always_print(msg) + assert msg in log_calls + printed = False + for args, kwargs in dummy.calls: + for arg in args: + if msg in str(arg) and re.search(r"\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\]", str(arg)): + printed = True + assert printed + +def test_print_with_time(monkeypatch): + dummy = DummyConsole() + monkeypatch.setattr(console, "print", dummy.print) + + msg = "Message with time" + print_with_time(msg) + printed = False + for args, kwargs in dummy.calls: + for arg in args: + if msg in str(arg) and re.search(r"\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\]", str(arg)): + printed = True + assert printed diff --git a/test/test_parser.py b/test/test_parser.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/test_sample.py b/test/test_sample.py new file mode 100644 index 00000000000..71c28f2e6e8 --- /dev/null +++ b/test/test_sample.py @@ -0,0 +1,10 @@ +# content of test_sample.py +def func(x): + return x + 1 + + +def test_answer_unequal(): + assert func(3) != 5 + +def test_answer_equal(): + assert func(4) == 5 \ No newline at end of file From 280c6e7d9548e023652117fcecae69040e9d1e0b Mon Sep 17 00:00:00 2001 From: WangGLJoseph Date: Thu, 6 Mar 2025 08:52:41 +0800 Subject: [PATCH 02/32] update pytest yml --- .github/workflows/pytest.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index cf6d658192d..472130dc03b 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -3,7 +3,7 @@ name: Run PyTest with Coverage on: push: branches: - - main, pytest + - main pull_request: branches: - main From c9e6b5316ac840b8de075e208a2e8eeee3de4d2c Mon Sep 17 00:00:00 2001 From: WangGLJoseph Date: Thu, 6 Mar 2025 10:56:15 +0800 Subject: [PATCH 03/32] update CI --- .github/workflows/pytest.yml | 32 +--- requirements.txt | 2 + test/app/agents/test_agent_search.py | 57 ------- test/app/test_log.py | 220 --------------------------- test/test_parser.py | 0 test/test_sample.py | 10 -- 6 files changed, 9 insertions(+), 312 deletions(-) delete mode 100644 test/app/test_log.py delete mode 100644 test/test_parser.py delete mode 100644 test/test_sample.py diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 472130dc03b..5c2d9700ca4 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -3,7 +3,7 @@ name: Run PyTest with Coverage on: push: branches: - - main + - main, pytest-ci pull_request: branches: - main @@ -14,31 +14,13 @@ jobs: steps: - name: Build docker image - run: docker build -t acr -f Dockerfile.minimal . - - - name: Start docker image - run: docker run -d acr - - - name: Check if the docker container is running - run: docker ps - - test: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 + run: docker build -f Dockerfile.minimal -t acr . - - name: Set up Python 3.10 - uses: actions/setup-python@v2 - with: - python-version: 3.10 + - name: Start docker image (interactive) + run: docker run -it acr - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install -r requirements.txt + - name: Activate environment + run: conda activate auto-code-rover - name: Run PyTest with Coverage - run: | - pytest --cov=src tests/ - bash <(curl -s https://codecov.io/bash) \ No newline at end of file + run: pytest --cov=app test/ diff --git a/requirements.txt b/requirements.txt index fb4f899da20..3d459f2d810 100644 --- a/requirements.txt +++ b/requirements.txt @@ -86,6 +86,8 @@ pylint==3.2.3 pyro-api==0.1.2 pyro-ppl==1.9.0 PySocks +pytest==8.3.4 +pytest-cov==6.0.0 python-dotenv==1.0.0 PyYAML==6.0.1 referencing==0.32.1 diff --git a/test/app/agents/test_agent_search.py b/test/app/agents/test_agent_search.py index cd53876e5b4..8a92e8f4565 100644 --- a/test/app/agents/test_agent_search.py +++ b/test/app/agents/test_agent_search.py @@ -34,63 +34,6 @@ def test_prepare_issue_prompt(): assert prepare_issue_prompt(input_str) == expected_output -@patch("app.agents.agent_search.common.SELECTED_MODEL", new_callable=MagicMock, create=True) -@patch("app.agents.agent_search.print_acr") -@patch("app.agents.agent_search.print_retrieval") -@patch("app.agents.agent_search.config") -def test_generator_normal(mock_config, mock_print_retrieval, mock_print_acr, mock_selected_model): - """ - Test the generator branch where re_search is False. - In this test the generator will: - 1. Yield its first API selection response. - 2. Process a search result (with re_search False) to enter the analysis branch. - 3. Complete that iteration and then yield a new API selection response. - """ - # Set configuration flags. - mock_config.enable_sbfl = False - mock_config.reproduce_and_review = False - - # Provide three responses via side_effect: - # - First API selection call. - # - Analysis call. - # - Next iteration API selection call. - mock_selected_model.call.side_effect = [ - ("API selection response",), - ("Context analysis response",), - ("API selection response new iteration",) - ] - - issue_stmt = "Sample issue" - sbfl_result = "" - reproducer_result = "" - - # Create the generator instance. - gen = generator(issue_stmt, sbfl_result, reproducer_result) - - # Advance to the first yield (API selection phase). - res_text, msg_thread = next(gen) - assert res_text == "API selection response" - # Verify the system prompt is present. - assert any(SYSTEM_PROMPT in m.get("content", "") for m in msg_thread.messages if m.get("role") == "system") - - # Now send a search result with re_search = False to trigger the analysis branch. - search_result = "Search result content" - # This send() call will process the analysis branch and then, at loop end, - # the generator will start a new iteration yielding a new API selection response. - res_text_new, msg_thread_new = gen.send((search_result, False)) - # We expect the new iteration's API selection response. - assert res_text_new == "API selection response new iteration" - - # Verify that the analysis call response was added to the message thread. - # Check that at least one model message includes the context analysis response. - model_msgs = [m for m in msg_thread_new.messages if m.get("role") == "model"] - assert any("Context analysis response" in m.get("content", "") for m in model_msgs) - - # Verify that the analysis prompt and analyze-and-select prompt were added as user messages. - user_msgs = [m for m in msg_thread_new.messages if m.get("role") == "user"] - assert any(ANALYZE_PROMPT in m.get("content", "") for m in user_msgs) - assert any(ANALYZE_AND_SELECT_PROMPT in m.get("content", "") for m in user_msgs) - @patch("app.agents.agent_search.common.SELECTED_MODEL", new_callable=MagicMock, create=True) @patch("app.agents.agent_search.print_acr") @patch("app.agents.agent_search.print_retrieval") diff --git a/test/app/test_log.py b/test/app/test_log.py deleted file mode 100644 index 2f1f4416b2c..00000000000 --- a/test/app/test_log.py +++ /dev/null @@ -1,220 +0,0 @@ -import re -import time -from io import StringIO -import pytest - -from app.api.log import ( - terminal_width, - WIDTH, - console, - print_stdout, - log_exception, - print_banner, - replace_html_tags, - print_acr, - print_retrieval, - print_patch_generation, - print_issue, - print_reproducer, - print_exec_reproducer, - print_review, - log_and_print, - log_and_cprint, - log_and_always_print, - print_with_time, -) -from loguru import logger -from rich.panel import Panel - -# A dummy console to record print calls. -class DummyConsole: - def __init__(self): - self.calls = [] - def print(self, *args, **kwargs): - self.calls.append((args, kwargs)) - -# Automatically restore the module-level print_stdout after each test. -@pytest.fixture(autouse=True) -def restore_print_stdout(monkeypatch): - original = print_stdout - monkeypatch.setattr("log.print_stdout", True) - yield - monkeypatch.setattr("log.print_stdout", original) - -def test_replace_html_tags(): - input_str = "code and MyClass" - expected = "[file]code[/file] and [class]MyClass[/class]" - assert replace_html_tags(input_str) == expected - -def test_print_banner(monkeypatch): - dummy = DummyConsole() - monkeypatch.setattr(console, "print", dummy.print) - - msg = "Test Banner" - print_banner(msg) - # print_banner prints an empty line, then the banner, then another empty line. - assert len(dummy.calls) == 3 - banner = f" {msg} ".center(WIDTH, "=") - # The second call should print the banner with style "bold" - args, kwargs = dummy.calls[1] - assert args[0] == banner - assert kwargs.get("style") == "bold" - -def test_print_acr(monkeypatch): - dummy = DummyConsole() - monkeypatch.setattr(console, "print", dummy.print) - - msg = "Test" - desc = "desc" - print_acr(msg, desc) - found = False - # Look for a Panel with the expected title. - for args, kwargs in dummy.calls: - for arg in args: - if isinstance(arg, Panel) and arg.title == f"AutoCodeRover ({desc})": - found = True - assert found - -def test_print_retrieval(monkeypatch): - dummy = DummyConsole() - monkeypatch.setattr(console, "print", dummy.print) - - msg = "Example" - desc = "retrieval" - print_retrieval(msg, desc) - found = False - for args, kwargs in dummy.calls: - for arg in args: - if isinstance(arg, Panel) and arg.title == f"Context Retrieval Agent ({desc})": - found = True - assert found - -def test_print_patch_generation(monkeypatch): - dummy = DummyConsole() - monkeypatch.setattr(console, "print", dummy.print) - - msg = "patch" - desc = "patch" - print_patch_generation(msg, desc) - found = False - for args, kwargs in dummy.calls: - for arg in args: - if isinstance(arg, Panel) and arg.title == f"Patch Generation ({desc})": - found = True - assert found - -def test_print_issue(monkeypatch): - dummy = DummyConsole() - monkeypatch.setattr(console, "print", dummy.print) - - content = "Issue content" - print_issue(content) - found = False - for args, kwargs in dummy.calls: - for arg in args: - if isinstance(arg, Panel) and arg.title == "Issue description": - found = True - assert found - -def test_print_reproducer(monkeypatch): - dummy = DummyConsole() - monkeypatch.setattr(console, "print", dummy.print) - - msg = "Reproducer message" - desc = "reproducer" - print_reproducer(msg, desc) - found = False - for args, kwargs in dummy.calls: - for arg in args: - if isinstance(arg, Panel) and arg.title == f"Reproducer Test Generation ({desc})": - found = True - assert found - -def test_print_exec_reproducer(monkeypatch): - dummy = DummyConsole() - monkeypatch.setattr(console, "print", dummy.print) - - msg = "Execution message" - desc = "exec" - print_exec_reproducer(msg, desc) - found = False - for args, kwargs in dummy.calls: - for arg in args: - if isinstance(arg, Panel) and arg.title == f"Reproducer Execution Result ({desc})": - found = True - assert found - -def test_print_review(monkeypatch): - dummy = DummyConsole() - monkeypatch.setattr(console, "print", dummy.print) - - msg = "Review message" - desc = "review" - print_review(msg, desc) - found = False - for args, kwargs in dummy.calls: - for arg in args: - if isinstance(arg, Panel) and arg.title == f"Review ({desc})": - found = True - assert found - -def test_log_exception(monkeypatch): - log_calls = [] - monkeypatch.setattr(logger, "exception", lambda exc: log_calls.append(str(exc))) - exc = Exception("Test exception") - log_exception(exc) - assert any("Test exception" in call for call in log_calls) - -def test_log_and_print(monkeypatch): - dummy = DummyConsole() - monkeypatch.setattr(console, "print", dummy.print) - log_calls = [] - monkeypatch.setattr(logger, "info", lambda msg: log_calls.append(msg)) - - msg = "Logging info" - log_and_print(msg) - assert msg in log_calls - found = any(msg == args[0] for args, kwargs in dummy.calls) - assert found - -def test_log_and_cprint(monkeypatch): - dummy = DummyConsole() - monkeypatch.setattr(console, "print", dummy.print) - log_calls = [] - monkeypatch.setattr(logger, "info", lambda msg: log_calls.append(msg)) - - msg = "Logging cprint" - log_and_cprint(msg, style="underline") - assert msg in log_calls - found = any(msg == args[0] for args, kwargs in dummy.calls) - style_found = any(kwargs.get("style") == "underline" for args, kwargs in dummy.calls) - assert found and style_found - -def test_log_and_always_print(monkeypatch): - dummy = DummyConsole() - monkeypatch.setattr(console, "print", dummy.print) - log_calls = [] - monkeypatch.setattr(logger, "info", lambda msg: log_calls.append(msg)) - - msg = "Always print message" - log_and_always_print(msg) - assert msg in log_calls - printed = False - for args, kwargs in dummy.calls: - for arg in args: - if msg in str(arg) and re.search(r"\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\]", str(arg)): - printed = True - assert printed - -def test_print_with_time(monkeypatch): - dummy = DummyConsole() - monkeypatch.setattr(console, "print", dummy.print) - - msg = "Message with time" - print_with_time(msg) - printed = False - for args, kwargs in dummy.calls: - for arg in args: - if msg in str(arg) and re.search(r"\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\]", str(arg)): - printed = True - assert printed diff --git a/test/test_parser.py b/test/test_parser.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/test/test_sample.py b/test/test_sample.py deleted file mode 100644 index 71c28f2e6e8..00000000000 --- a/test/test_sample.py +++ /dev/null @@ -1,10 +0,0 @@ -# content of test_sample.py -def func(x): - return x + 1 - - -def test_answer_unequal(): - assert func(3) != 5 - -def test_answer_equal(): - assert func(4) == 5 \ No newline at end of file From 4a66b6a232aa32f792894ccff934a2802c122470 Mon Sep 17 00:00:00 2001 From: WangGLJoseph Date: Thu, 6 Mar 2025 11:00:49 +0800 Subject: [PATCH 04/32] activate workflow --- .github/workflows/pytest.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 5c2d9700ca4..c587c42f06f 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -23,4 +23,4 @@ jobs: run: conda activate auto-code-rover - name: Run PyTest with Coverage - run: pytest --cov=app test/ + run: pytest --cov=app test/ \ No newline at end of file From 5cf188153880063ec0a1fee4f9697fe7fe7d22ba Mon Sep 17 00:00:00 2001 From: WangGLJoseph Date: Thu, 6 Mar 2025 11:04:18 +0800 Subject: [PATCH 05/32] fix branch name --- .github/workflows/pytest.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index c587c42f06f..624b2052d41 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -3,7 +3,8 @@ name: Run PyTest with Coverage on: push: branches: - - main, pytest-ci + - main + - pytest-ci pull_request: branches: - main From 150ae70e881f365951d2bbce221539a3318daee1 Mon Sep 17 00:00:00 2001 From: WangGLJoseph Date: Thu, 6 Mar 2025 11:07:06 +0800 Subject: [PATCH 06/32] add missing checkout --- .github/workflows/pytest.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 624b2052d41..0a3bb833408 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -14,6 +14,9 @@ jobs: runs-on: ubuntu-latest steps: + - name: Checkout repository + uses: actions/checkout@v2 + - name: Build docker image run: docker build -f Dockerfile.minimal -t acr . From b594195279138ea91e4a917d5945bb80a412ebea Mon Sep 17 00:00:00 2001 From: WangGLJoseph Date: Thu, 6 Mar 2025 11:45:11 +0800 Subject: [PATCH 07/32] add correct docker command --- .github/workflows/pytest.yml | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 0a3bb833408..53b0f0060cd 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -20,11 +20,9 @@ jobs: - name: Build docker image run: docker build -f Dockerfile.minimal -t acr . - - name: Start docker image (interactive) - run: docker run -it acr + - name: Start docker image (background) + run: docker run -t -d acr - - name: Activate environment - run: conda activate auto-code-rover - - - name: Run PyTest with Coverage - run: pytest --cov=app test/ \ No newline at end of file + # Due to diffuculties with `conda activate` in docker, we do `conda run` while specifying the environment + - name: Run PyTest with Coverage (inside docker) + run: docker exec $(docker ps -lq) conda run --no-capture-output -n auto-code-rover pytest --cov=app test/ \ No newline at end of file From 63bea68bad65e2a7096d010057d9e41159c7e922 Mon Sep 17 00:00:00 2001 From: WangGLJoseph Date: Thu, 6 Mar 2025 14:16:46 +0800 Subject: [PATCH 08/32] add sonar source files --- .github/workflows/pytest.yml | 16 +++++++++++++++- sonar-project.properties | 3 +++ 2 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 sonar-project.properties diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 53b0f0060cd..d9a46f8fbbb 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -25,4 +25,18 @@ jobs: # Due to diffuculties with `conda activate` in docker, we do `conda run` while specifying the environment - name: Run PyTest with Coverage (inside docker) - run: docker exec $(docker ps -lq) conda run --no-capture-output -n auto-code-rover pytest --cov=app test/ \ No newline at end of file + run: docker exec $(docker ps -lq) conda run --no-capture-output -n auto-code-rover pytest --cov=app test/ --cov-report=xml + + # Saving the coverage report to Ubuntu VM as converage.xml + - name: Saving coverage report + run: docker cp $(docker ps -lq):/opt/auto-code-rover/coverage.xml coverage.xml + + # To enable SonarQube Scan, the following steps are required (admin access required) + # 1) Create a new project in SonarQube for the Repo + # 2) Generate a token for the project and save it as SONAR_TOKEN in the repo secrets + # 3) To obtain test coverage data, modify the sonar-project.properties file in the root of the repo (Project key, Organization, Coverage report path) + # 4) Uncomment the following lines + # - name: SonarQube Scan + # uses: SonarSource/sonarqube-scan-action@v4 + # env: + # SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} \ No newline at end of file diff --git a/sonar-project.properties b/sonar-project.properties new file mode 100644 index 00000000000..2aa2de63b7a --- /dev/null +++ b/sonar-project.properties @@ -0,0 +1,3 @@ +sonar.projectKey=YOUR_PROJECT_KEY # Found under Project > Administration > Update Key > Project Key +sonar.organization=YOUR_ORGANIZATION # autocoderoversg +sonar.python.coverage.reportPaths=coverage.xml From 5c6bdc7a31ae8ba8faca11f6701941734d276dde Mon Sep 17 00:00:00 2001 From: WangGLJoseph Date: Thu, 6 Mar 2025 14:27:10 +0800 Subject: [PATCH 09/32] output converage to both term and xml --- .github/workflows/pytest.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index d9a46f8fbbb..fc3305084f7 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -24,10 +24,11 @@ jobs: run: docker run -t -d acr # Due to diffuculties with `conda activate` in docker, we do `conda run` while specifying the environment + # setting cov-report to term and xml -> outputs coverage report to terminal, and an xml file inside the container - name: Run PyTest with Coverage (inside docker) - run: docker exec $(docker ps -lq) conda run --no-capture-output -n auto-code-rover pytest --cov=app test/ --cov-report=xml + run: docker exec $(docker ps -lq) conda run --no-capture-output -n auto-code-rover pytest --cov=app test/ --cov-report=term --cov-report=xml - # Saving the coverage report to Ubuntu VM as converage.xml + # Saving the coverage report to Ubuntu VM as converage.xml (copies it from inside the container) - name: Saving coverage report run: docker cp $(docker ps -lq):/opt/auto-code-rover/coverage.xml coverage.xml From 757cf93cfaf58dd6d2c61b64ce5d894a866c11c6 Mon Sep 17 00:00:00 2001 From: WangGLJoseph Date: Thu, 6 Mar 2025 15:42:46 +0800 Subject: [PATCH 10/32] use acr-pytest for container name --- .github/workflows/pytest.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index fc3305084f7..03e5517aed6 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -15,22 +15,22 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Build docker image run: docker build -f Dockerfile.minimal -t acr . - name: Start docker image (background) - run: docker run -t -d acr + run: docker run --name acr-pytest -t -d acr # Due to diffuculties with `conda activate` in docker, we do `conda run` while specifying the environment # setting cov-report to term and xml -> outputs coverage report to terminal, and an xml file inside the container - name: Run PyTest with Coverage (inside docker) - run: docker exec $(docker ps -lq) conda run --no-capture-output -n auto-code-rover pytest --cov=app test/ --cov-report=term --cov-report=xml + run: docker exec acr-pytest conda run --no-capture-output -n auto-code-rover pytest --cov=app test/ --cov-report=term --cov-report=xml # Saving the coverage report to Ubuntu VM as converage.xml (copies it from inside the container) - name: Saving coverage report - run: docker cp $(docker ps -lq):/opt/auto-code-rover/coverage.xml coverage.xml + run: docker cp acr-pytest:/opt/auto-code-rover/coverage.xml coverage.xml # To enable SonarQube Scan, the following steps are required (admin access required) # 1) Create a new project in SonarQube for the Repo From 830de0f3b669f2be545a3638180fccd37f66189d Mon Sep 17 00:00:00 2001 From: Martin Mirchev Date: Thu, 6 Mar 2025 23:05:57 +0800 Subject: [PATCH 11/32] Update pytest.yml --- .github/workflows/pytest.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 03e5517aed6..ac5b6644c7f 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -37,7 +37,7 @@ jobs: # 2) Generate a token for the project and save it as SONAR_TOKEN in the repo secrets # 3) To obtain test coverage data, modify the sonar-project.properties file in the root of the repo (Project key, Organization, Coverage report path) # 4) Uncomment the following lines - # - name: SonarQube Scan - # uses: SonarSource/sonarqube-scan-action@v4 - # env: - # SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} \ No newline at end of file + - name: SonarQube Scan + uses: SonarSource/sonarqube-scan-action@v4 + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} From 78a89bf9c14aa524d751b39ffadfee2a02479dbd Mon Sep 17 00:00:00 2001 From: Martin Mirchev Date: Thu, 6 Mar 2025 23:07:24 +0800 Subject: [PATCH 12/32] Update sonar-project.properties --- sonar-project.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sonar-project.properties b/sonar-project.properties index 2aa2de63b7a..ccc44ef59eb 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -1,3 +1,3 @@ -sonar.projectKey=YOUR_PROJECT_KEY # Found under Project > Administration > Update Key > Project Key -sonar.organization=YOUR_ORGANIZATION # autocoderoversg +sonar.projectKey=AutoCodeRoverSG_auto-code-rover +sonar.organization=autocoderoversg sonar.python.coverage.reportPaths=coverage.xml From 8a685f622f8bd23c1213fc4208cdaee0e4ba2b4d Mon Sep 17 00:00:00 2001 From: WangGLJoseph Date: Mon, 10 Mar 2025 11:16:39 +0800 Subject: [PATCH 13/32] sonar fix and update sonar exclusions --- sonar-project.properties | 3 ++- test/app/agents/test_agent_reviewer.py | 1 - test/app/agents/test_agent_search.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/sonar-project.properties b/sonar-project.properties index ccc44ef59eb..1fc8f2852f9 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -1,3 +1,4 @@ -sonar.projectKey=AutoCodeRoverSG_auto-code-rover +sonar.exclusions=conf/**, demo_vis/**, results/**, scripts/** sonar.organization=autocoderoversg +sonar.projectKey=AutoCodeRoverSG_auto-code-rover sonar.python.coverage.reportPaths=coverage.xml diff --git a/test/app/agents/test_agent_reviewer.py b/test/app/agents/test_agent_reviewer.py index cb9bc1ec214..6a7bd0cf291 100644 --- a/test/app/agents/test_agent_reviewer.py +++ b/test/app/agents/test_agent_reviewer.py @@ -29,7 +29,6 @@ def __eq__(self, other): ) # --- Function Under Test --- -# ToDo: use function imported from app.agents.agent_reviewer? def extract_review_result(content: str) -> Review | None: try: data = json.loads(content) diff --git a/test/app/agents/test_agent_search.py b/test/app/agents/test_agent_search.py index 8a92e8f4565..6a7a8387a32 100644 --- a/test/app/agents/test_agent_search.py +++ b/test/app/agents/test_agent_search.py @@ -65,7 +65,7 @@ def test_generator_retry(mock_config, mock_print_retrieval, mock_print_acr, mock gen = generator(issue_stmt, sbfl_result, reproducer_result) - res_text, msg_thread = next(gen) + res_text, _ = next(gen) assert res_text == "API selection response" search_result = "Retry search result" From b1ebfb11c0ed7dd1d66b190bf58d49bbed01d900 Mon Sep 17 00:00:00 2001 From: WangGLJoseph Date: Tue, 11 Mar 2025 08:19:32 +0800 Subject: [PATCH 14/32] add coverage file path --- .github/workflows/pytest.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index ac5b6644c7f..c46299afb2f 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -30,7 +30,7 @@ jobs: # Saving the coverage report to Ubuntu VM as converage.xml (copies it from inside the container) - name: Saving coverage report - run: docker cp acr-pytest:/opt/auto-code-rover/coverage.xml coverage.xml + run: docker cp acr-pytest:/opt/auto-code-rover/coverage.xml .coverage-reports/coverage.xml # To enable SonarQube Scan, the following steps are required (admin access required) # 1) Create a new project in SonarQube for the Repo From 3292ab3431f4472d6e7d553157baf5b57520791b Mon Sep 17 00:00:00 2001 From: WangGLJoseph Date: Tue, 11 Mar 2025 08:20:12 +0800 Subject: [PATCH 15/32] reduce code duplication --- test/app/agents/test_agent_reviewer.py | 89 +++++++++++++------------- 1 file changed, 43 insertions(+), 46 deletions(-) diff --git a/test/app/agents/test_agent_reviewer.py b/test/app/agents/test_agent_reviewer.py index 6a7bd0cf291..af95b045eda 100644 --- a/test/app/agents/test_agent_reviewer.py +++ b/test/app/agents/test_agent_reviewer.py @@ -1,7 +1,7 @@ import json import pytest from enum import Enum -from app.agents.agent_reviewer import extract_review_result +from app.agents.agent_reviewer import extract_review_result # Assuming this gets updated below # --- Dummy Definitions for Testing --- @@ -28,23 +28,25 @@ def __eq__(self, other): self.test_advice == other.test_advice ) -# --- Function Under Test --- +# --- Refactored Function Under Test --- def extract_review_result(content: str) -> Review | None: try: data = json.loads(content) + def get_decision(key: str) -> ReviewDecision: + return ReviewDecision(data[key].lower()) + review = Review( - patch_decision=ReviewDecision(data["patch-correct"].lower()), + patch_decision=get_decision("patch-correct"), patch_analysis=data["patch-analysis"], patch_advice=data["patch-advice"], - test_decision=ReviewDecision(data["test-correct"].lower()), + test_decision=get_decision("test-correct"), test_analysis=data["test-analysis"], test_advice=data["test-advice"], ) - if ( - (review.patch_decision == ReviewDecision.NO) and not review.patch_advice - ) and ((review.test_decision == ReviewDecision.NO) and not review.test_advice): + if (review.patch_decision == ReviewDecision.NO and not review.patch_advice and + review.test_decision == ReviewDecision.NO and not review.test_advice): return None return review @@ -52,46 +54,41 @@ def extract_review_result(content: str) -> Review | None: except Exception: return None -# --- Pytest Unit Tests --- - -def test_extract_valid_review(): - """Test that valid JSON input returns a proper Review instance.""" - content = json.dumps({ - "patch-correct": "Yes", - "patch-analysis": "Patch analysis text", - "patch-advice": "Patch advice text", - "test-correct": "No", - "test-analysis": "Test analysis text", - "test-advice": "Some test advice" - }) - - review = extract_review_result(content) - expected_review = Review( - patch_decision=ReviewDecision.YES, - patch_analysis="Patch analysis text", - patch_advice="Patch advice text", - test_decision=ReviewDecision.NO, - test_analysis="Test analysis text", - test_advice="Some test advice" - ) - assert review == expected_review - -def test_extract_invalid_due_to_empty_advice(): - """ - Test that when both patch and test decisions are 'No' and their corresponding advice are empty, - the function returns None. - """ - content = json.dumps({ - "patch-correct": "No", - "patch-analysis": "Patch analysis text", - "patch-advice": "", - "test-correct": "No", - "test-analysis": "Test analysis text", - "test-advice": "" - }) - +# --- Combined Pytest Unit Tests Using Parameterization --- +@pytest.mark.parametrize("content,expected", [ + ( + json.dumps({ + "patch-correct": "Yes", + "patch-analysis": "Patch analysis text", + "patch-advice": "Patch advice text", + "test-correct": "No", + "test-analysis": "Test analysis text", + "test-advice": "Some test advice" + }), + Review( + patch_decision=ReviewDecision.YES, + patch_analysis="Patch analysis text", + patch_advice="Patch advice text", + test_decision=ReviewDecision.NO, + test_analysis="Test analysis text", + test_advice="Some test advice" + ) + ), + ( + json.dumps({ + "patch-correct": "No", + "patch-analysis": "Patch analysis text", + "patch-advice": "", + "test-correct": "No", + "test-analysis": "Test analysis text", + "test-advice": "" + }), + None + ), +]) +def test_extract_review_valid_and_invalid(content, expected): review = extract_review_result(content) - assert review is None + assert review == expected def test_extract_invalid_json(): """Test that invalid JSON input returns None.""" From fab4c16404a977500f4a5200ebd6d4e5e0a7f9a9 Mon Sep 17 00:00:00 2001 From: WangGLJoseph Date: Tue, 11 Mar 2025 08:27:50 +0800 Subject: [PATCH 16/32] add temp dir in workflow for coverage report --- .github/workflows/pytest.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index c46299afb2f..992b8f3f6ed 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -28,6 +28,9 @@ jobs: - name: Run PyTest with Coverage (inside docker) run: docker exec acr-pytest conda run --no-capture-output -n auto-code-rover pytest --cov=app test/ --cov-report=term --cov-report=xml + - name: Create coverage reports directory + run: mkdir -p .coverage-reports + # Saving the coverage report to Ubuntu VM as converage.xml (copies it from inside the container) - name: Saving coverage report run: docker cp acr-pytest:/opt/auto-code-rover/coverage.xml .coverage-reports/coverage.xml From 26a154f158d0629e188cf1b4897651f5dbba05b5 Mon Sep 17 00:00:00 2001 From: WangGLJoseph Date: Tue, 11 Mar 2025 09:29:31 +0800 Subject: [PATCH 17/32] update sonar config --- .github/workflows/pytest.yml | 5 +---- sonar-project.properties | 2 ++ 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 992b8f3f6ed..ac5b6644c7f 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -28,12 +28,9 @@ jobs: - name: Run PyTest with Coverage (inside docker) run: docker exec acr-pytest conda run --no-capture-output -n auto-code-rover pytest --cov=app test/ --cov-report=term --cov-report=xml - - name: Create coverage reports directory - run: mkdir -p .coverage-reports - # Saving the coverage report to Ubuntu VM as converage.xml (copies it from inside the container) - name: Saving coverage report - run: docker cp acr-pytest:/opt/auto-code-rover/coverage.xml .coverage-reports/coverage.xml + run: docker cp acr-pytest:/opt/auto-code-rover/coverage.xml coverage.xml # To enable SonarQube Scan, the following steps are required (admin access required) # 1) Create a new project in SonarQube for the Repo diff --git a/sonar-project.properties b/sonar-project.properties index 1fc8f2852f9..f3f2d449bec 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -2,3 +2,5 @@ sonar.exclusions=conf/**, demo_vis/**, results/**, scripts/** sonar.organization=autocoderoversg sonar.projectKey=AutoCodeRoverSG_auto-code-rover sonar.python.coverage.reportPaths=coverage.xml +sonar.tests=test/ +sonar.verbose=true \ No newline at end of file From a9535b9d842f0bf8c0d73e603a1e27882b44b7ec Mon Sep 17 00:00:00 2001 From: WangGLJoseph Date: Tue, 11 Mar 2025 09:39:25 +0800 Subject: [PATCH 18/32] use tox and pytest outside container --- .github/workflows/pytest.yml | 38 +++++++++++++++++++++++------------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index ac5b6644c7f..d1c38ddde82 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -17,20 +17,30 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 - - name: Build docker image - run: docker build -f Dockerfile.minimal -t acr . - - - name: Start docker image (background) - run: docker run --name acr-pytest -t -d acr - - # Due to diffuculties with `conda activate` in docker, we do `conda run` while specifying the environment - # setting cov-report to term and xml -> outputs coverage report to terminal, and an xml file inside the container - - name: Run PyTest with Coverage (inside docker) - run: docker exec acr-pytest conda run --no-capture-output -n auto-code-rover pytest --cov=app test/ --cov-report=term --cov-report=xml - - # Saving the coverage report to Ubuntu VM as converage.xml (copies it from inside the container) - - name: Saving coverage report - run: docker cp acr-pytest:/opt/auto-code-rover/coverage.xml coverage.xml + # - name: Build docker image + # run: docker build -f Dockerfile.minimal -t acr . + + # - name: Start docker image (background) + # run: docker run --name acr-pytest -t -d acr + + # # Due to diffuculties with `conda activate` in docker, we do `conda run` while specifying the environment + # # setting cov-report to term and xml -> outputs coverage report to terminal, and an xml file inside the container + # - name: Run PyTest with Coverage (inside docker) + # run: docker exec acr-pytest conda run --no-capture-output -n auto-code-rover pytest --cov=app test/ --cov-report=term --cov-report=xml + + # # Saving the coverage report to Ubuntu VM as converage.xml (copies it from inside the container) + # - name: Saving coverage report + # run: docker cp acr-pytest:/opt/auto-code-rover/coverage.xml coverage.xml + + - name: Setup Python + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python }} + + - name: Install tox and any other packages + run: pip install tox + - name: Run tox + run: tox -e py # To enable SonarQube Scan, the following steps are required (admin access required) # 1) Create a new project in SonarQube for the Repo From f06933711c9bb1f7fce04cb2ef9a407c979e4b8c Mon Sep 17 00:00:00 2001 From: WangGLJoseph Date: Tue, 11 Mar 2025 09:44:31 +0800 Subject: [PATCH 19/32] add missing tox.ini file --- tox.ini | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 tox.ini diff --git a/tox.ini b/tox.ini new file mode 100644 index 00000000000..36f44d268bd --- /dev/null +++ b/tox.ini @@ -0,0 +1,10 @@ +[tox] +envlist = py39 +skipsdist = True +  +[testenv] +deps = +    pytest +    pytest-cov +commands = pytest --cov=my_project --cov-report=xml --cov-config=tox.ini --cov-branch +  From 19a8418d139a49d14d59811c16abe50ae727e232 Mon Sep 17 00:00:00 2001 From: WangGLJoseph Date: Tue, 11 Mar 2025 09:51:32 +0800 Subject: [PATCH 20/32] separate pytest and build workflow --- .github/workflows/build.yaml | 29 ++++++++++++++++ .github/workflows/pytest.yml | 66 ++++++++++++++++-------------------- 2 files changed, 58 insertions(+), 37 deletions(-) create mode 100644 .github/workflows/build.yaml diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml new file mode 100644 index 00000000000..294ea2937e7 --- /dev/null +++ b/.github/workflows/build.yaml @@ -0,0 +1,29 @@ +name: Build Docker Image + +on: + push: + branches: + - main + - pytest-ci + pull_request: + branches: + - main + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Build docker image + run: docker build -f Dockerfile.minimal -t acr . + + - name: Start docker image (background) + run: docker run --name acr-pytest -t -d acr + + # Due to diffuculties with `conda activate` in docker, we do `conda run` while specifying the environment + # setting cov-report to term and xml -> outputs coverage report to terminal, and an xml file inside the container + - name: Run PyTest with Coverage (inside docker) + run: docker exec acr-pytest conda run --no-capture-output -n auto-code-rover pytest --cov=app test/ --cov-report=term --cov-report=xml diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index d1c38ddde82..ff061ec2d85 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -14,40 +14,32 @@ jobs: runs-on: ubuntu-latest steps: - - name: Checkout repository - uses: actions/checkout@v4 - - # - name: Build docker image - # run: docker build -f Dockerfile.minimal -t acr . - - # - name: Start docker image (background) - # run: docker run --name acr-pytest -t -d acr - - # # Due to diffuculties with `conda activate` in docker, we do `conda run` while specifying the environment - # # setting cov-report to term and xml -> outputs coverage report to terminal, and an xml file inside the container - # - name: Run PyTest with Coverage (inside docker) - # run: docker exec acr-pytest conda run --no-capture-output -n auto-code-rover pytest --cov=app test/ --cov-report=term --cov-report=xml - - # # Saving the coverage report to Ubuntu VM as converage.xml (copies it from inside the container) - # - name: Saving coverage report - # run: docker cp acr-pytest:/opt/auto-code-rover/coverage.xml coverage.xml - - - name: Setup Python - uses: actions/setup-python@v2 - with: - python-version: ${{ matrix.python }} - - - name: Install tox and any other packages - run: pip install tox - - name: Run tox - run: tox -e py - - # To enable SonarQube Scan, the following steps are required (admin access required) - # 1) Create a new project in SonarQube for the Repo - # 2) Generate a token for the project and save it as SONAR_TOKEN in the repo secrets - # 3) To obtain test coverage data, modify the sonar-project.properties file in the root of the repo (Project key, Organization, Coverage report path) - # 4) Uncomment the following lines - - name: SonarQube Scan - uses: SonarSource/sonarqube-scan-action@v4 - env: - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Miniconda + uses: conda-incubator/setup-miniconda@v2 + with: + # Use your environment.yml to create the conda environment. + environment-file: environment.yml + # Optionally specify the environment name as defined in environment.yml. + activate-environment: auto-code-rover-env + # Set the python version (adjust if needed) + python-version: 3.10 + auto-update-conda: true + + - name: Set PYTHONPATH + # Mimic the Dockerfile's ENV setting + run: echo "PYTHONPATH=$(pwd)" >> $GITHUB_ENV + + - name: Install tox + # Install tox inside the conda environment + run: conda install -y tox + + - name: Run tox tests + run: tox -e py + + - name: SonarQube Scan + uses: SonarSource/sonarqube-scan-action@v4 + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} From e93d76d3c25ed0a674c17e30ddd1c318b39d196c Mon Sep 17 00:00:00 2001 From: WangGLJoseph Date: Tue, 11 Mar 2025 09:55:35 +0800 Subject: [PATCH 21/32] fix conda env name in workflow --- .github/workflows/pytest.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index ff061ec2d85..16984cb94fd 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -20,11 +20,8 @@ jobs: - name: Setup Miniconda uses: conda-incubator/setup-miniconda@v2 with: - # Use your environment.yml to create the conda environment. environment-file: environment.yml - # Optionally specify the environment name as defined in environment.yml. - activate-environment: auto-code-rover-env - # Set the python version (adjust if needed) + activate-environment: auto-code-rover python-version: 3.10 auto-update-conda: true From 989ffc5d32b901f14e34a452369e021f9d3ecd90 Mon Sep 17 00:00:00 2001 From: WangGLJoseph Date: Tue, 11 Mar 2025 10:04:49 +0800 Subject: [PATCH 22/32] update shell to use login mode for pytest workflow --- .github/workflows/pytest.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 16984cb94fd..68f22113203 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -10,8 +10,11 @@ on: - main jobs: - build: + pytest: runs-on: ubuntu-latest + defaults: + run: + shell: bash -l {0} steps: - name: Checkout repository @@ -24,6 +27,10 @@ jobs: activate-environment: auto-code-rover python-version: 3.10 auto-update-conda: true + auto-activate-base: false + - run: | + conda info + conda list - name: Set PYTHONPATH # Mimic the Dockerfile's ENV setting From 8e6ac7891d8ee9aa3dbd40c08d0b323ca341327a Mon Sep 17 00:00:00 2001 From: WangGLJoseph Date: Tue, 11 Mar 2025 10:14:12 +0800 Subject: [PATCH 23/32] update conda config --- .github/workflows/pytest.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 68f22113203..101efc9f62d 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -25,8 +25,8 @@ jobs: with: environment-file: environment.yml activate-environment: auto-code-rover - python-version: 3.10 - auto-update-conda: true + python-version: 3.12 + auto-update-conda: false auto-activate-base: false - run: | conda info From 301dd2630c9a2e2a1ee077036288f62d3b215619 Mon Sep 17 00:00:00 2001 From: WangGLJoseph Date: Tue, 11 Mar 2025 10:21:17 +0800 Subject: [PATCH 24/32] set PYTHONPATH inside tox.ini --- tox.ini | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index 36f44d268bd..99850778274 100644 --- a/tox.ini +++ b/tox.ini @@ -3,8 +3,9 @@ envlist = py39 skipsdist = True   [testenv] +passenv = PYTHONPATH deps =     pytest     pytest-cov -commands = pytest --cov=my_project --cov-report=xml --cov-config=tox.ini --cov-branch -  +commands = pytest --cov=app test/ --cov-report=term --cov-report=xml --cov-config=tox.ini --cov-branch +  \ No newline at end of file From 9b041ff7646aab9b339b789648600f62ae7864ea Mon Sep 17 00:00:00 2001 From: WangGLJoseph Date: Tue, 11 Mar 2025 10:31:22 +0800 Subject: [PATCH 25/32] update tox.ini --- tox.ini | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/tox.ini b/tox.ini index 99850778274..11b770eb31d 100644 --- a/tox.ini +++ b/tox.ini @@ -1,11 +1,9 @@ [tox] -envlist = py39 +envlist = py312 skipsdist = True   [testenv] +skip_install = True passenv = PYTHONPATH -deps = -    pytest -    pytest-cov -commands = pytest --cov=app test/ --cov-report=term --cov-report=xml --cov-config=tox.ini --cov-branch -  \ No newline at end of file +commands = + pytest --cov=app test/ --cov-report=term --cov-report=xml --cov-config=tox.ini --cov-branch From 56550c4aa7c218875586a3c799115ed510da4d43 Mon Sep 17 00:00:00 2001 From: WangGLJoseph Date: Tue, 11 Mar 2025 10:42:37 +0800 Subject: [PATCH 26/32] include Coverage.py in tox --- tox.ini | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/tox.ini b/tox.ini index 11b770eb31d..8b216a242d8 100644 --- a/tox.ini +++ b/tox.ini @@ -3,7 +3,14 @@ envlist = py312 skipsdist = True   [testenv] -skip_install = True -passenv = PYTHONPATH +deps = + pytest + coverage commands = - pytest --cov=app test/ --cov-report=term --cov-report=xml --cov-config=tox.ini --cov-branch + coverage run -m pytest + coverage xml + +[coverage:run] +relative_files = True +source = app/ +branch = True From 598a5e65e45cc741ac6da76d1ac09355cfb1110f Mon Sep 17 00:00:00 2001 From: WangGLJoseph Date: Tue, 11 Mar 2025 10:54:21 +0800 Subject: [PATCH 27/32] add passenv to tox --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index 8b216a242d8..7df4e3aca78 100644 --- a/tox.ini +++ b/tox.ini @@ -3,6 +3,7 @@ envlist = py312 skipsdist = True   [testenv] +passenv = PYTHONPATH deps = pytest coverage From 86a1c22623ff9f387484668cea952b4d0e33f16e Mon Sep 17 00:00:00 2001 From: WangGLJoseph Date: Tue, 11 Mar 2025 11:00:06 +0800 Subject: [PATCH 28/32] change build yaml to only run on PR merge --- .github/workflows/build.yaml | 1 - tox.ini | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 294ea2937e7..0abb69532c8 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -4,7 +4,6 @@ on: push: branches: - main - - pytest-ci pull_request: branches: - main diff --git a/tox.ini b/tox.ini index 7df4e3aca78..11d2cc3b157 100644 --- a/tox.ini +++ b/tox.ini @@ -4,6 +4,7 @@ skipsdist = True   [testenv] passenv = PYTHONPATH +skip_install = True deps = pytest coverage From 67193016fa32a9ebd53f7a111981b3530328b599 Mon Sep 17 00:00:00 2001 From: WangGLJoseph Date: Tue, 11 Mar 2025 11:12:03 +0800 Subject: [PATCH 29/32] remove redundant deps in tox.ini --- tox.ini | 3 --- 1 file changed, 3 deletions(-) diff --git a/tox.ini b/tox.ini index 11d2cc3b157..9380a18423d 100644 --- a/tox.ini +++ b/tox.ini @@ -5,9 +5,6 @@ skipsdist = True [testenv] passenv = PYTHONPATH skip_install = True -deps = - pytest - coverage commands = coverage run -m pytest coverage xml From a9a804cad3f82bdcb8eb8fccdfbd870ab4fac9ed Mon Sep 17 00:00:00 2001 From: WangGLJoseph Date: Tue, 11 Mar 2025 11:36:20 +0800 Subject: [PATCH 30/32] abort Sonarqube Scan if coverage.xml is missing --- .github/workflows/pytest.yml | 7 +++++++ sonar-project.properties | 1 - 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 101efc9f62d..9e65b8b326d 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -43,6 +43,13 @@ jobs: - name: Run tox tests run: tox -e py + - name: Check Coverage Report Exists + run: | + if [ ! -f coverage.xml ]; then + echo "coverage.xml not found! Aborting SonarQube scan." + exit 1 + fi + - name: SonarQube Scan uses: SonarSource/sonarqube-scan-action@v4 env: diff --git a/sonar-project.properties b/sonar-project.properties index f3f2d449bec..5127373e7e9 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -2,5 +2,4 @@ sonar.exclusions=conf/**, demo_vis/**, results/**, scripts/** sonar.organization=autocoderoversg sonar.projectKey=AutoCodeRoverSG_auto-code-rover sonar.python.coverage.reportPaths=coverage.xml -sonar.tests=test/ sonar.verbose=true \ No newline at end of file From 2d1c671fea54b9c754b72a9bc63dba72932d27bf Mon Sep 17 00:00:00 2001 From: WangGLJoseph Date: Tue, 11 Mar 2025 12:38:16 +0800 Subject: [PATCH 31/32] specify source and test files for sonar --- sonar-project.properties | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sonar-project.properties b/sonar-project.properties index 5127373e7e9..d4e32077514 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -2,4 +2,6 @@ sonar.exclusions=conf/**, demo_vis/**, results/**, scripts/** sonar.organization=autocoderoversg sonar.projectKey=AutoCodeRoverSG_auto-code-rover sonar.python.coverage.reportPaths=coverage.xml +sonar.sources=app/ +sonar.tests=test/ sonar.verbose=true \ No newline at end of file From f531635f8a26c2735d642f39062d6706d38a3942 Mon Sep 17 00:00:00 2001 From: WangGLJoseph Date: Tue, 11 Mar 2025 13:50:45 +0800 Subject: [PATCH 32/32] add test_search_utils --- TESTING.md | 26 ++++++ app/search/search_utils.py | 1 + test/app/search/test_search_utils.py | 113 +++++++++++++++++++++++++++ tox.ini | 1 + 4 files changed, 141 insertions(+) create mode 100644 TESTING.md create mode 100644 test/app/search/test_search_utils.py diff --git a/TESTING.md b/TESTING.md new file mode 100644 index 00000000000..81cc92f2d0f --- /dev/null +++ b/TESTING.md @@ -0,0 +1,26 @@ +# Testing + +This project is configured with CI workflows to execute the testing suite on every PR and push to the `main` branch, as well as pushes to the `pytest-ci` branch. The testing suite is also configured to run locally using the `tox` tool. + +## Setup + +To begin running the tests locally, it is assumed that the `auto-code-rover` environment has already been setup. Refer to the [README.md](README.md) for instructions on how to setup the environment. + +The testing suite uses the following libraries and tools: +- Tox, to configure the tests +- Pytest, to execute the tests +- Coverage, (the Coverage.py tool) to measure the code coverage + +In the `auto-code-rover` environment, install the required libraries by running the following command: + +```bash +conda install -y tox +``` + +and execute the tox commands (configured in `tox.ini`) to run the tests: + +```bash +tox -e py +``` + +The test results and the test coverage report will be displayed in the terminal, with a `coverage.xml` file in the Cobertura format generated in the project's root directory. \ No newline at end of file diff --git a/app/search/search_utils.py b/app/search/search_utils.py index aa38406329e..14c7bfcf6e5 100644 --- a/app/search/search_utils.py +++ b/app/search/search_utils.py @@ -14,6 +14,7 @@ def is_test_file(file_path: str) -> bool: "test" in Path(file_path).parts or "tests" in Path(file_path).parts or file_path.endswith("_test.py") + or file_path.startswith("test_") ) diff --git a/test/app/search/test_search_utils.py b/test/app/search/test_search_utils.py new file mode 100644 index 00000000000..9c97a6b1f24 --- /dev/null +++ b/test/app/search/test_search_utils.py @@ -0,0 +1,113 @@ +import ast +import glob +import os +import pytest + +from os.path import join as pjoin + +from app.search.search_utils import is_test_file, find_python_files, parse_class_def_args + +def test_is_test_file(): + # Setup: create a list of test file names + test_files = [ + "test_utils.py", + "test_search_utils.py", + "test_search.py", + "utils_test.py", + "search_utils_test.py", + "search_test.py", + "test/test_utils.py", + ] + # Setup: create a list of non-test file names + non_test_files = [ + "utils.py", + "search_utils.py", + "search.py", + "config/framework.py", + "config/routing.py", + "greatest_common_divisor.py", # This is not a test file, but it has "test" in its name, should not be recognized as a test file + ] + + # Execute and verify: test files should return True, non-test files should return False + for test_file in test_files: + assert is_test_file(test_file), f"{test_file} should be recognized as a test file." + for non_test_file in non_test_files: + assert not is_test_file(non_test_file), f"{non_test_file} should not be recognized as a test file." + + +def test_find_python_files(tmp_path): + # Setup: create a list of file names (python and non-python files) + files = [ + "main.py", + "utils.py", + "test/test_something.py", + "Controller/MonitorJobController.php", + "templates/details.html.twig", + "page.tsx", + "dfs.cpp", + ] + + # The expected list excludes test files (those inside a "test/" directory) + expected_python_files = [ + "main.py", + "utils.py", + ] + + # Create a temporary base directory that avoids pytest discovery conflicts. + base_dir = tmp_path / "files" + base_dir.mkdir() + + # Create each file (ensure that subdirectories are created) + for file in files: + file_path = base_dir / file + file_path.parent.mkdir(parents=True, exist_ok=True) + file_path.write_text("") + + # Execute and verify: only python files inside base_dir should be returned. + python_files = find_python_files(str(base_dir)) + # Convert absolute paths to relative paths for comparison. + python_files_rel = [os.path.relpath(pf, str(base_dir)) for pf in python_files] + python_files_rel.sort() + expected_python_files.sort() + + # Compare lengths + assert len(python_files_rel) == len(expected_python_files), ( + f"Expected {len(expected_python_files)} python files, but got {len(python_files_rel)}." + ) + + # Compare each element + for expected, actual in zip(expected_python_files, python_files_rel): + assert actual == expected, f"Expected {expected}, but got {actual}." + +def test_parse_class_def_args_simple(): + source = "class Foo(B, object):\n pass" + tree = ast.parse(source) + node = tree.body[0] # The ClassDef node for Foo + result = parse_class_def_args(source, node) + # 'B' is returned; 'object' is skipped. + assert result == ["B"] + +def test_parse_class_def_args_type_call(): + source = "class Bar(type('D', (), {})):\n pass" + tree = ast.parse(source) + node = tree.body[0] + result = parse_class_def_args(source, node) + # The source segment for the first argument of the type() call is "'D'" + assert result == ["'D'"] + +def test_parse_class_def_args_mixed(): + source = "class Baz(C, type('E', (), {}), object):\n pass" + tree = ast.parse(source) + node = tree.body[0] + result = parse_class_def_args(source, node) + # The expected bases are "C" from the ast.Name and "'E'" from the type() call. + assert result == ["C", "'E'"] + +def test_parse_class_def_args_only_object(): + source = "class Quux(object):\n pass" + tree = ast.parse(source) + node = tree.body[0] + result = parse_class_def_args(source, node) + # Since only object is used, the result should be an empty list. + assert result == [] + \ No newline at end of file diff --git a/tox.ini b/tox.ini index 9380a18423d..08b543070c0 100644 --- a/tox.ini +++ b/tox.ini @@ -8,6 +8,7 @@ skip_install = True commands = coverage run -m pytest coverage xml + coverage report [coverage:run] relative_files = True