Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions src/commands/manifest/scripts/assemble.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,74 @@ describe('records → assemble → sidecar', () => {
'Resolved dependencies of a are not reachable from its direct dependencies: g:dep:jar:1, g:loose:jar:1',
)
})
it.for([
{
name: 'an edge through a node its root does not hold',
records: [
'project\ta\tg\ta\t1\ta',
'root\tr1\ta\tcompile\t1',
'node\tr1\tg:lib:jar:1\tg\tlib\t1\tjar\t\t1',
'node\tr1\tg:dep:jar:1\tg\tdep\t1\tjar\t\t0',
'edge\tr1\tg:lib:jar:1\tg:ghost:jar:1',
'edge\tr1\tg:ghost:jar:1\tg:dep:jar:1',
],
unreached:
'a are not reachable from its direct dependencies: g:dep:jar:1',
},
{
name: "an edge in another project's root",
records: [
'project\ta\tg\ta\t1\ta',
'project\tb\tg\tb\t1\tb',
'root\tr1\ta\tcompile\t1',
'node\tr1\tg:lib:jar:1\tg\tlib\t1\tjar\t\t1',
'node\tr1\tg:dep:jar:1\tg\tdep\t1\tjar\t\t0',
'root\tr2\tb\tcompile\t1',
'node\tr2\tg:lib:jar:1\tg\tlib\t1\tjar\t\t1',
'node\tr2\tg:dep:jar:1\tg\tdep\t1\tjar\t\t0',
'edge\tr2\tg:lib:jar:1\tg:dep:jar:1',
],
unreached:
'a are not reachable from its direct dependencies: g:dep:jar:1',
},
{
// Maven: dependencyManagement makes `managed` compile under a test parent,
// so the compile root holds it without a parent.
name: "a node whose parent edge only the project's other root holds",
records: [
'project\ta\tg\ta\t1\ta',
'root\tr1\ta\tcompile\t1',
'node\tr1\tg:managed:jar:1\tg\tmanaged\t1\tjar\t\t0',
'root\tr2\ta\ttest\t0',
'node\tr2\tg:harness:jar:1\tg\tharness\t1\tjar\t\t1',
'node\tr2\tg:managed:jar:1\tg\tmanaged\t1\tjar\t\t0',
'edge\tr2\tg:harness:jar:1\tg:managed:jar:1',
],
unreached: undefined,
},
{
name: "a build project's artifacts, direct in one root only",
records: [
'project\ta\tg\ta\t1\ta',
'project\tg:lib:1\tg\tlib\t1\tlib',
'root\tr1\ta\tcompile\t1',
'node\tr1\tg:lib:jar:1\tg\tlib\t1\tjar\t\t1\tg:lib:1',
'root\tr2\ta\ttest\t0',
'node\tr2\tg:lib:test-jar:tests:1\tg\tlib\t1\ttest-jar\ttests\t0\tg:lib:1',
],
unreached: undefined,
},
])('checks reachability for $name', ({ records, unreached }) => {
const assemble = () =>
assembleFacts(
parseRecords(['meta\tmaven\t3.9.9\t17', ...records].join('\n')),
)
if (unreached) {
expect(assemble).toThrow(`Resolved dependencies of ${unreached}`)
} else {
expect(assemble).not.toThrow()
}
})
it("gives a dependency on a build project that project's id, and marks only it firstParty", () => {
const records = [
'meta\tgradle\t8.0\t17',
Expand Down
11 changes: 6 additions & 5 deletions src/commands/manifest/scripts/test/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@ workspace-enumeration sibling (`socket-workspaces.init.gradle`,
against a matrix of build-tool versions, asserting they still emit the
expected line-protocol records.

Every ecosystem's records are also checked against the shared
`assert-reachability.py` invariant: within each resolution root, every component
must be reachable from one of that root's direct dependencies, or the manifest
consumer reports it as an orphaned component. It's unit-tested over
synthetic records in `test/manifest-reachability-assertions.test.mts`.
The main Gradle, Maven and sbt smoke tests also assemble their records with the
facts assembler (`assemble-records.mts`). It fails on a root outside every
project, and on a resolved dependency no direct dependency of its project
reaches, the manifest consumer's orphaned component. This runs the assembler's
TypeScript directly, so the suites need Node 22+ and the repo's installed
dependencies (`pnpm install`).

## Run locally, on demand

Expand Down
22 changes: 22 additions & 0 deletions src/commands/manifest/scripts/test/assemble-records.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Assemble a records file the way `socket manifest` does. The assembler throws on a resolved
// dependency no direct dependency of its project reaches, the manifest consumer's "orphaned
// component", but checks only the roots of known projects, so a root of any other project fails here.
import { readFileSync } from 'node:fs'

import { assembleFacts } from '../assemble.mts'
import { parseRecords } from '../records.mts'

const recordsFile = process.argv[2]
if (!recordsFile) {
throw new Error('usage: assemble-records.mts <records.tsv>')
}
const parsed = parseRecords(readFileSync(recordsFile, 'utf8'))
for (const [rootId, root] of parsed.roots) {
if (!parsed.projects.has(root.projectKey)) {
throw new Error(
`root ${rootId} belongs to ${root.projectKey}, which has no project record`,
)
}
}
assembleFacts(parsed, { fileExists: () => true })
console.log(`PASS (assembled): ${recordsFile}`)
47 changes: 0 additions & 47 deletions src/commands/manifest/scripts/test/assert-reachability.py

This file was deleted.

6 changes: 6 additions & 0 deletions src/commands/manifest/scripts/test/compat-cache.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,9 @@
# `mktemp -d` for a cold run.
SOCKET_COMPAT_CACHE="${SOCKET_COMPAT_CACHE:-${TMPDIR:-/tmp}/socket-manifest-compat}"
mkdir -p "$SOCKET_COMPAT_CACHE"

# Assemble a records file the way `socket manifest` does, failing on an orphaned component. Runs the
# assembler's TypeScript directly, so it needs Node 22+ and the repo's installed dependencies.
assemble_records() {
node --experimental-strip-types --no-warnings "$(dirname "${BASH_SOURCE[0]}")/assemble-records.mts" "$1"
}
Original file line number Diff line number Diff line change
Expand Up @@ -123,5 +123,5 @@ if errors:
sys.exit(1)
print(f"PASS (failures, withFiles={sys.argv[2]}): every failure reported, no partial or fabricated component")
PY
python3 "$HERE/../assert-reachability.py" "$RECORDS"
assemble_records "$RECORDS"
done
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
# - foo (prod) appears in a prod root, bar (test) only in non-prod roots -> the assembler's dev flag;
# - every resolved dependency gets an on-disk jar `file` record under -Psocket.withFiles (incl. the
# production dep reached via the legacy `compile` config on old Gradle);
# - every component is reachable from a direct dependency of its own root (no orphaned components);
# - its records assemble into facts with no orphaned components;
# - -Psocket.populateFilesFor scopes materialization to a single GAV;
# - a jar that cannot be fetched is reported as a failure, and the other jars still materialize.
#
Expand Down Expand Up @@ -72,7 +72,7 @@ print(f"PASS ({'scoped' if scoped else 'full'}): nodes {sorted(nodes)}; foo prod
+ ("; bar skipped" if scoped else "; bar jar"))
PY

python3 "$HERE/../assert-reachability.py" "$RECORDS"
assemble_records "$RECORDS"

# Second run: scope --with-files to a single GAV and assert ONLY that artifact is materialized.
SCOPE="$HERE/.populate-for.txt"
Expand All @@ -98,7 +98,7 @@ if errors:
print("PASS (populateFilesFor scoping): foo materialized, bar skipped")
PY

python3 "$HERE/../assert-reachability.py" "$RECORDS"
assemble_records "$RECORDS"

# Third run: bar's jar is gone from the repository, so it must be reported rather than emitted as
# resolved with no file.
Expand Down
14 changes: 11 additions & 3 deletions src/commands/manifest/scripts/test/maven-compat/assert-records.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@

errors = []

# NOTE: the generic "every component is reachable from a direct dependency of its root" invariant
# lives in ../assert-reachability.py, which smoke-test.sh runs alongside this file. What remains here
# is Maven-specific: which coordinate lands in which root, and jar materialization.
# NOTE: smoke-test.sh also assembles the records (../assemble-records.mts), which fails on an
# 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.
Expand Down Expand Up @@ -98,6 +98,14 @@ def edge_pair(parent, child):
if pair and not any(root_projects.get(rid) == 'demo:mid:1.0' for rid in edge_roots(*pair)):
errors.append("edge demo.ext:med-io -> demo.ext:med-util missing from mid")

# A managed scope applies to a transitive dependency as it is: demo.ext:managed is compile under
# its test parent, so it is on the runtime classpath.
pair = edge_pair('demo.ext:mgd-test:jar:', 'demo.ext:managed:jar:')
if pair:
if in_prod(pair[0]): errors.append("test dep demo.ext:mgd-test wrongly in a prod root")
elif not in_prod(pair[1]): errors.append("managed-scope demo.ext:managed not in a prod root")
elif not edge_roots(*pair): errors.append("edge demo.ext:mgd-test -> demo.ext:managed missing")

# An edge across the prod/dev split: container-lib (compile) onto container-api, which app
# declares provided. The edge is kept and container-api stays dev.
pair = edge_pair('demo.ext:container-lib:jar:', 'demo.ext:container-api:jar:')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,18 @@
<version>1.0</version>
</parent>
<artifactId>app</artifactId>
<!-- A managed scope applies to a transitive dependency as it is, so demo.ext:managed is compile
under its test parent, and in the compile root without one. -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>demo.ext</groupId>
<artifactId>managed</artifactId>
<version>1.0</version>
<scope>compile</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- internal reactor modules -> inter-module edges, bare-id components. lib's jar comes in
through mid, which excludes demo.ext:libdep, so only lib's test-jar further down brings
Expand Down Expand Up @@ -74,5 +86,11 @@
<type>test-jar</type>
<scope>test</scope>
</dependency>
<dependency>
<groupId>demo.ext</groupId>
<artifactId>mgd-test</artifactId>
<version>1.0</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
15 changes: 9 additions & 6 deletions src/commands/manifest/scripts/test/maven-compat/smoke-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,17 @@
# 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);
# - marks a node prod by the scope Maven kept it with, and keeps it reachable in the prod root;
# - 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;
# - every component is reachable from a direct dependency of its own root (no orphaned components);
# - 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);
# - materializes resolved external jars under -Dsocket.withFiles;
# - scopes that materialization to -Dsocket.populateFilesFor (a newline-delimited GAV file).
#
# The three external artifacts are stubs generated into a file-based repo at test time (see
# The external artifacts are stubs generated into a file-based repo at test time (see
# ../make-stub-repo.sh, shared with the Gradle fixture): the fixture only needs their graph shape, it
# never compiles against or runs them, so nothing here can age into a CVE alert or a version bump.
#
Expand Down Expand Up @@ -48,7 +49,9 @@ bash "$HERE/../make-stub-repo.sh" "$STUBS" \
'demo.ext:med-util:2.0' \
'demo.ext:med-io:1.0+demo.ext:med-util:2.0' \
'demo.ext:container-api:1.0' \
'demo.ext:container-lib:1.0+demo.ext:container-api:1.0'
'demo.ext:container-lib:1.0+demo.ext:container-api:1.0' \
'demo.ext:mgd-test:1.0+demo.ext:managed:1.0' \
'demo.ext:managed:1.0'
# Maven's own plugin closure stays cached between runs; the stubs never do, so every run has to
# resolve them from the repo just generated.
rm -rf "$M2/demo" "$RECORDS" "$PROJECT"/*/target "$PROJECT"/target
Expand All @@ -72,7 +75,7 @@ if not any(r[0] == 'node' and r[1] in app and r[2].startswith('demo.ext:libdep:'
print("FAIL: demo.ext:libdep missing from app"); sys.exit(1)
print("PASS: demo.ext:libdep in app through lib's test-jar")
PY
python3 "$HERE/../assert-reachability.py" "$RECORDS"
assemble_records "$RECORDS"

# Second run: scope --with-files to a single GAV and assert ONLY that artifact is materialized.
# The GAV comes from the records the first run just emitted, so it always matches the fixture pom.
Expand Down Expand Up @@ -121,4 +124,4 @@ if errors:
print("PASS (populateFilesFor scoping): demo.ext:tool materialized, demo.ext:harness skipped")
PY

python3 "$HERE/../assert-reachability.py" "$RECORDS"
assemble_records "$RECORDS"
Original file line number Diff line number Diff line change
Expand Up @@ -76,4 +76,4 @@ if errors:
print(f"PASS (relocation): demo.ext:moved direct in core, under core in app (overridden), never under {wrapper}")
PY

python3 "$HERE/../assert-reachability.py" "$RECORDS"
assemble_records "$RECORDS"
4 changes: 2 additions & 2 deletions src/commands/manifest/scripts/test/sbt-compat/smoke-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
# ConfigRef, updateFull-vs-update) keep producing correct facts:
# - the two expected dependency nodes are present (demo.ext:tool prod, demo.ext:harness test);
# - tool appears in a prod root, harness only in non-prod roots -> the assembler's dev flag;
# - every component is reachable from a direct dependency of its own root (no orphaned components);
# - its records assemble into facts with no orphaned components;
# - both get an on-disk jar `file` record under -Dsocket.withFiles;
# - a Maven relocation's target, declared directly (demo.old:moved) or through a dependency
# (demo.old:legacy, from tool), stays in the graph although coursier leaves its caller out of the report;
Expand Down Expand Up @@ -93,4 +93,4 @@ if errors:
print(f"PASS: demo.ext:tool prod+jar; demo.ext:harness dev+jar ({len(nodes)} nodes)")
PY

python3 "$HERE/../assert-reachability.py" "$RECORDS"
assemble_records "$RECORDS"
Loading
Loading