Repository navigation
fix: package-owned testdata selects the owning Go package's tests instead of always falling back - #85
Conversation
…tead of always falling back By Go convention `pkg/testdata/**` is read by the tests in `pkg`: the go tool runs a package's tests with the package's directory as the working directory (DiffCI#83). DiffCI treated every `testdata/` edit as a full-validation fallback, so `internal/parser/testdata/case1.json` reran the whole suite instead of `go test ./internal/parser`. Changes under `<dir>/testdata/` of a package that reported `_test.go` files now select those tests (new `GO_TESTDATA` impact reason). The route is refused, and the earlier full-validation fallback stands, when: - the parent directory is not a discovered Go package, or it has no `_test.go` files (`byOwner` comes from `profile.goTestPackages`, so a legacy cached profile without that evidence fails closed), - a `_`-prefixed or `.`-prefixed directory lies on the way to `testdata` or below it (`_`/`.`-prefixed directories keep falling back, as do `_data` and `.data` siblings), - another package's `_test.go` file names the changed path or its `testdata` directory by reference - checked with the existing reference machinery against both the file path and the directory, so a sibling writing `"../parser/testdata"` (the issue's example) or `"../parser/testdata/case1.json"` blocks the selective route and keeps full validation, - the file is a modeled or reported `go:embed` input: embedding makes it a production runtime input, not only test input (`goEmbedPaths` is checked before the owner route), - a listed `_test.go` file's contents cannot be read: an unresolvable sibling reader cannot be cleared, so ownership is refused. Added to `declaredTests`: a GO profile has no `profile.tests` entries, so a declared `inputs` entry naming any Go path was always stale ("selects no tests") and forced the very fallback the declaration was meant to override. The go adapter's reported `_test.go` files now join the declared candidates, behind globs the repository names, so `inputs` keeps working as the overriding workaround the issue relies on, and its declared tests union with the package-owned selection. Validation: - npm run typecheck, npm run build, npm run audit:boundary: clean - npm test: 791 passed / 8 failed / 0 skipped; every failure is tests/action-verify.test.ts, which fails the same 8/8 on the unmodified tree under Node 24 on Windows - unrelated to this change - fixture additions in tests/repo/go-runtime-inputs.test.ts (51 cases): owned testdata selects `./pkg`; sibling reference to path or directory falls back; cross-package references resolve, `../p/data/...` never matches `p/testdata/...`; ownerless, `_`/`.`-routed and nested-`_`/`.`-prefixed testdata fall back; legacy cached profile and cached round-trip fail closed; embedded testdata stays full with repository contents wired like src/analyze.ts does; declared inputs union with the package tests Docs: release-boundary.md states the selective case and the refused cases; using-diffci.md's Go fallback table records what still lands in the discovery-excluded fallback and documents the package-testdata selection. Unsupported cases (unchanged, still full validation): testdata claimed by an unreadable sibling test; `testdata` below a package without tests; embedded testdata; dynamic path construction (`filepath.Join(dir, ...)`); testdata read via a renamed directory alias. Related to DiffCI#76, closes DiffCI#83. Signed-off-by: Shounak Joshi <shounakjoshi88@gmail.com>
adityankale190895
left a comment
There was a problem hiding this comment.
Thanks for this — the scope matches #83 closely, the refusal table is clear, and the declared-inputs fix for Go profiles is a real bug fix. Typecheck is clean and all 51 cases in tests/repo/go-runtime-inputs.test.ts pass locally.
One blocker, though: the cross-package check can let a test be skipped when it should run, which is the failure DiffCI must not have.
1. Non-test Go readers are not checked (blocker, reproduced)
goPackageTestDataOwnerFor only scans other packages' _test.go files. A shared helper in an ordinary .go file (the common testutil/golden.go pattern) can read another package's testdata on behalf of its callers' tests. Reproduction, appended to go-runtime-inputs.test.ts:
test("non-test Go helper naming testdata", async () => {
const files = {
"p/testdata/settings.json": "expected",
"service/service.go": 'package service\nimport "os"\nfunc Read(string) ([]byte, error) { return os.ReadFile("../p/testdata/settings.json") }\n',
};
await fixture(files, (graph, root) => {
const { impact, plan } = planChange(graph, { path: "p/testdata/settings.json", changeType: "modified" }, { repoPath: root, fileContents: files });
// actual on this branch: SELECTIVE ["p/value_test.go"], fallbackRequired false
// service/service_test.go calls Read() and is skipped; on main this is FULL
});
});Suggestion: scan every Go file of other packages (e.g. profile.goDependencyPaths.sources plus the test files), not only _test.go, and add this case as a fixture.
2. Non-Go readers bypass reference resolution (likely, not reproduced)
The GO_TESTDATA branch in processChange (src/repo/impact.ts, around the new early return) runs before the general non-code reference resolution. Previously the Go fallback made that moot; now a JS/Python test in a mixed repo, or a Go test in an excluded nested module (goExcludedModuleRoots), that names p/testdata/... would no longer be found. Either let the normal reference resolution run as well (union rather than return), or fall back when any reader outside the reported Go packages names the path or its testdata directory.
3. Repeated reads (performance)
For each changed testdata path the loop re-reads every other package's test files through an uncached readRepositoryFile. A golden-file refresh touching hundreds of files in a large Go repo becomes changed-paths × test-files reads. Reading each candidate reader once per analysis and reusing the contents would keep this linear.
4. Minor
The 8 action-verify.test.ts failures you saw are Windows-local and also fail on main; we'll confirm with the Linux CI run.
Happy to re-review once 1 and 2 are addressed — 1 with the fixture above, 2 with at least one non-Go reader fixture.
The package-owned testdata route judged only other packages' `_test.go` files, so a helper in an ordinary `.go` file (the `testutil/golden.go` pattern) reading a package's testdata on behalf of its callers' tests and a test outside every go-reported package (nested module, other language) naming it directly fell through the gap. Since go scopes it, the current adapter puts a package's test files into sourcePaths too, so a package's members are exactly its sourcePaths and the test-file grouping stands in for legacy cached profiles. Readers outside package discovery never take the convention (their tests run with a different working directory), and a reference there disqualifies the route like a cross-package one. Go import clauses are excluded from the scan: importing `example.com/project/p` names the package, not the directories inside it, and counting it would disqualify every sibling's route.
|
Pushed 1. Ordinary non-test Go sources as cross-package readers (fixed): the route now scans every package's full source list, not only its 2. Readers outside every go-reported package (fixed): files no package reported (nested-module tests, other-language tests, ordinary 3. Read amplification (bounded): all requested reads stay on New fixtures in
Local run: affected suite 53/53 green (51 pre-existing + 2 new), typecheck clean. I also noticed |
adityankale190895
left a comment
There was a problem hiding this comment.
Thanks a lot for the quick and careful follow-up, @shounakjoshi88-a11y. becdd40 addresses most of the review well:
- Point 1: ordinary
.gofiles in other packages are now scanned, and theservice/service.gofixture covers it. - Point 2: readers outside the go-reported packages now block the selective route, and the nested-module fixture covers it.
- Point 3: you're right, and I was wrong:
readRepositoryFilealready reads each file once per analysis. Thanks for pointing it out. - Leaving out
importclauses is a good catch. Without it, every package that importspwould blockp's route.
A few things are still left. I found these by reading the code, not by running it.
1. Packages without tests are never scanned (blocker)
goPackageTestDataOwnerFor loops over byOwner, which only holds packages that report _test.go files. A shared helper package with no tests of its own, the usual testutil/golden.go shape, is never in that map. It is also left out of goOutsideReaders, because its files are in goDependencyPaths.sources (knownSources). So this case still plans SELECTIVE ["p/value_test.go"] and skips the callers' tests:
testutil/golden.go package testutil; reads "../p/testdata/settings.json" (no _test.go files)
service/service_test.go calls testutil.Golden(...)
change: p/testdata/settings.json
The new service/service.go fixture passes only because service has its own service_test.go. Suggestion: loop over every package in goSourceFilesByOwner (all reported packages) rather than byOwner, and add a fixture where the helper package has no tests.
2. Non-Go, non-test helpers (smaller, can be a follow-up)
The outside pool only takes .go files and files that match the test matcher. In a mixed repo, a non-test JS or Python module that reads p/testdata/... for the tests that import it is not checked. The early return in processChange also skips the general reference resolution that used to follow such indirect readers. Running that resolution as well, and taking the union with the GO_TESTDATA selection, would cover this.
3. The bare "testdata" literal blocks the route in most real repos (not a safety issue)
referenceTokens("p/testdata") includes the bare testdata token. With ancestors: true, contentNamesPath returns true for a word with no slash. So any other package that uses the common filepath.Join("testdata", name) or os.ReadDir("testdata") blocks p's route. In a repo where two or more packages use their own testdata, the new route would rarely be used. That fails safe, but it limits how much of #83 this delivers. A bare testdata in package q refers to q/testdata, so it could resolve against the reader's own directory and not count as a reference to p/testdata.
4. Minor
- Every changed file under the same
testdatadirectory rescans the same readers. Caching the result perdataDirwould keep a large golden-file refresh cheap. - The branch is behind
main. GitHub still reports it as mergeable. - CI hasn't run yet because the PR is from a fork. We'll approve the workflow run so the Linux results confirm the
action-verifyfailures are Windows-only.
Happy to re-review once 1 is fixed with a fixture. 2 and 3 can go into this PR or a follow-up, whichever you prefer.
…stdata readers The cross-package check looped only over packages reporting \_test.go\ files, so a helper package without tests of its own (the usual \ estutil/golden.go\ shape) was never scanned: its ordinary source sits in goSourceFilesByOwner but not in the test-file grouping, and its files are knownSources, so the outside-readers pool skipped it too (2026-11-09, review of DiffCI#85, point 1). The scan now covers the union of every reported package's members and listed tests, so a testless helper package's reference to another package's testdata disqualifies the selective route; legacy cached profiles without source metadata degrade to the listed tests exactly as before. On the pre-fix tree the new fixture reproduces the predicted miss (SELECTIVE [p/value_test.go] while service's test, which calls the helper, is skipped); with the fix the route is refused. The outside-readers pool also only took .go files and test-matched files, so a non-test source module in a modeled mixed repository went unchecked (2026-11-09, review of DiffCI#85, point 2). The pool now takes every modeled source module (isSourceFilePath) and the python adapter's modeled sources; their references fall back like any other reader outside package discovery, which keeps the GO_TESTDATA reason set clean instead of reintroducing general reference resolution under the route's early return. A Go adapter plus any JavaScript file is itself a mixed-repo global blocker, so no healthy graph holds a JS helper; go + python repositories model cleanly, and the fixture stubs the python adapter's parse results the way the go metadata is stubbed.
|
Pushed a single commit on 1. Helper packages without tests (blocker) — fixed, with the fixture. The cross-package scan now runs over the union of every go-reported package's members and its listed tests ( 2. Non-Go, non-test helpers — in this PR, via the outside-readers pool rather than resolution. Took you up on the second shape: the pool now takes every modeled source module ( 3. Agreed — deferring. The bare 4. On the per- |
adityankale190895
left a comment
There was a problem hiding this comment.
Thanks again, @shounakjoshi88-a11y. 1408f03 resolves the remaining blocker and point 2 as well. I reviewed it by reading the code:
- Point 1 (blocker):
goReadersByPackagenow covers every go-reported package, not just the ones with_test.gofiles. The go adapter reports a package'sGoFileseven when it has no tests, so./testutilgets its own entry, and the package keys use the same./dirform asgoTestPackages. Thetestutil/golden.gofixture covers exactly the case from the last review. - Point 2: the outside pool now includes every modeled source module plus
pythonSources. The import-clause stripping applies only to Go readers, so Python content is scanned in full, and an unreadable reader still refuses the route. Your reason for using the pool instead of general resolution (duplicate evidence rows fromaddAffectedTest) makes sense, and so does using Python rather than JS for the fixture, given the mixed Go/JS blocker. - Points 3 and 4: fine as a follow-up together. Please open an issue for the bare
"testdata"resolution and the per-dataDirmemo so they're tracked.
No rebase needed: a trial merge against current main is clean.
I've approved the Core CI and CodeQL runs for this commit. Approving now, and we'll merge once Linux CI is green, which should also confirm the 8 action-verify failures are Windows-only.
|
Thanks for the quick re-review and approval! Opened #90 to track the bare |
|
Merged in 6e20260 — thank you, @shounakjoshi88-a11y! Three careful rounds, each with fixtures that reproduced the miss before fixing it, made this an easy one to land. Package-owned CI also passed on Two small asks, only if you feel like it:
And thanks for already filing #90 for the bare |
Add the README Contributors entry and the in-docs credit next to the GO_TESTDATA route. Update the fallback wording to match what #85 merged: any code outside the owning package that names the testdata (other packages' sources or tests, helper packages without tests, nested modules, Python helpers) keeps full validation, not only another package's _test.go files. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: adityankale190895 <adityankale1995@gmail.com>
|
Thanks for the review and the merge! Follow-ups for the bare |
Closes #83 (part of #76).
Changed behavior
By Go convention
pkg/testdata/**is read by the tests inpkg: thegotool runs a package's tests with the package's directory as the working directory. DiffCI treated everytestdata/edit as a full-validation fallback, sointernal/parser/testdata/case1.jsonreran the whole suite instead of justgo test ./internal/parser.Changes under
<dir>/testdata/of a package that reported_test.gofiles now select that package's tests (newGO_TESTDATAimpact reason) instead of falling back — the same selection the worked-out convention implies.The route is refused (and the previous full-validation fallback stands) when any of these hold:
_test.gofilesbyOwneris built fromprofile.goTestPackages; legacy cached profiles without that evidence fail closed_-prefixed or.-prefixed directory lies on the way totestdataor below it_data,.data,_hidden, …)_test.gofile names the changed path or itstestdatadirectory by reference"../parser/testdata"literal example; checked through the existing reference machinery (contentNamesPath), so../p/data/...never matchesp/testdata/...go:embedinput_test.gofile's contents cannot be readOne supporting fix: declared
inputson a GO repository never resolved any tests (profile.testsis a JS-runner notion and is empty for Go profiles), so any declaration naming a Go path was always "stale" and forced the very fallback it was meant to override. The go adapter's reported_test.gofiles now join the declared-test candidates, behind globs the repository names — so theinputsworkaround the issue's last Done-when bullet depends on actually overrides the fallback, and its declared tests union with the package-owned selection.Fixture
tests/repo/go-runtime-inputs.test.ts(now 51 cases) exercises the real graph → impact → planner pipeline with syntheticgo listmetadata, no Go toolchain needed:p/testdata/settings.jsonselects./p(p/value_test.go), plansSELECTIVE, skipsserviceandunrelated../p/testdata/settings.jsonor../p/testdata(the issue's example form) keeps full validationorphan/testdata/…,_scratch/p/testdata/…andp/testdata/_hidden/…fall backgoTestPackagesfails closed (both via deletion and via theGraphCacheround-trip)src/analyze.tsdoes (the pre-existing embedded-testdata canary keeps passing)inputsunion with the package-owned tests (DECLARED_INPUT+GO_TESTDATAin oneSELECTIVEplan)Validation results (Node 24.12, Windows 11)
npm ci— 0 vulnerabilitiesnpm run typecheck— cleannpm test— 791 passed / 8 failed / 0 skipped (799). Every failure is intests/action-verify.test.ts, which fails the same 8/8 on the unmodified tree on this machine (verified by stashing this change and re-running the file) — they are spawn/exit-code assertions and are unrelated to this change; CI on Linux runs the same file green onmain.npm run build— cleannpm run audit:boundary— "Release boundary passed: 331 reviewed files"Unsupported cases (still full validation)
_test.gofile whose contents cannot be readtestdatabelow a package without_test.gofiles, and profiles cached before this changefilepath.Join(dir, name)), and directories read through non-literal aliases — the reference check is deliberately literal-only, per the issue's scopeSigned-off-by: Shounak Joshi shounakjoshi88@gmail.com