|
6 | 6 | from pathlib import Path |
7 | 7 | from types import SimpleNamespace |
8 | 8 | from typing import TYPE_CHECKING |
9 | | -from unittest.mock import call |
| 9 | +from unittest.mock import Mock, call |
10 | 10 |
|
11 | 11 | import pytest |
12 | 12 |
|
|
15 | 15 | from tests.conftest import ( |
16 | 16 | _infer_expected_file, |
17 | 17 | assert_exact_directory_content, |
| 18 | + assert_inputs_not_mutated, |
18 | 19 | assert_parser_modules, |
19 | 20 | assert_parser_results, |
20 | 21 | ) |
@@ -94,6 +95,80 @@ def test_assert_parser_modules_rejects_unexpected_module(tmp_path: Path) -> None |
94 | 95 | assert_parser_modules({("sample.py",): "value = 1\n"}, expected_dir) |
95 | 96 |
|
96 | 97 |
|
| 98 | +def test_assert_inputs_not_mutated_allows_unchanged_nested_values() -> None: |
| 99 | + """Mutation guard accepts unchanged dict/list inputs and ignores immutable labels.""" |
| 100 | + schema = {"properties": {"name": {"type": "string"}}, "required": ["name"]} |
| 101 | + |
| 102 | + with assert_inputs_not_mutated({"schema": schema, "description": "ignored"}): |
| 103 | + assert schema["properties"]["name"]["type"] == "string" |
| 104 | + |
| 105 | + |
| 106 | +def test_assert_inputs_not_mutated_reports_nested_mutation() -> None: |
| 107 | + """Mutation guard reports the label for nested dict/list mutations.""" |
| 108 | + schema = {"properties": {"name": {"type": "string"}}, "required": ["name"]} |
| 109 | + |
| 110 | + with ( |
| 111 | + pytest.raises(pytest.fail.Exception, match="schema was mutated"), |
| 112 | + assert_inputs_not_mutated({"schema": schema}), |
| 113 | + ): |
| 114 | + schema["required"].append("age") |
| 115 | + |
| 116 | + |
| 117 | +def test_run_generate_and_assert_detects_mutated_input( |
| 118 | + monkeypatch: pytest.MonkeyPatch, |
| 119 | + tmp_path: Path, |
| 120 | +) -> None: |
| 121 | + """run_generate_and_assert can guard caller-provided dict/list inputs.""" |
| 122 | + schema = {"properties": {"name": {"type": "string"}}, "required": ["name"]} |
| 123 | + |
| 124 | + def fake_generate(*, input_: object, **_: object) -> str: |
| 125 | + assert isinstance(input_, dict) |
| 126 | + input_["required"].append("age") |
| 127 | + return "value = 1\n" |
| 128 | + |
| 129 | + monkeypatch.setattr(main_conftest, "generate", fake_generate) |
| 130 | + assert_output_noop = Mock() |
| 131 | + monkeypatch.setattr(main_conftest, "assert_output", assert_output_noop) |
| 132 | + |
| 133 | + with pytest.raises(pytest.fail.Exception, match="input_ was mutated"): |
| 134 | + main_conftest.run_generate_and_assert( |
| 135 | + input_=schema, |
| 136 | + expected_file=tmp_path / "expected.py", |
| 137 | + assert_input_unchanged=True, |
| 138 | + ) |
| 139 | + assert_output_noop.assert_not_called() |
| 140 | + |
| 141 | + |
| 142 | +def test_run_generate_file_and_assert_detects_mutated_unchanged_input( |
| 143 | + monkeypatch: pytest.MonkeyPatch, |
| 144 | + tmp_path: Path, |
| 145 | +) -> None: |
| 146 | + """run_generate_file_and_assert can guard cached parsed original objects.""" |
| 147 | + cached_schema = {"properties": {"name": {"type": "string"}}, "required": ["name"]} |
| 148 | + input_path = tmp_path / "schema.json" |
| 149 | + output_path = tmp_path / "output.py" |
| 150 | + |
| 151 | + def fake_generate(*, input_: object, **options: object) -> None: |
| 152 | + assert input_ == input_path |
| 153 | + cached_schema["required"].append("age") |
| 154 | + output = options["output"] |
| 155 | + assert isinstance(output, Path) |
| 156 | + output.write_text("value = 1\n", encoding="utf-8") |
| 157 | + |
| 158 | + monkeypatch.setattr(main_conftest, "generate", fake_generate) |
| 159 | + assert_file_noop = Mock() |
| 160 | + |
| 161 | + with pytest.raises(pytest.fail.Exception, match="cached schema was mutated"): |
| 162 | + main_conftest.run_generate_file_and_assert( |
| 163 | + input_path=input_path, |
| 164 | + output_path=output_path, |
| 165 | + assert_func=assert_file_noop, |
| 166 | + expected_file="unused.py", |
| 167 | + unchanged_inputs={"cached schema": cached_schema}, |
| 168 | + ) |
| 169 | + assert_file_noop.assert_not_called() |
| 170 | + |
| 171 | + |
97 | 172 | def test_builtin_parity_mock_call_preservation(mocker: MockerFixture) -> None: |
98 | 173 | """Mock call history is restored after parity-only calls.""" |
99 | 174 | mocked_callable = mocker.Mock() |
|
0 commit comments