From 2cc983388b86f065cc94fd1c448f617b7ad46fa1 Mon Sep 17 00:00:00 2001 From: Davide Angelocola Date: Sun, 19 Jul 2026 08:36:42 +0200 Subject: [PATCH] ci: release-smoke - run smoke checks as real JUnit @Test methods exec:exec ran everything through one main() method: if the first check threw, every check after it silently never ran. Split it into 16 independent @Test methods (junit-jupiter, moved to src/test/java) so `mvn test` reports each one separately via Surefire's standard "Tests run: X, Failures: 0" summary, and one failing check no longer hides the rest. --enable-native-access=ALL-UNNAMED moves from a java command line flag to Surefire's argLine. Also add a Duration column to the results table (started_at/ completed_at from the Actions API), so slow legs are visible without opening each job. Co-Authored-By: Claude Sonnet 5 --- .github/smoke/pom.xml | 25 +++--- .../Smoke.java => test/java/SmokeTest.java} | 85 ++++++++++--------- .github/workflows/release-smoke.yml | 22 +++-- 3 files changed, 70 insertions(+), 62 deletions(-) rename .github/smoke/src/{main/java/Smoke.java => test/java/SmokeTest.java} (91%) diff --git a/.github/smoke/pom.xml b/.github/smoke/pom.xml index 9c20381..c6e614a 100644 --- a/.github/smoke/pom.xml +++ b/.github/smoke/pom.xml @@ -4,8 +4,8 @@ reactor. It exercises a PUBLISHED release pulled from Maven Central (no source build, no submodule, no zig), one native classifier at a time, so it tests exactly what users consume. See release-smoke.yml, which runs this - once per OS/arch/libc leg via `mvnw compile exec:exec` with - -Dzstd.version/-Dzstd.classifier for the version and native jar under test. + once per OS/arch/libc leg via `mvnw test` with -Dzstd.version/ + -Dzstd.classifier for the version and native jar under test. --> ${zstd.version} runtime + + org.junit.jupiter + junit-jupiter + 6.1.2 + test + smoke - org.codehaus.mojo - exec-maven-plugin - 3.6.3 + org.apache.maven.plugins + maven-surefire-plugin + 3.5.6 - java - runtime - - --enable-native-access=ALL-UNNAMED - -classpath - - Smoke - + --enable-native-access=ALL-UNNAMED diff --git a/.github/smoke/src/main/java/Smoke.java b/.github/smoke/src/test/java/SmokeTest.java similarity index 91% rename from .github/smoke/src/main/java/Smoke.java rename to .github/smoke/src/test/java/SmokeTest.java index c29c4a7..a98b5ad 100644 --- a/.github/smoke/src/main/java/Smoke.java +++ b/.github/smoke/src/test/java/SmokeTest.java @@ -25,6 +25,9 @@ import io.github.dfa1.zstd.ZstdSkippableContent; import io.github.dfa1.zstd.ZstdStreamResult; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -41,7 +44,9 @@ /// library loads and links correctly on the host OS/arch/libc, and that /// off-heap `MemorySegment` interop and native struct layouts (streaming /// buffers, frame progression) work there. -/// Run against a release from Maven Central, one arch per CI matrix leg. +/// Run against a release from Maven Central, one arch per CI matrix leg, via +/// `mvn test` — a JUnit failure here fails just that one check, so a single +/// broken symbol doesn't hide every other result behind it. /// /// This deliberately does *not* re-derive full API correctness — that's the /// job of the module's JUnit suite, which runs once per PR and fails with a @@ -51,37 +56,22 @@ /// mismatch, native error-code marshaling). Pure-Java logic identical on every /// platform (enum bounds sweeps, dictionary-training algorithm variants, /// record equals/hashCode) belongs in the unit suite, not here. -public class Smoke { +class SmokeTest { private static final String PLATFORM = System.getProperty("os.name") + "/" + System.getProperty("os.arch"); - public static void main(String[] args) throws IOException { - versionAndSizing(); - coreRoundTrip(); - explicitBoundDecompress(); - zeroCopyFrameSize(); - corruptAndInvalidInput(); - frameIntrospection(); - skippableFrames(); - compressContextAdvancedParameters(); - decompressContextAdvanced(); - prefixCompression(); - multiThreadRoundTrip(); + private static ZstdDictionary dict; + private static byte[] message; + @BeforeAll + static void trainDictionary() { List samples = jsonSamples(); - ZstdDictionary dict = ZstdDictionary.train(samples, 8 * 1024); - byte[] message = samples.get(7); - - contextDictionaryPaths(dict, message); - digestedDictionaries(dict, message); - zeroCopyContextCompression(); - streamingZeroCopy(); - streamingIo(); - - System.out.println("OK " + PLATFORM + " | zstd " + Zstd.version() + " | all smoke checks passed"); + dict = ZstdDictionary.train(samples, 8 * 1024); + message = samples.get(7); } - private static void versionAndSizing() { + @Test + void versionAndSizing() { check(!Zstd.version().isBlank(), "version() returned blank"); check(Zstd.versionNumber() > 0, "versionNumber() not positive"); @@ -98,7 +88,8 @@ private static void versionAndSizing() { check(Zstd.estimateDecompressDictSize(4096) > 0, "estimateDecompressDictSize() not positive"); } - private static void coreRoundTrip() { + @Test + void coreRoundTrip() { byte[] original = sampleText(); byte[] compressedDefault = Zstd.compress(original); @@ -109,7 +100,8 @@ private static void coreRoundTrip() { check(compressed.length < original.length, "expected compression to shrink the input"); } - private static void explicitBoundDecompress() { + @Test + void explicitBoundDecompress() { byte[] original = sampleText(); byte[] compressed = Zstd.compress(original); @@ -125,7 +117,8 @@ private static void explicitBoundDecompress() { } } - private static void zeroCopyFrameSize() { + @Test + void zeroCopyFrameSize() { byte[] original = sampleText(); byte[] compressed = Zstd.compress(original); try (Arena arena = Arena.ofConfined()) { @@ -134,7 +127,8 @@ private static void zeroCopyFrameSize() { } } - private static void corruptAndInvalidInput() { + @Test + void corruptAndInvalidInput() { byte[] original = sampleText(); try (ZstdCompressContext cctx = new ZstdCompressContext().checksum(true)) { byte[] framed = cctx.compress(original); @@ -162,7 +156,8 @@ private static void corruptAndInvalidInput() { } } - private static void frameIntrospection() { + @Test + void frameIntrospection() { byte[] original = sampleText(); byte[] compressed = Zstd.compress(original); @@ -187,7 +182,8 @@ private static void frameIntrospection() { check(!header.hasChecksum(), "header(byte[]).hasChecksum() expected false (checksum not enabled)"); } - private static void skippableFrames() { + @Test + void skippableFrames() { byte[] payload = "smoke-test skippable payload".getBytes(StandardCharsets.UTF_8); byte[] frame = ZstdFrame.writeSkippableFrame(payload, 3); @@ -199,7 +195,8 @@ private static void skippableFrames() { check(read.magicVariant() == 3, "readSkippableFrame() magicVariant mismatch"); } - private static void compressContextAdvancedParameters() { + @Test + void compressContextAdvancedParameters() { byte[] original = sampleText(); try (ZstdCompressContext cctx = new ZstdCompressContext()) { cctx.level(5) @@ -220,7 +217,8 @@ private static void compressContextAdvancedParameters() { } } - private static void decompressContextAdvanced() { + @Test + void decompressContextAdvanced() { byte[] original = sampleText(); try (ZstdCompressContext cctx = new ZstdCompressContext().windowLog(23); ZstdDecompressContext dctx = new ZstdDecompressContext()) { @@ -236,7 +234,8 @@ private static void decompressContextAdvanced() { } } - private static void prefixCompression() { + @Test + void prefixCompression() { byte[] previousVersion = sampleText(); byte[] newVersion = (new String(previousVersion, StandardCharsets.UTF_8) + " and a tiny delta at the end") .getBytes(StandardCharsets.UTF_8); @@ -256,7 +255,8 @@ private static void prefixCompression() { } } - private static void multiThreadRoundTrip() { + @Test + void multiThreadRoundTrip() { // Native worker-thread spawning is exactly the platform-specific // behavior this smoke suite exists for: pthreads on glibc/musl, // Win32 _beginthreadex on Windows. 2 MiB clears zstd's 512 KiB @@ -271,7 +271,8 @@ private static void multiThreadRoundTrip() { } } - private static void contextDictionaryPaths(ZstdDictionary dict, byte[] message) { + @Test + void contextDictionaryPaths() { try (ZstdCompressContext cctx = new ZstdCompressContext(); ZstdDecompressContext dctx = new ZstdDecompressContext()) { byte[] compressed = cctx.compress(message, dict); @@ -280,7 +281,8 @@ private static void contextDictionaryPaths(ZstdDictionary dict, byte[] message) } } - private static void digestedDictionaries(ZstdDictionary dict, byte[] message) { + @Test + void digestedDictionaries() { try (ZstdCompressDictionary cdict = dict.compressDict(); ZstdDecompressDictionary ddict = dict.decompressDict(); ZstdCompressContext cctx = new ZstdCompressContext(); @@ -293,7 +295,8 @@ private static void digestedDictionaries(ZstdDictionary dict, byte[] message) { } } - private static void zeroCopyContextCompression() { + @Test + void zeroCopyContextCompression() { try (Arena arena = Arena.ofConfined(); ZstdCompressContext cctx = new ZstdCompressContext(); ZstdDecompressContext dctx = new ZstdDecompressContext()) { @@ -309,7 +312,8 @@ private static void zeroCopyContextCompression() { } } - private static void streamingZeroCopy() { + @Test + void streamingZeroCopy() { try (ZstdCompressStream cs = new ZstdCompressStream()) { check(cs.sizeOf() > 0, "no-arg ZstdCompressStream.sizeOf() not positive"); } @@ -344,7 +348,8 @@ private static void streamingZeroCopy() { } } - private static void streamingIo() throws IOException { + @Test + void streamingIo() throws IOException { byte[] original = sampleText(); ByteArrayOutputStream sink = new ByteArrayOutputStream(); diff --git a/.github/workflows/release-smoke.yml b/.github/workflows/release-smoke.yml index 0ba2ff5..e32644d 100644 --- a/.github/workflows/release-smoke.yml +++ b/.github/workflows/release-smoke.yml @@ -92,10 +92,11 @@ jobs: distribution: ${{ matrix.distribution }} java-version: '25' - # --- native runner path (glibc / macOS / Windows): a real `mvn compile - # exec:exec` against .github/smoke/pom.xml, via the vendored Maven - # Wrapper so no preinstalled Maven is required. -Dzstd.version and - # -Dzstd.classifier pin the release and native jar under test. --- + # --- native runner path (glibc / macOS / Windows): a real `mvn test` + # against .github/smoke/pom.xml (JUnit 5 via Surefire), via the + # vendored Maven Wrapper so no preinstalled Maven is required. + # -Dzstd.version and -Dzstd.classifier pin the release and native + # jar under test. --- - name: Smoke-test ${{ needs.resolve.outputs.version }} on ${{ matrix.classifier }} if: ${{ !matrix.container }} shell: bash @@ -104,7 +105,7 @@ jobs: run: | sh .github/smoke/mvnw -f .github/smoke/pom.xml --no-transfer-progress \ -Dzstd.version="$VERSION" -Dzstd.classifier=${{ matrix.classifier }} \ - compile exec:exec + test # --- container leg: extraction needs unzip, which some minimal images # (al2023) ship without any package manager at all to install it @@ -135,7 +136,7 @@ jobs: docker run --rm -v "$PWD:/w" -v "$RUNNER_TEMP/maven:/opt/maven:ro" -w /w "${{ matrix.container }}" \ /opt/maven/bin/mvn -f .github/smoke/pom.xml --no-transfer-progress \ -Dzstd.version="$VERSION" -Dzstd.classifier=${{ matrix.classifier }} \ - compile exec:exec + test results: name: results @@ -161,15 +162,18 @@ jobs: echo echo "Every host below gates the workflow — a failure on any one of them is a real portability regression." echo - echo "| Host | Result |" - echo "|---|---|" + echo "| Host | Result | Duration |" + echo "|---|---|---|" } >> "$GITHUB_STEP_SUMMARY" echo "$jobs" | jq -r '.[] | (if .conclusion == "success" then "✅ pass" elif .conclusion == "skipped" then "⏭️ skipped" else "❌ **" + (.conclusion // "unknown") + "**" end) as $result | - "| " + .name + " | " + $result + " |"' >> "$GITHUB_STEP_SUMMARY" + (((.completed_at | fromdateiso8601) - (.started_at | fromdateiso8601))) as $dur | + (($dur / 60) | floor) as $m | ($dur % 60) as $s | + (if $m > 0 then ($m | tostring) + "m " + ($s | tostring) + "s" else ($s | tostring) + "s" end) as $duration | + "| " + .name + " | " + $result + " | " + $duration + " |"' >> "$GITHUB_STEP_SUMMARY" total=$(echo "$jobs" | jq 'length') pass=$(echo "$jobs" | jq '[.[] | select(.conclusion == "success")] | length')