From 56034b84fc67f78b3b78cfca83470be3890f29bb Mon Sep 17 00:00:00 2001 From: Jeppe Fredsgaard Blaabjerg Date: Thu, 12 Nov 2026 13:20:23 +0100 Subject: [PATCH 1/2] refactor(manifest): give a Maven sibling's jar and test-jar their own ids The Maven facts extension wrote both a reactor sibling's jar and its test-jar under one bare groupId:artifactId:version id, so one record node mixed the two artifacts' scopes, directness and children, and the walk needed special cases for it. Each artifact now has its own coordinate and names the sibling module as its project; the assembler already merges a project's artifacts into its component, as it does for Gradle variants, so the facts are unchanged. Resolves REA-1165. --- .../socket/SocketFactsRecordsEngine.java | 42 +++----- .../java/tech/coana/socket/SocketSupport.java | 5 - .../test/maven-compat/assert-records.py | 37 +++---- .../test/maven-compat/project/app/pom.xml | 4 +- .../smoke-test-repo-inheritance.sh | 18 ++-- .../scripts/test/maven-compat/smoke-test.sh | 19 ++-- test/maven-compat-smoke-assertions.test.mts | 100 ++++++++++++++---- 7 files changed, 135 insertions(+), 90 deletions(-) diff --git a/src/commands/manifest/scripts/maven-extension/src/main/java/tech/coana/socket/SocketFactsRecordsEngine.java b/src/commands/manifest/scripts/maven-extension/src/main/java/tech/coana/socket/SocketFactsRecordsEngine.java index 14e098e1e9..bdf677ef70 100644 --- a/src/commands/manifest/scripts/maven-extension/src/main/java/tech/coana/socket/SocketFactsRecordsEngine.java +++ b/src/commands/manifest/scripts/maven-extension/src/main/java/tech/coana/socket/SocketFactsRecordsEngine.java @@ -53,7 +53,7 @@ * (same contract as the Gradle/SBT scripts; no JSON/hashing here). Per module: a test root with the * whole resolution and a compile root with the nodes kept with a prod scope (compile/runtime/system), * each with the resolved edges between its own nodes. A reactor module becomes a component only - * where another depends on it, by its bare {@code groupId:artifactId:version} id. + * where another depends on one of its artifacts, each of which names the module as its project. * *

Resolution goes through {@link ProjectDependenciesResolver} — the same component Maven's own * lifecycle uses to build a project's classpath — so the graph, the scope and management semantics, @@ -309,9 +309,8 @@ private void collectModule( reportUnscannable(unscannable, moduleCoord, "dependencies resolved without the socket-facts graph transformer"); } if (graph == null) return; - Set visited = new HashSet<>(); for (DependencyNode child : edges.get(graph)) { - String id = visit(child, edges, passingScopes, reactorGavs, unresolved, opts, nodes, visited); + String id = visit(child, edges, passingScopes, reactorGavs, unresolved, opts, nodes); if (id != null) directIds.add(id); } for (String id : unfetched) { @@ -410,8 +409,7 @@ private String visit( Set reactorGavs, Set unresolved, Options opts, - Map nodes, - Set visited) { + Map nodes) { Dependency dep = dn.getDependency(); Artifact artifact = dn.getArtifact(); if (dep == null || artifact == null) return null; @@ -423,20 +421,17 @@ private String visit( boolean internal = reactorGavs.contains(gav); String type = artifact.getProperty(ARTIFACT_PROPERTY_TYPE, artifact.getExtension()); String classifier = artifact.getClassifier(); - String version = artifact.getBaseVersion(); - String key = externalId(artifact); - String id = internal ? SocketSupport.bareId(artifact.getGroupId(), artifact.getArtifactId(), version) : key; + String id = externalId(artifact); - // One walk per artifact: a shared subtree is already recorded. Keyed by coordinate, as a sibling's jar - // and test-jar share a node but can collect different children, e.g. when one declaration has exclusions. - if (!visited.add(key)) return id; + // One walk per artifact: a shared subtree is already recorded. + if (nodes.containsKey(id)) return id; - Node node = internal - ? upsert(nodes, id, artifact.getGroupId(), artifact.getArtifactId(), "", "", version, gav) - : upsert(nodes, id, artifact.getGroupId(), artifact.getArtifactId(), - type == null ? "" : type, classifier == null ? "" : classifier, version, ""); - // A sibling's jar and test-jar share a node, each kept with its own scope. - if (isProd(scope)) node.prod = true; + // A sibling module's artifacts, such as its jar and test-jar, name the module as their project; the + // assembler merges them into its component. + Node node = new Node(id, artifact.getGroupId(), artifact.getArtifactId(), + type == null ? "" : type, classifier == null ? "" : classifier, artifact.getBaseVersion(), internal ? gav : ""); + node.prod = isProd(scope); + nodes.put(id, node); // Maven wrote each accepted node's resolved file back onto the node; a reactor module reports its // own dirs through its `project` record instead of a `file` record. @@ -446,7 +441,7 @@ private String visit( } for (DependencyNode child : edges.get(dn)) { - String childId = visit(child, edges, passingScopes, reactorGavs, unresolved, opts, nodes, visited); + String childId = visit(child, edges, passingScopes, reactorGavs, unresolved, opts, nodes); if (childId != null) node.children.add(childId); } return id; @@ -476,17 +471,6 @@ private static boolean isProd(String scope) { return scope.equals("compile") || scope.equals("runtime") || scope.equals("system"); } - private static Node upsert( - Map nodes, String id, String groupId, String artifactId, String type, String classifier, String version, - String project) { - Node node = nodes.get(id); - if (node == null) { - node = new Node(id, groupId, artifactId, type, classifier, version, project); - nodes.put(id, node); - } - return node; - } - // ---- emission ---- // Shaped like Gradle's runtime and test classpaths. An edge from a prod node onto a dev one, such as diff --git a/src/commands/manifest/scripts/maven-extension/src/main/java/tech/coana/socket/SocketSupport.java b/src/commands/manifest/scripts/maven-extension/src/main/java/tech/coana/socket/SocketSupport.java index e14b981afa..12d68a11cc 100644 --- a/src/commands/manifest/scripts/maven-extension/src/main/java/tech/coana/socket/SocketSupport.java +++ b/src/commands/manifest/scripts/maven-extension/src/main/java/tech/coana/socket/SocketSupport.java @@ -38,11 +38,6 @@ public static String coordId(String groupId, String artifactId, String type, Str return sb.toString(); } - /** Bare {@code groupId:artifactId:version} id used for reactor (internal) module components. */ - public static String bareId(String groupId, String artifactId, String version) { - return coordId(groupId, artifactId, null, null, version); - } - /** * Translate a config-name glob to a case-sensitive regex. Supports {@code *}, {@code ?}, and * {@code [...]} character classes: enumerations ({@code [cC]}), ranges ({@code [a-z]}), and diff --git a/src/commands/manifest/scripts/test/maven-compat/assert-records.py b/src/commands/manifest/scripts/test/maven-compat/assert-records.py index 66e8e374f2..d04a4f38af 100755 --- a/src/commands/manifest/scripts/test/maven-compat/assert-records.py +++ b/src/commands/manifest/scripts/test/maven-compat/assert-records.py @@ -6,7 +6,7 @@ import sys rows = [l.rstrip('\n').split('\t') for l in open(sys.argv[1]) if l.strip()] tool = None -roots, root_projects, nodes, files, direct, edges = {}, {}, {}, {}, {}, {} +roots, root_projects, nodes, projects, files, direct, edges = {}, {}, {}, {}, {}, {}, {} for r in rows: if r[0] == 'meta': tool = r[1] elif r[0] == 'root': @@ -16,6 +16,8 @@ elif r[0] == 'node': nodes.setdefault(r[2], set()).add(r[1]) # coordId -> {rootId} if r[8] == '1': direct.setdefault(r[2], set()).add(r[1]) # coordId -> {rootId where direct} + # coordId to the build projects its rows resolve it to. + projects.setdefault(r[2], set()).add(r[9] if len(r) > 9 else '') elif r[0] == 'file': files.setdefault(r[2], set()).add(r[3]) # coordId -> {path} elif r[0] == 'edge': # (parent, child) to the roots that hold the edge. @@ -27,17 +29,9 @@ # orphaned component. What remains here is Maven-specific: which coordinate lands in which root, # and jar materialization. -def coord(prefix, fields=None): - """The one emitted coordId starting with prefix, or None. Keeps assertions version-agnostic. - - fields pins the count of colon-separated fields. Without it a prefix that stops before the - type, as the bare-id lookup below does, would also match the typed form it is meant to rule - out: 'demo:lib:' is a prefix of both 'demo:lib:1.0' and 'demo:lib:jar:1.0'. - """ - hits = sorted( - c for c in set(nodes) | set(files) - if c.startswith(prefix) and (fields is None or c.count(':') + 1 == fields) - ) +def coord(prefix): + """The one emitted coordId starting with prefix, or None. Keeps assertions version-agnostic.""" + hits = sorted(c for c in set(nodes) | set(files) if c.startswith(prefix)) if len(hits) > 1: errors.append(f"expected one coordinate for {prefix!r}, got {hits}") return hits[0] if hits else None @@ -45,8 +39,8 @@ def coord(prefix, fields=None): ext = coord('demo.ext:tool:jar:') harness = coord('demo.ext:harness:jar:') harness_core = coord('demo.ext:harness-core:jar:') -# Internal module: bare groupId:artifactId:version, no type field. -lib = coord('demo:lib:', fields=3) +# Sibling module demo:lib: app reaches its jar through mid and declares its test-jar. +lib_jar, lib_tests = coord('demo:lib:jar:'), coord('demo:lib:test-jar:') if tool != 'maven': errors.append(f"meta tool {tool!r} != 'maven'") @@ -63,9 +57,16 @@ def has_jar(cid): return any(p.endswith('.jar') for p in files.get(cid, ())) if not harness_core: errors.append("missing transitive test dep demo.ext:harness-core") elif in_prod(harness_core): errors.append("transitive test dep demo.ext:harness-core wrongly in a prod root") -if not lib: errors.append("internal module demo:lib not emitted by its bare id") -elif not in_prod(lib): errors.append("internal module demo:lib not in app's prod root") -elif not direct.get(lib): errors.append("internal module demo:lib not marked direct") +def direct_in(cid, project): return any(root_projects.get(rid) == project for rid in direct.get(cid, ())) + +if 'demo:lib:1.0' in nodes: errors.append("sibling demo:lib emitted under its bare id") +elif not lib_jar or not lib_tests: errors.append("sibling demo:lib's jar or test-jar missing") +elif projects[lib_jar] | projects[lib_tests] != {'demo:lib:1.0'}: + errors.append("sibling demo:lib's artifacts do not name demo:lib:1.0 as their project") +elif not in_prod(lib_jar): errors.append("sibling demo:lib's jar not in a prod root") +elif in_prod(lib_tests): errors.append("sibling demo:lib's test-jar wrongly in a prod root") +elif direct_in(lib_jar, 'demo:app:1.0'): errors.append("sibling demo:lib's jar wrongly direct in app") +elif not direct_in(lib_tests, 'demo:app:1.0'): errors.append("sibling demo:lib's test-jar not direct in app") def edge_roots(parent, child): return edges.get((parent, child), set()) @@ -119,4 +120,4 @@ def edge_pair(parent, child): print("FAIL:") for e in errors: print(" -", e) sys.exit(1) -print(f"PASS: tool=maven; {ext} prod+jar; harness/harness-core dev; internal demo:lib (bare id, direct); demo.ext:shared prod; edges onto kept dependencies") +print(f"PASS: tool=maven; {ext} prod+jar; harness/harness-core dev; sibling demo:lib jar prod, test-jar direct; demo.ext:shared prod; edges onto kept dependencies") diff --git a/src/commands/manifest/scripts/test/maven-compat/project/app/pom.xml b/src/commands/manifest/scripts/test/maven-compat/project/app/pom.xml index 557f557163..5c54ecb76a 100644 --- a/src/commands/manifest/scripts/test/maven-compat/project/app/pom.xml +++ b/src/commands/manifest/scripts/test/maven-compat/project/app/pom.xml @@ -20,9 +20,9 @@ - + libdep. --> demo mid diff --git a/src/commands/manifest/scripts/test/maven-compat/smoke-test-repo-inheritance.sh b/src/commands/manifest/scripts/test/maven-compat/smoke-test-repo-inheritance.sh index d3b9776a48..2c5eb339a3 100755 --- a/src/commands/manifest/scripts/test/maven-compat/smoke-test-repo-inheritance.sh +++ b/src/commands/manifest/scripts/test/maven-compat/smoke-test-repo-inheritance.sh @@ -42,16 +42,21 @@ echo "+ $("$MVN" -v 2>/dev/null | head -1) (repo inheritance)" python3 - "$RECORDS" <<'PY' import sys rows = [l.rstrip('\n').split('\t') for l in open(sys.argv[1]) if l.strip()] -roots, nodes, files, edges, failures = {}, {}, {}, set(), [] +roots, nodes, projects, files, edges, failures = {}, {}, {}, {}, set(), [] for r in rows: if r[0] == 'root': roots[r[1]] = r[2] # rootId -> projectKey - elif r[0] == 'node': nodes.setdefault(r[2], set()).add(r[1]) # coordId -> {rootId} + elif r[0] == 'node': + nodes.setdefault(r[2], set()).add(r[1]) # coordId -> {rootId} + # coordId to the build project it resolves to, if any. + if len(r) > 9 and r[9]: projects[r[2]] = r[9] elif r[0] == 'file': files.setdefault(r[2], set()).add(r[3]) # coordId -> {path} elif r[0] == 'edge': edges.add((r[1], r[2], r[3])) # (rootId, parent, child) elif r[0] == 'failure': failures.append(r[1:]) WIDGET = 'demo.scoped:widget:jar:1.0' LIBA = 'demo:liba:1.0' +# liba's jar, which names liba as its project. +LIBA_JAR = 'demo:liba:jar:1.0' LIBB = 'demo:libb:1.0' errors = [] @@ -70,16 +75,17 @@ for project_key in (LIBA, LIBB): errors.append(f"{WIDGET} missing from {project_key}'s graph") # The point of the fixture: libb reaches widget only through the reactor module liba. -if not any(rid in roots_for(LIBB) and p == LIBA and c == WIDGET for rid, p, c in edges): - errors.append(f"no {LIBA} -> {WIDGET} edge in libb's root") +if not any(rid in roots_for(LIBB) and p == LIBA_JAR and c == WIDGET for rid, p, c in edges): + errors.append(f"no {LIBA_JAR} -> {WIDGET} edge in libb's root") jars = [p for p in files.get(WIDGET, ()) if p.endswith('.jar')] if not jars: errors.append(f"{WIDGET} jar not materialized: {files.get(WIDGET)}") # A reactor sibling reports its dirs through its `project` record, never a `file` record. -if files.get(LIBA): - errors.append(f"reactor module {LIBA} should have no file records, got {files[LIBA]}") +liba_files = {p for c, ps in files.items() if projects.get(c) == LIBA for p in ps} +if liba_files: + errors.append(f"reactor module {LIBA} should have no file records, got {sorted(liba_files)}") if errors: print("FAIL:") diff --git a/src/commands/manifest/scripts/test/maven-compat/smoke-test.sh b/src/commands/manifest/scripts/test/maven-compat/smoke-test.sh index 7628e07d31..f2560e094b 100755 --- a/src/commands/manifest/scripts/test/maven-compat/smoke-test.sh +++ b/src/commands/manifest/scripts/test/maven-compat/smoke-test.sh @@ -4,15 +4,15 @@ # tested separately in `nx test utils`). Guards, across the supported Maven range, that the extension: # - emits the external prod dep demo.ext:tool (in a prod root) and the test dep demo.ext:harness + # its transitive demo.ext:harness-core (only in a non-prod root -> the assembler's dev flag); -# - emits the internal reactor module demo:lib by its bare groupId:artifactId:version id (so the -# inter-module edge lines up with its `project` record); +# - emits a sibling module's jar and test-jar under their own coordinates, each naming the module +# as its project (the assembler merges them into the module's component); # - marks a node prod by the scope Maven kept it with, also one whose scope dependencyManagement # sets under a test parent; # - records every resolved edge, pointing at the node Maven kept: onto a direct dependency, onto a # mediated version, and from a prod node onto a provided one; # - its records assemble into facts with no orphaned components; -# - keeps a dependency only a sibling module's test-jar brings (mid excludes demo.ext:libdep from -# lib, and app reaches lib's jar through mid before lib's test-jar); +# - keeps a dependency only a sibling module's test-jar brings under that test-jar, not its jar +# (mid excludes demo.ext:libdep from lib); # - materializes resolved external jars under -Dsocket.withFiles; # - scopes that materialization to -Dsocket.populateFilesFor (a newline-delimited GAV file). # @@ -71,9 +71,14 @@ python3 - "$RECORDS" <<'PY' import sys rows = [l.rstrip('\n').split('\t') for l in open(sys.argv[1]) if l.strip()] app = {r[1] for r in rows if r[0] == 'root' and r[2] == 'demo:app:1.0'} -if not any(r[0] == 'node' and r[1] in app and r[2].startswith('demo.ext:libdep:') for r in rows): - print("FAIL: demo.ext:libdep missing from app"); sys.exit(1) -print("PASS: demo.ext:libdep in app through lib's test-jar") +def libdep_edge(parent): + return any(r[0] == 'edge' and r[1] in app and r[2].startswith(parent) + and r[3].startswith('demo.ext:libdep:') for r in rows) +if not libdep_edge('demo:lib:test-jar:'): + print("FAIL: no demo:lib test-jar -> demo.ext:libdep edge in app"); sys.exit(1) +if libdep_edge('demo:lib:jar:'): + print("FAIL: demo:lib jar -> demo.ext:libdep edge in app, though mid excludes it"); sys.exit(1) +print("PASS: demo.ext:libdep in app through lib's test-jar only") PY assemble_records "$RECORDS" diff --git a/test/maven-compat-smoke-assertions.test.mts b/test/maven-compat-smoke-assertions.test.mts index 6a956b0259..3ace5288b4 100644 --- a/test/maven-compat-smoke-assertions.test.mts +++ b/test/maven-compat-smoke-assertions.test.mts @@ -1,13 +1,10 @@ /** * Unit tests for the maven-compat fixture's record assertions. * - * The fixture proves the Maven extension emits internal reactor modules under - * their bare groupId:artifactId:version id, distinct from the typed - * groupId:artifactId:type:version form used for external artifacts. The lookup - * that checks this has to reject the typed form, or a regression that stopped - * emitting the bare id would still pass. The fixture also checks that the - * extension records every resolved edge and marks a node prod by the scope - * Maven kept it with. + * The fixture proves the Maven extension emits a sibling module's jar and + * test-jar under their own coordinates, each naming the module as its project, + * that it records every resolved edge, and that it marks a node prod by the + * scope Maven kept it with. * The fixture itself needs Maven and a JDK, so these run its assertions over * synthetic records instead. * @@ -38,11 +35,14 @@ function rootRow( return ['root', rootId, projectKey, '', options.prod ? '1' : '0'].join('\t') } -/** A `node` row: root id in field 2, coordinate in field 3, directness in field 9. */ +/** + * A `node` row: root id in field 2, coordinate in field 3, directness in field + * 9, the build project it resolves to in field 10. + */ function nodeRow( rootId: string, coordId: string, - options: { direct: boolean }, + options: { direct: boolean; project: string }, ): string { return [ 'node', @@ -54,6 +54,7 @@ function nodeRow( '', '', options.direct ? '1' : '0', + options.project, ].join('\t') } @@ -79,6 +80,9 @@ const MED_IO = 'demo.ext:med-io:jar:1.0' const MED_UTIL = 'demo.ext:med-util:jar:1.0' const CONTAINER_LIB = 'demo.ext:container-lib:jar:1.0' const CONTAINER_API = 'demo.ext:container-api:jar:1.0' +const MID_JAR = 'demo:mid:jar:1.0' +const LIB_JAR = 'demo:lib:jar:1.0' +const LIB_TESTS = 'demo:lib:test-jar:tests:1.0' const MGD_TEST = 'demo.ext:mgd-test:jar:1.0' const MANAGED = 'demo.ext:managed:jar:1.0' @@ -87,6 +91,7 @@ const DEV_NODES = new Set([ CONTAINER_API, HARNESS, HARNESS_CORE, + LIB_TESTS, MGD_TEST, ]) @@ -104,6 +109,7 @@ const EDGES = { harnessCore: { module: 'app', parent: HARNESS, child: HARNESS_CORE }, medIoUtil: { module: 'mid', parent: MED_IO, child: MED_UTIL }, mgdTestManaged: { module: 'app', parent: MGD_TEST, child: MANAGED }, + midLib: { module: 'app', parent: MID_JAR, child: LIB_JAR }, } satisfies Record function rootIdOf(module: string, prodRoot: boolean): string { @@ -112,24 +118,29 @@ function rootIdOf(module: string, prodRoot: boolean): string { type RecordsOptions = { dev?: string[] - internalModuleCoord?: string + extraNodes?: string[] + libProject?: string omitEdges?: Edge[] omitNodes?: string[] prod?: string[] + toggleDirect?: string[] } /** * Records in the extension's root layout: per module, a compile root with the * nodes Maven kept with a prod scope and a test root with the whole - * resolution. `dev`, `prod`, `omitEdges` and `omitNodes` alter it into outputs the - * assertions must reject. + * resolution. `dev`, `prod`, `extraNodes`, `omitEdges`, `omitNodes`, + * `libProject` and `toggleDirect` alter it into outputs the assertions must + * reject. */ function buildRecords({ dev = [], - internalModuleCoord = 'demo:lib:1.0', + extraNodes = [], + libProject = 'demo:lib:1.0', omitEdges = [], omitNodes = [], prod = [], + toggleDirect = [], }: RecordsOptions = {}): string { // Coordinate and directness of each module's nodes. const modules: Array<[string, Array<[string, boolean]>]> = [ @@ -148,7 +159,10 @@ function buildRecords({ [CONTAINER_LIB, true], [MGD_TEST, true], [MANAGED, false], - [internalModuleCoord, true], + [MID_JAR, true], + [LIB_JAR, false], + [LIB_TESTS, true], + ...extraNodes.map(coord => [coord, true] as [string, boolean]), ], ], [ @@ -156,6 +170,7 @@ function buildRecords({ [ [MED_UTIL, true], [MED_IO, true], + [LIB_JAR, true], ], ], ] @@ -170,7 +185,17 @@ function buildRecords({ rows.push(rootRow(rootId, `demo:${module}:1.0`, { prod: prodRoot })) for (const { 0: coord, 1: direct } of nodes) { if (inRoot(coord)) { - rows.push(nodeRow(rootId, coord, { direct })) + const project = coord.startsWith('demo:lib:') + ? libProject + : coord.startsWith('demo:mid:') + ? 'demo:mid:1.0' + : '' + rows.push( + nodeRow(rootId, coord, { + direct: direct !== toggleDirect.includes(coord), + project, + }), + ) } } for (const edge of Object.values(EDGES)) { @@ -224,16 +249,45 @@ describe('maven-compat assert-records.py', () => { it.for([ { - // 'demo:lib:' is a prefix of 'demo:lib:jar:1.0', so a lookup that matched - // on the prefix alone would report bare-id coverage it does not have. - name: 'an internal module emitted only under a typed id', - options: { internalModuleCoord: 'demo:lib:jar:1.0' }, - message: 'internal module demo:lib not emitted by its bare id', + name: 'a sibling emitted under its bare id', + options: { extraNodes: ['demo:lib:1.0'] }, + message: 'sibling demo:lib emitted under its bare id', + }, + { + name: 'a missing sibling jar', + options: { omitNodes: [LIB_JAR] }, + message: "sibling demo:lib's jar or test-jar missing", + }, + { + name: 'a missing sibling test-jar', + options: { omitNodes: [LIB_TESTS] }, + message: "sibling demo:lib's jar or test-jar missing", + }, + { + name: 'sibling artifacts that do not name their project', + options: { libProject: '' }, + message: + "sibling demo:lib's artifacts do not name demo:lib:1.0 as their project", + }, + { + name: 'a sibling jar left dev', + options: { dev: [LIB_JAR] }, + message: "sibling demo:lib's jar not in a prod root", + }, + { + name: 'a sibling test-jar in a prod root', + options: { prod: [LIB_TESTS] }, + message: "sibling demo:lib's test-jar wrongly in a prod root", + }, + { + name: 'a sibling jar marked direct', + options: { toggleDirect: [LIB_JAR] }, + message: "sibling demo:lib's jar wrongly direct in app", }, { - name: 'an internal module that is missing entirely', - options: { internalModuleCoord: 'demo:unrelated:1.0' }, - message: 'internal module demo:lib not emitted by its bare id', + name: 'a sibling test-jar not marked direct', + options: { toggleDirect: [LIB_TESTS] }, + message: "sibling demo:lib's test-jar not direct in app", }, { name: 'a compile-scoped node kept under a test parent left dev', From f6749d54336a524e90a78cd42c13b01d5561a682 Mon Sep 17 00:00:00 2001 From: Jeppe Fredsgaard Blaabjerg Date: Thu, 12 Nov 2026 15:22:33 +0100 Subject: [PATCH 2/2] test(manifest): pin the Maven sibling artifacts to the module that uses them Check that lib's jar is prod in app itself, not just in mid, and that mid's edge reaches it there; check liba's jar both names liba and has no file record. Read a coordinate's project without assuming a node row. --- .../test/maven-compat/assert-records.py | 13 ++++++----- .../smoke-test-repo-inheritance.sh | 7 ++++-- test/maven-compat-smoke-assertions.test.mts | 22 +++++++++++++------ 3 files changed, 28 insertions(+), 14 deletions(-) diff --git a/src/commands/manifest/scripts/test/maven-compat/assert-records.py b/src/commands/manifest/scripts/test/maven-compat/assert-records.py index d04a4f38af..1275559c00 100755 --- a/src/commands/manifest/scripts/test/maven-compat/assert-records.py +++ b/src/commands/manifest/scripts/test/maven-compat/assert-records.py @@ -57,20 +57,23 @@ def has_jar(cid): return any(p.endswith('.jar') for p in files.get(cid, ())) if not harness_core: errors.append("missing transitive test dep demo.ext:harness-core") elif in_prod(harness_core): errors.append("transitive test dep demo.ext:harness-core wrongly in a prod root") +def edge_roots(parent, child): + return edges.get((parent, child), set()) + def direct_in(cid, project): return any(root_projects.get(rid) == project for rid in direct.get(cid, ())) if 'demo:lib:1.0' in nodes: errors.append("sibling demo:lib emitted under its bare id") elif not lib_jar or not lib_tests: errors.append("sibling demo:lib's jar or test-jar missing") -elif projects[lib_jar] | projects[lib_tests] != {'demo:lib:1.0'}: +elif projects.get(lib_jar, set()) | projects.get(lib_tests, set()) != {'demo:lib:1.0'}: errors.append("sibling demo:lib's artifacts do not name demo:lib:1.0 as their project") -elif not in_prod(lib_jar): errors.append("sibling demo:lib's jar not in a prod root") +elif not any(roots.get(rid) and root_projects.get(rid) == 'demo:app:1.0' for rid in nodes.get(lib_jar, ())): + errors.append("sibling demo:lib's jar not in app's prod root") +elif not any(root_projects.get(rid) == 'demo:app:1.0' for rid in edge_roots(coord('demo:mid:jar:'), lib_jar)): + errors.append("edge demo:mid -> demo:lib's jar missing from app") elif in_prod(lib_tests): errors.append("sibling demo:lib's test-jar wrongly in a prod root") elif direct_in(lib_jar, 'demo:app:1.0'): errors.append("sibling demo:lib's jar wrongly direct in app") elif not direct_in(lib_tests, 'demo:app:1.0'): errors.append("sibling demo:lib's test-jar not direct in app") -def edge_roots(parent, child): - return edges.get((parent, child), set()) - def edge_pair(parent, child): """Both coordinates, or None after reporting the missing ones.""" found = (coord(parent), coord(child)) diff --git a/src/commands/manifest/scripts/test/maven-compat/smoke-test-repo-inheritance.sh b/src/commands/manifest/scripts/test/maven-compat/smoke-test-repo-inheritance.sh index 2c5eb339a3..2ac369689f 100755 --- a/src/commands/manifest/scripts/test/maven-compat/smoke-test-repo-inheritance.sh +++ b/src/commands/manifest/scripts/test/maven-compat/smoke-test-repo-inheritance.sh @@ -46,7 +46,8 @@ roots, nodes, projects, files, edges, failures = {}, {}, {}, {}, set(), [] for r in rows: if r[0] == 'root': roots[r[1]] = r[2] # rootId -> projectKey elif r[0] == 'node': - nodes.setdefault(r[2], set()).add(r[1]) # coordId -> {rootId} + # coordId to the roots that hold it. + nodes.setdefault(r[2], set()).add(r[1]) # coordId to the build project it resolves to, if any. if len(r) > 9 and r[9]: projects[r[2]] = r[9] elif r[0] == 'file': files.setdefault(r[2], set()).add(r[3]) # coordId -> {path} @@ -83,7 +84,9 @@ if not jars: errors.append(f"{WIDGET} jar not materialized: {files.get(WIDGET)}") # A reactor sibling reports its dirs through its `project` record, never a `file` record. -liba_files = {p for c, ps in files.items() if projects.get(c) == LIBA for p in ps} +if projects.get(LIBA_JAR) != LIBA: + errors.append(f"{LIBA_JAR} does not name {LIBA} as its project") +liba_files = {p for c, ps in files.items() if c == LIBA_JAR or projects.get(c) == LIBA for p in ps} if liba_files: errors.append(f"reactor module {LIBA} should have no file records, got {sorted(liba_files)}") diff --git a/test/maven-compat-smoke-assertions.test.mts b/test/maven-compat-smoke-assertions.test.mts index 3ace5288b4..dcbbbccb62 100644 --- a/test/maven-compat-smoke-assertions.test.mts +++ b/test/maven-compat-smoke-assertions.test.mts @@ -81,6 +81,12 @@ const MED_UTIL = 'demo.ext:med-util:jar:1.0' const CONTAINER_LIB = 'demo.ext:container-lib:jar:1.0' const CONTAINER_API = 'demo.ext:container-api:jar:1.0' const MID_JAR = 'demo:mid:jar:1.0' +// The reactor module each sibling artifact's coordinate resolves to, by artifactId. +const SIBLING_PROJECTS: Record = { + __proto__: null, + lib: 'demo:lib:1.0', + mid: 'demo:mid:1.0', +} as unknown as Record const LIB_JAR = 'demo:lib:jar:1.0' const LIB_TESTS = 'demo:lib:test-jar:tests:1.0' const MGD_TEST = 'demo.ext:mgd-test:jar:1.0' @@ -129,9 +135,8 @@ type RecordsOptions = { /** * Records in the extension's root layout: per module, a compile root with the * nodes Maven kept with a prod scope and a test root with the whole - * resolution. `dev`, `prod`, `extraNodes`, `omitEdges`, `omitNodes`, - * `libProject` and `toggleDirect` alter it into outputs the assertions must - * reject. + * resolution. `dev`, `extraNodes`, `libProject`, `omitEdges`, `omitNodes`, + * `prod` and `toggleDirect` alter it into outputs the assertions must reject. */ function buildRecords({ dev = [], @@ -187,9 +192,7 @@ function buildRecords({ if (inRoot(coord)) { const project = coord.startsWith('demo:lib:') ? libProject - : coord.startsWith('demo:mid:') - ? 'demo:mid:1.0' - : '' + : (SIBLING_PROJECTS[coord.split(':')[1] ?? ''] ?? '') rows.push( nodeRow(rootId, coord, { direct: direct !== toggleDirect.includes(coord), @@ -253,6 +256,11 @@ describe('maven-compat assert-records.py', () => { options: { extraNodes: ['demo:lib:1.0'] }, message: 'sibling demo:lib emitted under its bare id', }, + { + name: "a missing edge onto the sibling's jar", + options: { omitEdges: [EDGES.midLib] }, + message: "edge demo:mid -> demo:lib's jar missing from app", + }, { name: 'a missing sibling jar', options: { omitNodes: [LIB_JAR] }, @@ -272,7 +280,7 @@ describe('maven-compat assert-records.py', () => { { name: 'a sibling jar left dev', options: { dev: [LIB_JAR] }, - message: "sibling demo:lib's jar not in a prod root", + message: "sibling demo:lib's jar not in app's prod root", }, { name: 'a sibling test-jar in a prod root',