|
20 | 20 | """ |
21 | 21 | from __future__ import annotations |
22 | 22 |
|
| 23 | +import json |
23 | 24 | import shutil |
24 | 25 | import tempfile |
25 | 26 | from pathlib import Path |
@@ -130,6 +131,94 @@ def test_scene_has_no_reverse_fks() -> None: |
130 | 131 | assert reverse_fks_for(_ENTITIES["Scene"]) == [] |
131 | 132 |
|
132 | 133 |
|
| 134 | +# --------------------------------------------------------------------------- |
| 135 | +# #368 fallout — a dotted @references ("Entity.field" / "Entity.a,b", the |
| 136 | +# normative explicit-fields form per spec/metamodel/identity.json) must still |
| 137 | +# resolve target_entity to the BARE entity name, not the raw dotted tail. |
| 138 | +# Deliberately not the shared reverse-finders-same-pair fixture — this shape |
| 139 | +# is narrow enough to inline and doesn't need a new cross-port corpus entry. |
| 140 | +# --------------------------------------------------------------------------- |
| 141 | + |
| 142 | + |
| 143 | +def _load_dotted_reference_entities() -> dict[str, MetaObject]: |
| 144 | + data = { |
| 145 | + "metadata.root": { |
| 146 | + "package": "acme::sport", |
| 147 | + "children": [ |
| 148 | + { |
| 149 | + "object.entity": { |
| 150 | + "name": "Team", |
| 151 | + "children": [ |
| 152 | + {"source.rdb": {"@table": "teams"}}, |
| 153 | + {"field.long": {"name": "id"}}, |
| 154 | + {"identity.primary": {"name": "id", "@fields": "id"}}, |
| 155 | + ], |
| 156 | + } |
| 157 | + }, |
| 158 | + { |
| 159 | + "object.entity": { |
| 160 | + "name": "Match", |
| 161 | + "children": [ |
| 162 | + {"source.rdb": {"@table": "matches"}}, |
| 163 | + {"field.long": {"name": "id"}}, |
| 164 | + {"field.long": {"name": "teamFk"}}, |
| 165 | + {"identity.primary": {"name": "id", "@fields": "id"}}, |
| 166 | + { |
| 167 | + "identity.reference": { |
| 168 | + "name": "teamRef", |
| 169 | + "@fields": "teamFk", |
| 170 | + "@references": "acme::sport::Team.id", |
| 171 | + } |
| 172 | + }, |
| 173 | + ], |
| 174 | + } |
| 175 | + }, |
| 176 | + ], |
| 177 | + } |
| 178 | + } |
| 179 | + tmp = Path(tempfile.mkdtemp(prefix="reverse-finders-dotted-")) |
| 180 | + try: |
| 181 | + (tmp / "meta.json").write_text(json.dumps(data)) |
| 182 | + result = MetaDataLoader.from_directory(str(tmp)) |
| 183 | + assert not result.errors, [f"{e.code}: {e.message}" for e in result.errors] |
| 184 | + return { |
| 185 | + c.name: c |
| 186 | + for c in result.root.children() |
| 187 | + if c.type == TYPE_OBJECT and isinstance(c, MetaObject) |
| 188 | + } |
| 189 | + finally: |
| 190 | + shutil.rmtree(tmp, ignore_errors=True) |
| 191 | + |
| 192 | + |
| 193 | +def test_reverse_fks_for_resolves_bare_target_entity_from_dotted_references() -> None: |
| 194 | + """The USER-VISIBLE symptom this guards: ``reverse_fks_for()`` is a public, |
| 195 | + documented function (its ``ReverseFk.target_entity`` is asserted directly by |
| 196 | + ``test_reverse_fks_for_game_session`` above) whose docstring promises "the bare |
| 197 | + target entity (T)". A dotted ``@references`` used to leak the raw tail |
| 198 | + ("Team.id") into that field instead of bareing it to "Team" -- a correctness |
| 199 | + bug in a tested return value, independent of whether any current caller |
| 200 | + happens to consume the field (see the next test: today, none does).""" |
| 201 | + entities = _load_dotted_reference_entities() |
| 202 | + fks = reverse_fks_for(entities["Match"]) |
| 203 | + assert [(f.fk_field, f.target_entity) for f in fks] == [("teamFk", "Team")] |
| 204 | + |
| 205 | + |
| 206 | +def test_router_reverse_finder_name_unaffected_by_dotted_references() -> None: |
| 207 | + """The generated finder METHOD NAME derives only from the FK-holding entity's |
| 208 | + own name + FK field (never from target_entity), so it was already correct |
| 209 | + with a dotted @references even before the fix above -- the router_generator.py |
| 210 | + bug corrupted a returned data field, not the emitted router source.""" |
| 211 | + entities = _load_dotted_reference_entities() |
| 212 | + index = build_object_index(list(entities.values())) |
| 213 | + src = render_router(entities["Match"], index) |
| 214 | + assert src is not None |
| 215 | + assert "def find_matches_by_team_fk(self, team_fk: Any) -> list[Any]: ..." in src |
| 216 | + assert ( |
| 217 | + "def find_matches_by_team_fk_in(self, team_fk_values: list[Any]) -> list[Any]: ..." |
| 218 | + in src |
| 219 | + ) |
| 220 | + |
| 221 | + |
133 | 222 | # --------------------------------------------------------------------------- |
134 | 223 | # Same-pair: THREE distinct GameSession→Scene finders — the collision case. |
135 | 224 | # --------------------------------------------------------------------------- |
|
0 commit comments