|
| 1 | +"""Tests for handing the conformance tests fixer's implementation changes to the unit tests fixer. |
| 2 | +
|
| 3 | +When the conformance tests fixer changes implementation code, the unit tests are re-run and fixed next. |
| 4 | +The unit tests fixer must know about that change so it adjusts the unit tests instead of reverting it. |
| 5 | +""" |
| 6 | + |
| 7 | +import os |
| 8 | +import tempfile |
| 9 | +from types import SimpleNamespace |
| 10 | + |
| 11 | +import pytest |
| 12 | + |
| 13 | +import plain_spec |
| 14 | +from memory_management import MemoryManager |
| 15 | +from render_machine.actions import fix_unit_tests as fix_unit_tests_module |
| 16 | +from render_machine.actions.fix_conformance_test import FixConformanceTest |
| 17 | +from render_machine.actions.fix_unit_tests import FixUnitTests |
| 18 | +from render_machine.implementation_code_helpers import ImplementationCodeHelpers |
| 19 | +from render_machine.render_types import ConformanceTestsRunningContext, UnitTestsRunningContext |
| 20 | + |
| 21 | + |
| 22 | +class FakeCodeplainAPI: |
| 23 | + def __init__(self, conformance_fix_response=None, unittests_fix_response=None): |
| 24 | + self.conformance_fix_response = conformance_fix_response |
| 25 | + self.unittests_fix_response = unittests_fix_response or {} |
| 26 | + self.unittests_fix_calls = [] |
| 27 | + |
| 28 | + def fix_conformance_tests_issue(self, *args, **kwargs): |
| 29 | + return self.conformance_fix_response |
| 30 | + |
| 31 | + def fix_unittests_issue(self, *args, **kwargs): |
| 32 | + self.unittests_fix_calls.append(kwargs) |
| 33 | + return self.unittests_fix_response |
| 34 | + |
| 35 | + |
| 36 | +class FakeConformanceTests: |
| 37 | + def fetch_existing_conformance_test_files(self, *args): |
| 38 | + return {}, {} |
| 39 | + |
| 40 | + def store_conformance_tests_files(self, *args): |
| 41 | + pass |
| 42 | + |
| 43 | + |
| 44 | +@pytest.fixture |
| 45 | +def build_folder(): |
| 46 | + with tempfile.TemporaryDirectory() as folder: |
| 47 | + with open(os.path.join(folder, "app.py"), "w", encoding="utf-8") as source_file: |
| 48 | + source_file.write("def add(a, b):\n return a + b\n") |
| 49 | + yield folder |
| 50 | + |
| 51 | + |
| 52 | +@pytest.fixture |
| 53 | +def memory_folder(): |
| 54 | + with tempfile.TemporaryDirectory() as folder: |
| 55 | + yield folder |
| 56 | + |
| 57 | + |
| 58 | +@pytest.fixture(autouse=True) |
| 59 | +def isolate_from_git_and_console(monkeypatch): |
| 60 | + monkeypatch.setattr(ImplementationCodeHelpers, "get_code_diff", staticmethod(lambda *args: {})) |
| 61 | + monkeypatch.setattr(plain_spec, "collect_linked_resources", lambda *args: None) |
| 62 | + monkeypatch.setattr(fix_unit_tests_module.render_utils, "print_inputs", lambda *args: None) |
| 63 | + |
| 64 | + |
| 65 | +def make_conformance_context(): |
| 66 | + return ConformanceTestsRunningContext( |
| 67 | + current_testing_module_name="mod", |
| 68 | + current_testing_frid="1", |
| 69 | + fix_attempts=0, |
| 70 | + conformance_tests_json={"1": {"folder_name": "conformance_1"}}, |
| 71 | + conformance_tests_render_attempts=0, |
| 72 | + current_testing_frid_specifications={}, |
| 73 | + should_prepare_testing_environment=False, |
| 74 | + frid_being_implemented="1", |
| 75 | + ) |
| 76 | + |
| 77 | + |
| 78 | +def make_render_context(api, build_folder, memory_folder, conformance_tests_running_context): |
| 79 | + return SimpleNamespace( |
| 80 | + codeplain_api=api, |
| 81 | + build_folder=build_folder, |
| 82 | + memory_manager=MemoryManager(api, memory_folder), |
| 83 | + conformance_tests=FakeConformanceTests(), |
| 84 | + conformance_tests_running_context=conformance_tests_running_context, |
| 85 | + unit_tests_running_context=UnitTestsRunningContext(fix_attempts=1), |
| 86 | + plain_source_tree={}, |
| 87 | + module_name="mod", |
| 88 | + required_modules=None, |
| 89 | + frid_context=SimpleNamespace(frid="1", linked_resources={}), |
| 90 | + get_required_modules_functionalities=lambda: {}, |
| 91 | + run_state=SimpleNamespace(render_id="test-render-id", unittest_batch_id=1), |
| 92 | + ) |
| 93 | + |
| 94 | + |
| 95 | +def run_conformance_fix(render_context): |
| 96 | + return FixConformanceTest().execute(render_context, {"previous_conformance_tests_issue": "tests failed"}) |
| 97 | + |
| 98 | + |
| 99 | +def test_conformance_fix_of_implementation_code_is_remembered(build_folder, memory_folder): |
| 100 | + api = FakeCodeplainAPI( |
| 101 | + conformance_fix_response=[ |
| 102 | + FixConformanceTest.ISSUE_REASON_CODE_IMPLEMENTATION_CODE, |
| 103 | + {"app.py": "def add(a, b):\n return a + b + 1\n"}, |
| 104 | + {"hypothesis": "off by one", "approach": "add one"}, |
| 105 | + "prepared issue", |
| 106 | + ] |
| 107 | + ) |
| 108 | + ctx = make_conformance_context() |
| 109 | + render_context = make_render_context(api, build_folder, memory_folder, ctx) |
| 110 | + |
| 111 | + outcome, _ = run_conformance_fix(render_context) |
| 112 | + |
| 113 | + assert outcome == FixConformanceTest.IMPLEMENTATION_CODE_UPDATED |
| 114 | + assert len(ctx.implementation_code_fixes) == 1 |
| 115 | + recorded_fix = ctx.implementation_code_fixes[0] |
| 116 | + assert recorded_fix["hypothesis"] == "off by one" |
| 117 | + assert recorded_fix["approach"] == "add one" |
| 118 | + assert list(recorded_fix["code_diff"]) == ["app.py"] |
| 119 | + assert "+ return a + b + 1" in recorded_fix["code_diff"]["app.py"] |
| 120 | + |
| 121 | + |
| 122 | +def test_consecutive_implementation_fixes_accumulate(build_folder, memory_folder): |
| 123 | + ctx = make_conformance_context() |
| 124 | + for attempt in range(2): |
| 125 | + api = FakeCodeplainAPI( |
| 126 | + conformance_fix_response=[ |
| 127 | + FixConformanceTest.ISSUE_REASON_CODE_IMPLEMENTATION_CODE, |
| 128 | + {"app.py": f"def add(a, b):\n return a + b + {attempt + 1}\n"}, |
| 129 | + {"hypothesis": f"hypothesis {attempt}", "approach": f"approach {attempt}"}, |
| 130 | + "prepared issue", |
| 131 | + ] |
| 132 | + ) |
| 133 | + render_context = make_render_context(api, build_folder, memory_folder, ctx) |
| 134 | + run_conformance_fix(render_context) |
| 135 | + |
| 136 | + assert [fix["hypothesis"] for fix in ctx.implementation_code_fixes] == ["hypothesis 0", "hypothesis 1"] |
| 137 | + |
| 138 | + |
| 139 | +def test_conformance_fix_without_summary_is_still_remembered(build_folder, memory_folder): |
| 140 | + api = FakeCodeplainAPI( |
| 141 | + conformance_fix_response=[ |
| 142 | + FixConformanceTest.ISSUE_REASON_CODE_IMPLEMENTATION_CODE, |
| 143 | + {"app.py": "def add(a, b):\n return a + b + 1\n"}, |
| 144 | + None, |
| 145 | + "prepared issue", |
| 146 | + ] |
| 147 | + ) |
| 148 | + ctx = make_conformance_context() |
| 149 | + render_context = make_render_context(api, build_folder, memory_folder, ctx) |
| 150 | + |
| 151 | + run_conformance_fix(render_context) |
| 152 | + |
| 153 | + assert ctx.implementation_code_fixes[0]["hypothesis"] is None |
| 154 | + assert ctx.implementation_code_fixes[0]["approach"] is None |
| 155 | + assert "app.py" in ctx.implementation_code_fixes[0]["code_diff"] |
| 156 | + |
| 157 | + |
| 158 | +def test_conformance_fix_of_conformance_tests_is_not_remembered(build_folder, memory_folder): |
| 159 | + api = FakeCodeplainAPI( |
| 160 | + conformance_fix_response=[ |
| 161 | + FixConformanceTest.ISSUE_REASON_CODE_CONFORMANCE_TESTS, |
| 162 | + {"test_app.py": "def test_add(): pass\n"}, |
| 163 | + {"hypothesis": "wrong assertion", "approach": "fix test"}, |
| 164 | + "prepared issue", |
| 165 | + ] |
| 166 | + ) |
| 167 | + ctx = make_conformance_context() |
| 168 | + render_context = make_render_context(api, build_folder, memory_folder, ctx) |
| 169 | + |
| 170 | + outcome, _ = run_conformance_fix(render_context) |
| 171 | + |
| 172 | + assert outcome == FixConformanceTest.IMPLEMENTATION_CODE_NOT_UPDATED |
| 173 | + assert ctx.implementation_code_fixes == [] |
| 174 | + |
| 175 | + |
| 176 | +def test_implementation_fix_with_no_files_is_not_remembered(build_folder, memory_folder): |
| 177 | + api = FakeCodeplainAPI( |
| 178 | + conformance_fix_response=[ |
| 179 | + FixConformanceTest.ISSUE_REASON_CODE_IMPLEMENTATION_CODE, |
| 180 | + {}, |
| 181 | + {"hypothesis": "nothing", "approach": "nothing"}, |
| 182 | + "prepared issue", |
| 183 | + ] |
| 184 | + ) |
| 185 | + ctx = make_conformance_context() |
| 186 | + render_context = make_render_context(api, build_folder, memory_folder, ctx) |
| 187 | + |
| 188 | + outcome, _ = run_conformance_fix(render_context) |
| 189 | + |
| 190 | + assert outcome == FixConformanceTest.IMPLEMENTATION_CODE_NOT_UPDATED |
| 191 | + assert ctx.implementation_code_fixes == [] |
| 192 | + |
| 193 | + |
| 194 | +def run_unit_tests_fix(render_context): |
| 195 | + return FixUnitTests().execute(render_context, {"previous_unittests_issue": "1 failed"}) |
| 196 | + |
| 197 | + |
| 198 | +def test_unit_tests_fix_forwards_conformance_fixes(build_folder, memory_folder): |
| 199 | + api = FakeCodeplainAPI(unittests_fix_response={"test_app.py": "def test_add(): pass\n"}) |
| 200 | + ctx = make_conformance_context() |
| 201 | + ctx.implementation_code_fixes.append( |
| 202 | + {"hypothesis": "off by one", "approach": "add one", "code_diff": {"app.py": "+ return a + b + 1"}} |
| 203 | + ) |
| 204 | + render_context = make_render_context(api, build_folder, memory_folder, ctx) |
| 205 | + |
| 206 | + outcome, _ = run_unit_tests_fix(render_context) |
| 207 | + |
| 208 | + assert outcome == FixUnitTests.SUCCESSFUL_OUTCOME |
| 209 | + assert len(api.unittests_fix_calls) == 1 |
| 210 | + assert api.unittests_fix_calls[0]["conformance_tests_fixes"] == ctx.implementation_code_fixes |
| 211 | + # The forwarded list is a copy, so later conformance fixes do not mutate what was sent. |
| 212 | + assert api.unittests_fix_calls[0]["conformance_tests_fixes"] is not ctx.implementation_code_fixes |
| 213 | + |
| 214 | + |
| 215 | +def test_unit_tests_fix_outside_conformance_phase_sends_no_fixes(build_folder, memory_folder): |
| 216 | + api = FakeCodeplainAPI() |
| 217 | + render_context = make_render_context(api, build_folder, memory_folder, None) |
| 218 | + |
| 219 | + run_unit_tests_fix(render_context) |
| 220 | + |
| 221 | + assert api.unittests_fix_calls[0]["conformance_tests_fixes"] is None |
| 222 | + |
| 223 | + |
| 224 | +def test_unit_tests_fix_in_conformance_phase_without_implementation_changes_sends_no_fixes(build_folder, memory_folder): |
| 225 | + api = FakeCodeplainAPI() |
| 226 | + render_context = make_render_context(api, build_folder, memory_folder, make_conformance_context()) |
| 227 | + |
| 228 | + run_unit_tests_fix(render_context) |
| 229 | + |
| 230 | + assert api.unittests_fix_calls[0]["conformance_tests_fixes"] is None |
0 commit comments