Repository navigation
fix: name Laravel container tests in tests_for as guesses (part of #850) - #1176
2 commits merged into
Conversation
A PHP call on an expression, such as $this->service->run(), was bound to a same-named method in the calling file, so a test named after the method it tests was recorded as calling itself. Mark it as an expression call, as Python already does. When several imported classes define the called method, tests_for now lists the test with inferred_by: ambiguous_call, unless the test builds one of those classes itself. The summary counts the guesses, minimal mode keeps inferred_by, and the test-gap summary says PHP gaps are a guess. Guesses never count as coverage. Migration up and down in a migrations or migrate folder are not test gaps. Part of #850. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
code-review-graph reviewOverall risk: 0.85 (CRITICAL) — 37 changed function(s)/class(es), 47 affected flow(s), 6 test gap(s) (1 with no tested caller found, 5 reached only through a caller) Risk-scored changes
Affected execution flows
Test gaps
Reached only through a callerNo test names these directly, but a tested caller reaches them along the call path shown. That is a path in the call graph, not a record of execution -- the caller's tests may never take this branch. Treat it as where to look, not as coverage: these count as gaps and are scored as untested.
Powered by code-review-graph — local-first analysis; no code leaves the CI runner. |
There was a problem hiding this comment.
🟡 Changes recommended
The test-wide constructor exclusion can incorrectly skip unrelated ambiguous calls, causing tests_for to return zero despite a relevant test.
1 open issue
What changed in this PR
This PR partially addresses #850 by labeling uncertain PHP test links and reducing migration-related test-gap noise. Full Laravel container resolution remains deferred to #851.
Changes:
- Prevents PHP expression-receiver calls from binding to same-named local methods.
- Adds labeled guesses to
tests_for, preserving labels in minimal output without counting guesses as coverage. - Exempts migration
up/downmethods from test gaps and adds a PHP uncertainty notice.
| File | Description |
|---|---|
tests/test_test_gap_exemptions.py |
Tests migration exemptions and repository path boundaries. |
tests/test_php_guessed_test_links.py |
Tests PHP guessed links, labels, exclusions, and coverage separation. |
docs/FAQ.md |
Explains guessed links versus coverage. |
docs/COMMANDS.md |
Documents inference labels in tests_for output. |
code_review_graph/tools/review.py |
Passes repository roots into gap-exemption checks. |
code_review_graph/tools/query.py |
Adds guessed tests, summary counts, and minimal-mode labels. |
code_review_graph/parser.py |
Marks PHP expression receivers to avoid incorrect local binding. |
code_review_graph/graph.py |
Looks up PHP tests with ambiguous call candidates. |
code_review_graph/changes.py |
Adds migration exemptions and PHP gap uncertainty text. |
CHANGELOG.md |
Records behavior changes and the full-build requirement. |
Review effort: Balanced
| owners = built(test) & { | ||
| owner(c) for c in candidates if isinstance(c, str) | ||
| } | ||
| if owners and owner(symbol_qn) not in owners: | ||
| continue |
There was a problem hiding this comment.
Fixed in 923962a: a class built on the call's own line now settles only that call, so app(SyncBooks::class)->handle() keeps its guess; a class built on a line with no such call, as in $job = new X() and a later $job->withBatchId($id)->handle() (seen on monica), still settles the others. Both shapes have a test, and guess counts on koel and monica are unchanged.
A test that ran (new UpdateBooks)->handle() and then app(SyncBooks::class)->handle() lost the guess for SyncBooks::handle: any class the test built hid the guess for every ambiguous call in it. A class built on a call's own line now settles only that call. A class built on a line with no such call, as in $job = new X() and a later $job->withBatchId($id)->handle() (seen on monica), still settles the test's other calls. Guess counts on koel and monica are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Part of #850. This is the cheap half the triage named: migration
up/down, and a label sotests_forsays when a test link is a guess. Resolving Laravel container calls properly is #851 and is not done here.What was wrong
Laravel tests reach a class through the container, so the file never says which class a call runs on:
Two things went wrong, both seen on koel/koel:
$this->service->x(),app(X::class)->x()) has no named receiver, so the parser bound it to a method of the same name in the calling file. PHPUnit tests are often named after the method they test, so the test was recorded as calling itself. koel had 113 such edges from tests. App code has the same bug, for example a controllerinvite()calling$this->invitationService->invite(). Python already marks these calls asreceiver_form: expression; PHP now does too.$completion->execute()), the call stays unresolved with its candidates inambiguous_targets, andtests_forgave 0. Triage saw exactly this:tests_for execute= 0 with bareTESTED_BYedges.What changed
parser.py: PHP member calls on an expression getreceiver_form: "expression"(one line).graph.py: newGraphStore.get_ambiguous_call_tests(), PHP only. A test whose ambiguous call lists this method is returned, unless the test builds one of the other candidates itself ((new UpdateBooks)->handle()is taken to meanUpdateBooks).tools/query.py:tests_forlists those tests withinferred_by: "ambiguous_call". The summary says how many rows are guesses, and minimal mode now keepsinferred_byontests_for(it dropped the existingnaming_conventionlabel too).changes.py: a guess is never coverage, so gaps and risk do not change; the summary adds one line when PHP gaps remain.up/downare not gaps in amigrationsormigratefolder inside the repository (read from the repo-relative path, as test folders are).Easy to merge with #1154:
git merge-treeof this branch and #1154's head is clean. The newtests_forblock sits above the naming-convention block #1154 edits, and the CHANGELOG entry is at the end of Fixed.Proof
Shallow clones of koel/koel (2,809 files, 475 test files) and monicahq/monica (1,938 files, 516 test files), plus laravel/laravel, built with
origin/stagingand with this branch, each into its ownCRG_DATA_DIR.PYTHONHASHSEED=0for every query (see the note at the end).tests_for, before → after, checked against the test source:app/Services/Geolocation/IPinfoServicesetUpdoesapp(IPinfoService::class); both tests call$this->service->getCountryCodeFromIp(). Both right. The first test is itself namedgetCountryCodeFromIp, so before, its call was bound to itself.app/Services/Image/SvgSanitizersanitizecalls$this->service->sanitize()onnew SvgSanitizer(...). Right.app/Services/Integrations/TicketmasterService$this->service->searchEventForArtist()onapp(TicketmasterService::class). All right.app/Services/ScrobbleServicescrobble,updateNowPlayingorhasConnectedScrobbleron$this->service = new ScrobbleService(...). All right.app/Services/Integrations/ListenBrainzServiceListenBrainzServiceTesttests calling$this->service->...onapp(ListenBrainzService::class): right. 3 areScrobbleServiceTesttests, whereListenBrainzServiceis a Mockery mock: wrong, which is why they say they are guesses.app/Domains/Contact/DavClient/Jobs/SynchronizeAddressBooks(job)UpdateAddressBooksTestruns(new UpdateAddressBooks)->handle()underQueue::fake(), so this job'shandlenever runs. Before the "builds one of the classes" rule this gave 4 wrong guesses; with it, none.The issue's own shape (
$completion = app(CompleteSupplierCount::class); $completion->execute(1), with a second imported class that also hasexecute), in a small repo:tests_for app/Actions/CompleteSupplierCount.phpgave 0 before and 1 guess after (test_it_completes,inferred_by: ambiguous_call).Survey of every class under
app/Servicesandapp/Jobs(koel) andapp/Domains,app/Services,app/Actions(monica):tests_forgives 0tests_forgives 0 though a test file names the classCALLSedges from a test to itselfThe 15 self-calls left on koel come from the post-build resolver, when the test's own method is the only candidate the file backs; that is separate from this change.
detect_changeson two koel migrations plusIPinfoService.php, before → after:Tests
tests/test_php_guessed_test_links.py(new, 10 tests): the self-call, the guess for a method, a class and a file, the label in minimal mode, a pinned call without a label, no guess without an import, no guess when the test builds another candidate, guesses not counted as coverage, PHP only.tests/test_test_gap_exemptions.py: 4 migration tests, including a checkout under amigrationsfolder.staging. The ones that guard against over-reach (no guess without an import, no label on a pinned call, no guess when the test builds another candidate,upoutside a migration folder) pass on both.Checks
uv run ruff check code_review_graph/: clean.uv run --extra dev python -m mypy code_review_graph/ --ignore-missing-imports --no-strict-optional: no issues in 85 files.uv run pytest tests/: all pass. The machine was under heavy load from other runs, so the suite ran in parts; one timing test,test_cpp_header_prefilter.py::test_text_check_stays_fast_on_hostile_input[enum], failed once under that load and passes when run alone (39 of 39 in that file).Left for #851
Calls through
X::dispatch(),$this->post(...)and constructor injection are still not traced; a test reached only that way still shows no link.detect_changesstill counts these as gaps, now with a line saying PHP gaps are a guess.Found on the way, not fixed here
tests_foron a large class is not stable between runs:get_transitive_testscuts each hop to 50 callees taken from a Pythonset, so which callees survive depends on string hashing. On koel'sPodcastService.php, three runs of the unchangedstagingcode gave 58, 51 and 19 results.🤖 Generated with Claude Code