55# the declared dependencies whose parent POM is missing (lib, testlib), coursier (sbt 1.3+) names the
66# missing parent POM itself; both name ghost. Each failure's detail is its own module's message.
77#
8- # Usage: smoke-test-failure.sh <sbt-version> <scala-version>
8+ # A second run with files: a jar that cannot be fetched (nojar; lib/testlib's under coursier; ok's
9+ # tests classifier, test-scoped) must be reported and marked `unfetched` under its full coordinate,
10+ # in the configurations that requested it, while pomonly stays an artifactless module and pkwar keeps
11+ # its war. Coursier skips a missing jar silently and lists one for every Maven module whatever its
12+ # packaging; Ivy's own resolution report names what it failed to download.
13+ #
14+ # Then ../artifact-failure, whose only failure is ok's missing tests jar, without and with files: an
15+ # artifact failure names no module, yet sbt's update fails on it, so it must be reported in both runs
16+ # and must not make core or its dependent app unscannable.
17+ #
18+ # Usage: smoke-test-failure.sh <sbt-version> <scala-version> [coursier|ivy] (ivy: sbt 1.x only)
919set -euo pipefail
1020HERE=" $( cd " $( dirname " $0 " ) " && pwd) "
1121SBT_VERSION=" ${1:? usage: smoke-test-failure.sh <sbt-version> <scala-version>} "
1222SCALA_VERSION=" ${2:? usage: smoke-test-failure.sh <sbt-version> <scala-version>} "
23+ RESOLVER=" ${3:- coursier} "
1324PLUGIN=" $HERE /../../socket-facts.plugin.scala"
1425PROJECT=" $HERE /failure"
1526RECORDS=" $PROJECT /records.tsv"
@@ -34,6 +45,34 @@ for lib in lib testlib; do
3445</project>
3546POM
3647done
48+ mkdir -p " $REPO /demo/fail/nojar/1.0" " $REPO /demo/fail/pomonly/1.0" " $REPO /demo/fail/pkwar/1.0"
49+ cat > " $REPO /demo/fail/pkwar/1.0/pkwar-1.0.pom" << 'POM '
50+ <project xmlns="http://maven.apache.org/POM/4.0.0">
51+ <modelVersion>4.0.0</modelVersion>
52+ <groupId>demo.fail</groupId>
53+ <artifactId>pkwar</artifactId>
54+ <version>1.0</version>
55+ <packaging>war</packaging>
56+ </project>
57+ POM
58+ cp " $REPO /demo/fail/ok/1.0/ok-1.0.jar" " $REPO /demo/fail/pkwar/1.0/pkwar-1.0.war"
59+ cat > " $REPO /demo/fail/nojar/1.0/nojar-1.0.pom" << 'POM '
60+ <project xmlns="http://maven.apache.org/POM/4.0.0">
61+ <modelVersion>4.0.0</modelVersion>
62+ <groupId>demo.fail</groupId>
63+ <artifactId>nojar</artifactId>
64+ <version>1.0</version>
65+ </project>
66+ POM
67+ cat > " $REPO /demo/fail/pomonly/1.0/pomonly-1.0.pom" << 'POM '
68+ <project xmlns="http://maven.apache.org/POM/4.0.0">
69+ <modelVersion>4.0.0</modelVersion>
70+ <groupId>demo.fail</groupId>
71+ <artifactId>pomonly</artifactId>
72+ <version>1.0</version>
73+ <packaging>pom</packaging>
74+ </project>
75+ POM
3776rm -rf " $IVY /cache/demo.fail"
3877
3978GB=" $( mktemp -d) /global-base"
@@ -43,12 +82,20 @@ cp "$PLUGIN" "$GB/plugins/SocketFactsPlugin.scala"
4382mkdir -p " $PROJECT /project"
4483echo " sbt.version=$SBT_VERSION " > " $PROJECT /project/build.properties"
4584echo " scalaVersion in ThisBuild := \" $SCALA_VERSION \" " > " $PROJECT /scala-version.sbt"
85+ if [ " $RESOLVER " = ivy ]; then
86+ echo ' useCoursier in ThisBuild := false' >> " $PROJECT /scala-version.sbt"
87+ fi
4688rm -rf " $RECORDS " " $PROJECT /target" " $PROJECT /project/target"
4789
90+ run () {
91+ local dir=" $1 "
92+ shift
93+ ( cd " $dir " && sbt -Dsbt.global.base=" $GB " -Dsbt.server.autostart=false \
94+ -Dsbt.ivy.home=" $IVY " " $@ " --batch socketFacts )
95+ }
96+
4897echo " + sbt $SBT_VERSION (scala $SCALA_VERSION ) (failure)"
49- ( cd " $PROJECT " && sbt -Dsbt.global.base=" $GB " -Dsbt.server.autostart=false \
50- -Dsbt.ivy.home=" $IVY " \
51- -Dsocket.recordsFile=" $RECORDS " --batch socketFacts )
98+ run " $PROJECT " -Dsocket.recordsFile=" $RECORDS "
5299
53100python3 - " $RECORDS " << 'PY '
54101import sys
@@ -84,3 +131,112 @@ if errors:
84131 sys.exit(1)
85132print(f"PASS (failure): reported {sorted(failed)}; ok still emitted")
86133PY
134+
135+ rm -rf " $RECORDS " " $PROJECT /target" " $PROJECT /project/target"
136+ echo " + sbt $SBT_VERSION (scala $SCALA_VERSION ) (failure, with files)"
137+ run " $PROJECT " -Dsocket.withFiles=true -Dsocket.recordsFile=" $RECORDS "
138+
139+ python3 - " $RECORDS " << 'PY '
140+ import sys
141+ rows = [l.rstrip('\n').split('\t') for l in open(sys.argv[1]) if l.strip()]
142+ nodes = {r[2]: r for r in rows if r[0] == 'node'}
143+ unfetched = {r[2] for r in rows if r[0] == 'unfetched'}
144+ failed = {r[1] for r in rows if r[0] == 'failure'}
145+ config_of = {r[1]: r[3] for r in rows if r[0] == 'root'}
146+ unfetched_configs = {(r[2], config_of[r[1]]) for r in rows if r[0] == 'unfetched'}
147+ files = {r[2] for r in rows if r[0] == 'file'}
148+ errors = []
149+
150+ if 'demo.fail:pomonly:1.0' not in nodes:
151+ errors.append(f"pomonly is pom-packaged, so it must be emitted without an artifact; nodes={sorted(nodes)}")
152+ if any('pomonly' in u for u in unfetched) or 'demo.fail:pomonly:1.0' in failed:
153+ errors.append(f"pomonly has no jar to fetch; unfetched={sorted(unfetched)} failures={sorted(failed)}")
154+ for name in ('nojar', 'ok'):
155+ if f'demo.fail:{name}:1.0' not in failed:
156+ errors.append(f"{name}'s jar cannot be fetched, so it must be reported; failures={sorted(failed)}")
157+ if 'demo.fail:nojar:jar:1.0' not in unfetched:
158+ errors.append(f"nojar must be emitted as an unfetched jar; unfetched={sorted(unfetched)}")
159+ if 'demo.fail:ok:jar:1.0' not in files:
160+ errors.append("ok's own jar resolved, so its missing tests jar must not cost it its file")
161+ tests_configs = {c for (u, c) in unfetched_configs if u == 'demo.fail:ok:jar:tests:1.0'}
162+ if not tests_configs or any('test' not in c.lower() for c in tests_configs):
163+ errors.append(f"ok's tests jar is requested only in Test, so only test configs may miss it: {sorted(tests_configs)}")
164+ for name in ('nojar', 'lib', 'testlib'):
165+ if f'demo.fail:{name}:1.0' in nodes:
166+ errors.append(f"{name} has a jar-packaged POM, so it must not look artifactless")
167+ if f'demo.fail:{name}:jar:1.0' in nodes and f'demo.fail:{name}:jar:1.0' not in unfetched:
168+ errors.append(f"{name} is emitted without its jar, so it must be marked unfetched; unfetched={sorted(unfetched)}")
169+ if 'demo.fail:pkwar:war:1.0' not in files:
170+ errors.append(f"pkwar's war resolved, so it must keep its file; nodes={sorted(n for n in nodes if 'pkwar' in n)}")
171+ if any('pkwar' in f for f in failed):
172+ errors.append(f"pkwar's packaging is war, so a missing jar is expected; failures={sorted(failed)}")
173+ details = [r[2] for r in rows if r[0] == 'failure' and r[1] == 'demo.fail:nojar:1.0']
174+ if len(set(details)) != 1 or 'not found' not in details[0]:
175+ errors.append(f"nojar's jar is absent, so it must be reported once as not found: {sorted(set(details))}")
176+ stray = unfetched - {f'demo.fail:{n}:jar:1.0' for n in ('nojar', 'lib', 'testlib')} - {'demo.fail:ok:jar:tests:1.0'}
177+ if stray:
178+ errors.append(f"only the jars that are missing may be unfetched: {sorted(stray)}")
179+
180+ if errors:
181+ print("FAIL:")
182+ for e in errors:
183+ print(" -", e)
184+ sys.exit(1)
185+ print(f"PASS (failure, with files): unfetched {sorted(unfetched)}; pomonly artifactless")
186+ PY
187+
188+ ARTIFACT=" $HERE /artifact-failure"
189+ mkdir -p " $ARTIFACT /project"
190+ echo " sbt.version=$SBT_VERSION " > " $ARTIFACT /project/build.properties"
191+ cp " $PROJECT /scala-version.sbt" " $ARTIFACT /scala-version.sbt"
192+ rm -rf " $ARTIFACT /records.tsv" " $ARTIFACT /target" " $ARTIFACT " /* /target " $ARTIFACT /project/target"
193+ echo " + sbt $SBT_VERSION (scala $SCALA_VERSION ) (artifact failure)"
194+ run " $ARTIFACT " -Dsocket.recordsFile=" $ARTIFACT /records.tsv"
195+
196+ python3 - " $ARTIFACT /records.tsv" << 'PY '
197+ import sys
198+ rows = [l.rstrip('\n').split('\t') for l in open(sys.argv[1]) if l.strip()]
199+ unscannable = [r[1:] for r in rows if r[0] == 'unscannable']
200+ failed = {r[1] for r in rows if r[0] == 'failure'}
201+ errors = []
202+ if unscannable:
203+ errors.append(f"a missing artifact names no module, but must not make a project unscannable: {unscannable}")
204+ if failed != {'demo.fail:ok:1.0'}:
205+ errors.append(f"sbt's update fails on ok's missing tests jar, so it must be reported without files too; failures={sorted(failed)}")
206+ if errors:
207+ print("FAIL:")
208+ for e in errors:
209+ print(" -", e)
210+ sys.exit(1)
211+ print("PASS (artifact failure): ok's missing tests jar reported without files")
212+ PY
213+
214+ rm -rf " $ARTIFACT /records.tsv"
215+ echo " + sbt $SBT_VERSION (scala $SCALA_VERSION ) (artifact failure, with files)"
216+ run " $ARTIFACT " -Dsocket.withFiles=true -Dsocket.recordsFile=" $ARTIFACT /records.tsv"
217+
218+ python3 - " $ARTIFACT /records.tsv" << 'PY '
219+ import sys
220+ rows = [l.rstrip('\n').split('\t') for l in open(sys.argv[1]) if l.strip()]
221+ unscannable = [r[1:] for r in rows if r[0] == 'unscannable']
222+ project_of = {r[1]: r[2] for r in rows if r[0] == 'root'}
223+ unfetched_by = {}
224+ for r in rows:
225+ if r[0] == 'unfetched':
226+ unfetched_by.setdefault(project_of[r[1]], set()).add(r[2])
227+ failed = {r[1] for r in rows if r[0] == 'failure'}
228+ errors = []
229+ if unscannable:
230+ errors.append(f"a missing artifact names no module, but must not make a project unscannable: {unscannable}")
231+ for p in ('core', 'app'):
232+ if unfetched_by.get(p) != {'demo.fail:ok:jar:tests:1.0'}:
233+ errors.append(f"{p} reaches ok's missing tests jar, so only it may be unfetched: {sorted(unfetched_by.get(p, ()))}")
234+ if failed != {'demo.fail:ok:1.0'}:
235+ errors.append(f"only ok's tests jar is missing; failures={sorted(failed)}")
236+ if errors:
237+ print("FAIL:")
238+ for e in errors:
239+ print(" -", e)
240+ sys.exit(1)
241+ print("PASS (artifact failure, with files): ok's tests jar unfetched in core and app; nothing unscannable")
242+ PY
0 commit comments