diff --git a/toolchains/go/CHANGELOG.md b/toolchains/go/CHANGELOG.md index dcaa9049..c5f3e171 100644 --- a/toolchains/go/CHANGELOG.md +++ b/toolchains/go/CHANGELOG.md @@ -8,6 +8,7 @@ - Sibling modules required by version without a `go.work` no longer create project relationships, since those builds consume the published module rather than the local source. When the `go` binary is unavailable, projects with their own `go.mod` under a workspace `go.work` fall back to resolving relationships from their direct requires. - `replace` directives keep their meaning in the new model: a require replaced by a local directory always links to the project at that location (it consumes local source even without a `go.work`), while a require replaced by another module never links. - Imports within a project's own import path are treated as ownership rather than dependencies. `go list -deps ./...` enumerates packages belonging to projects nested inside the scanned project, which previously inferred an edge from the parent to every nested child — forming a cycle whenever a child declared `dependsOn` on its parent. +- Test binary pseudo-packages are no longer inferred as relationships. `go list -deps -test` reports a synthetic `pkg.test` package for each tested package; its `.test` suffix kept it from matching the package under test, so it resolved to whatever ancestor project it nested under (typically the module root) as a phantom development edge. It is now reduced to the real package path and recognised as ownership. ## 1.4.7 diff --git a/toolchains/go/src/package_graph.rs b/toolchains/go/src/package_graph.rs index 5c64ddbe..c354a94f 100644 --- a/toolchains/go/src/package_graph.rs +++ b/toolchains/go/src/package_graph.rs @@ -40,9 +40,15 @@ fn execute_go_list(dir: &VirtualPath, packages: &[String], test: bool) -> AnyRes .stdout .lines() .filter_map(|line| { - // Test binary pseudo-packages render as `pkg [pkg.test]`; - // only the real package path participates in matching. + // `go list -deps -test` reports a package's test artifacts: the + // package compiled into its test binary as `pkg [pkg.test]`, and + // the synthetic test binary itself as a bare `pkg.test`. Both + // describe the package under test, not a new import — reduce them + // to the real package path so ownership filtering claims them + // rather than leaking an edge to whatever project the `.test` + // path happens to nest under (e.g. the module root). let import_path = line.trim().split(' ').next().unwrap_or_default(); + let import_path = import_path.strip_suffix(".test").unwrap_or(import_path); (!import_path.is_empty()).then(|| import_path.to_owned()) }) diff --git a/toolchains/go/tests/__fixtures__/projects-single-module/libs/b/lib_test.go b/toolchains/go/tests/__fixtures__/projects-single-module/libs/b/lib_test.go new file mode 100644 index 00000000..d5a2540f --- /dev/null +++ b/toolchains/go/tests/__fixtures__/projects-single-module/libs/b/lib_test.go @@ -0,0 +1,7 @@ +package b + +import "testing" + +func TestB(t *testing.T) { + B() +} diff --git a/toolchains/go/tests/tier2_test.rs b/toolchains/go/tests/tier2_test.rs index 6b0569be..0ad1dc8d 100644 --- a/toolchains/go/tests/tier2_test.rs +++ b/toolchains/go/tests/tier2_test.rs @@ -542,6 +542,57 @@ mod go_toolchain_tier2 { }) ); } + + #[tokio::test(flavor = "multi_thread")] + async fn doesnt_infer_edges_to_root_from_test_binary_pseudo_packages() { + let sandbox = create_moon_sandbox("projects-single-module"); + let plugin = sandbox.create_toolchain("go").await; + + let mut input = ExtendProjectGraphInput::default(); + // The workspace root owns the module's `go.mod`, so it is + // itself a project — as in a single-module monorepo whose repo + // root is a project that everything nests under. + input.project_sources.insert(Id::raw("root"), ".".into()); + input.project_sources.insert(Id::raw("a"), "apps/a".into()); + input.project_sources.insert(Id::raw("b"), "libs/b".into()); + input.toolchain_config = json!({ + "inferRelationships": true, + "inferRelationshipsFromTests": true + }); + + let output = plugin.extend_project_graph(input).await; + + // `go list -deps -test ./...` emits a synthetic `.test` + // main package for every tested package (here libs/b). Its + // import path is the package path with a bare `.test` suffix, + // so it isn't nested under the owning project and slips past + // ownership filtering — yet it is still prefixed by the root + // module path. It must be discarded, not resolved to the root + // project as a phantom development edge (`b -> root`). + assert_eq!( + output.extended_projects, + BTreeMap::from_iter([ + ( + Id::raw("root"), + ExtendProjectOutput { + alias: Some("example.com/org".into()), + ..Default::default() + } + ), + ( + Id::raw("a"), + ExtendProjectOutput { + dependencies: vec![ProjectDependency { + id: Id::raw("b"), + scope: DependencyScope::Production, + via: Some("package example.com/org/libs/b".into()), + }], + ..Default::default() + } + ), + ]) + ); + } } }