From 9a72557eb746a8143f5ce5e6be296ad80fdcb4a6 Mon Sep 17 00:00:00 2001 From: Davide Angelocola Date: Tue, 21 Jul 2026 23:19:21 +0200 Subject: [PATCH] feat: replace raw int compression level with ZstdCompressionLevel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduces ZstdCompressionLevel, a record wrapping the compression level as a validated int, checked in its compact constructor against Zstd.minCompressionLevel()/maxCompressionLevel() (IllegalArgumentException if out of range) rather than deferring to native clamping/errors. The accepted range is queried from libzstd once and cached, since it never changes for the life of the process. Ships DEFAULT/FASTEST/MAX constants for the common cases; construct directly (new ZstdCompressionLevel(n)) for any other level — there is no separate of(int) factory, since a public record's canonical constructor can't be hidden anyway. Replaces every naked int level parameter with it across Zstd, ZstdCompressContext, ZstdCompressStream, ZstdCompressDictionary, ZstdDictionary, and ZstdOutputStream. Zstd.min/max/defaultCompressionLevel() keep returning int, since they're the bound queries the new type validates against. Breaking change; the library is pre-1.0 so no compatibility shims are added. Closes #93. --- .github/smoke/src/test/java/SmokeTest.java | 16 ++-- CHANGELOG.md | 13 +++ README.md | 2 +- .../dfa1/zstd/bench/CompressBenchmark.java | 3 +- .../zstd/bench/GoldenCorpusBenchmark.java | 3 +- .../bench/MultiThreadCompressBenchmark.java | 3 +- docs/how-to.md | 12 +-- docs/tutorial.md | 2 +- .../github/dfa1/zstd/it/GoldenCorpusTest.java | 3 +- .../dfa1/zstd/it/ZstdInteropExtrasTest.java | 9 +- .../dfa1/zstd/it/ZstdJniInteropTest.java | 7 +- .../main/java/io/github/dfa1/zstd/Zstd.java | 21 ++-- .../github/dfa1/zstd/ZstdCompressContext.java | 14 +-- .../dfa1/zstd/ZstdCompressDictionary.java | 22 ++--- .../dfa1/zstd/ZstdCompressParameter.java | 2 +- .../github/dfa1/zstd/ZstdCompressStream.java | 12 +-- .../dfa1/zstd/ZstdCompressionLevel.java | 43 +++++++++ .../io/github/dfa1/zstd/ZstdDictionary.java | 31 +++--- .../io/github/dfa1/zstd/ZstdOutputStream.java | 20 ++-- .../io/github/dfa1/zstd/RefPrefixTest.java | 6 +- .../dfa1/zstd/ZstdCompressionLevelTest.java | 95 +++++++++++++++++++ .../github/dfa1/zstd/ZstdDictionaryTest.java | 31 +++--- .../io/github/dfa1/zstd/ZstdMemoryTest.java | 8 +- .../github/dfa1/zstd/ZstdParameterTest.java | 28 +++--- .../dfa1/zstd/ZstdSegmentStreamTest.java | 2 +- .../io/github/dfa1/zstd/ZstdStreamTest.java | 34 +++---- .../java/io/github/dfa1/zstd/ZstdTest.java | 4 +- .../io/github/dfa1/zstd/ZstdTestSupport.java | 10 +- 28 files changed, 314 insertions(+), 142 deletions(-) create mode 100644 zstd/src/main/java/io/github/dfa1/zstd/ZstdCompressionLevel.java create mode 100644 zstd/src/test/java/io/github/dfa1/zstd/ZstdCompressionLevelTest.java diff --git a/.github/smoke/src/test/java/SmokeTest.java b/.github/smoke/src/test/java/SmokeTest.java index a98b5ad..32534a4 100644 --- a/.github/smoke/src/test/java/SmokeTest.java +++ b/.github/smoke/src/test/java/SmokeTest.java @@ -5,6 +5,7 @@ import io.github.dfa1.zstd.Zstd; import io.github.dfa1.zstd.ZstdCompressContext; import io.github.dfa1.zstd.ZstdCompressDictionary; +import io.github.dfa1.zstd.ZstdCompressionLevel; import io.github.dfa1.zstd.ZstdCompressParameter; import io.github.dfa1.zstd.ZstdCompressStream; import io.github.dfa1.zstd.ZstdDecompressContext; @@ -82,9 +83,11 @@ void versionAndSizing() { check(def >= min && def <= max, "defaultCompressionLevel() out of [min,max]"); check(Zstd.compressBound(1000) >= 1000, "compressBound() below input size"); - check(Zstd.estimateCompressContextSize(def) > 0, "estimateCompressContextSize() not positive"); + check(Zstd.estimateCompressContextSize(ZstdCompressionLevel.DEFAULT) > 0, + "estimateCompressContextSize() not positive"); check(Zstd.estimateDecompressContextSize() > 0, "estimateDecompressContextSize() not positive"); - check(Zstd.estimateCompressDictSize(4096, def) > 0, "estimateCompressDictSize() not positive"); + check(Zstd.estimateCompressDictSize(4096, ZstdCompressionLevel.DEFAULT) > 0, + "estimateCompressDictSize() not positive"); check(Zstd.estimateDecompressDictSize(4096) > 0, "estimateDecompressDictSize() not positive"); } @@ -95,7 +98,7 @@ void coreRoundTrip() { byte[] compressedDefault = Zstd.compress(original); checkArrayEquals(original, Zstd.decompress(compressedDefault), "compress(byte[]) round-trip mismatch"); - byte[] compressed = Zstd.compress(original, Zstd.maxCompressionLevel()); + byte[] compressed = Zstd.compress(original, ZstdCompressionLevel.MAX); checkArrayEquals(original, Zstd.decompress(compressed), "compress(byte[], level) round-trip mismatch"); check(compressed.length < original.length, "expected compression to shrink the input"); } @@ -199,7 +202,7 @@ void skippableFrames() { void compressContextAdvancedParameters() { byte[] original = sampleText(); try (ZstdCompressContext cctx = new ZstdCompressContext()) { - cctx.level(5) + cctx.level(new ZstdCompressionLevel(5)) .checksum(true) .longDistanceMatching(true) .windowLog(20) @@ -211,7 +214,7 @@ void compressContextAdvancedParameters() { check(cctx.sizeOf() > 0, "cctx.sizeOf() not positive"); cctx.reset(ZstdResetDirective.SESSION_AND_PARAMETERS); - byte[] afterReset = cctx.level(3).compress(original); + byte[] afterReset = cctx.level(new ZstdCompressionLevel(3)).compress(original); checkArrayEquals(original, Zstd.decompress(afterReset, original.length), "compress after SESSION_AND_PARAMETERS reset mismatch"); } @@ -361,7 +364,8 @@ void streamingIo() throws IOException { } ByteArrayOutputStream sinkPledged = new ByteArrayOutputStream(); - try (ZstdOutputStream zout = ZstdOutputStream.withPledgedSize(sinkPledged, 5, original.length)) { + try (ZstdOutputStream zout = + ZstdOutputStream.withPledgedSize(sinkPledged, new ZstdCompressionLevel(5), original.length)) { zout.write(original); } byte[] pledgedFrame = sinkPledged.toByteArray(); diff --git a/CHANGELOG.md b/CHANGELOG.md index 17242a7..3860ca1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,19 @@ git tags, which trigger publication to Maven Central. ## [Unreleased] +### Changed +- **Breaking:** every public API that took a raw `int` compression level now + takes a `ZstdCompressionLevel` value type, which validates the level against + the linked libzstd's accepted range at construction (throwing + `IllegalArgumentException`) rather than deferring to native clamping/errors. + Affects `Zstd.compress(byte[], …)`, `Zstd.estimateCompressContextSize`, + `Zstd.estimateCompressDictSize`, `ZstdCompressContext.level`, + `ZstdCompressStream`, `ZstdOutputStream`, `ZstdCompressDictionary`, + `ZstdDictionary.compressDict`/`trainCover`/`trainFastCover`/`finalizeFrom`. + Use `new ZstdCompressionLevel(19)` or the `DEFAULT`/`FASTEST`/`MAX` constants. + The `Zstd.min/max/defaultCompressionLevel()` bound queries still return `int`. + ([#93](https://github.com/dfa1/zstd-java/issues/93)) + ## [0.10] - 2026-07-18 ### Added diff --git a/README.md b/README.md index 0bcc6e0..132ed5e 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ One-shot round-trip with `byte[]` — the convenient path: import io.github.dfa1.zstd.Zstd; byte[] data = ...; -byte[] frame = Zstd.compress(data); // or Zstd.compress(data, level) +byte[] frame = Zstd.compress(data); // or Zstd.compress(data, new ZstdCompressionLevel(19)) byte[] back = Zstd.decompress(frame); // size read from the frame header ``` diff --git a/benchmark/src/main/java/io/github/dfa1/zstd/bench/CompressBenchmark.java b/benchmark/src/main/java/io/github/dfa1/zstd/bench/CompressBenchmark.java index e8c445e..6582dfe 100644 --- a/benchmark/src/main/java/io/github/dfa1/zstd/bench/CompressBenchmark.java +++ b/benchmark/src/main/java/io/github/dfa1/zstd/bench/CompressBenchmark.java @@ -5,6 +5,7 @@ import io.airlift.compress.v3.zstd.ZstdJavaCompressor; import io.github.dfa1.zstd.Zstd; import io.github.dfa1.zstd.ZstdCompressContext; +import io.github.dfa1.zstd.ZstdCompressionLevel; import java.lang.foreign.Arena; import java.lang.foreign.MemorySegment; import java.util.concurrent.TimeUnit; @@ -58,7 +59,7 @@ public void setup() { src = BenchData.generate(size); int bound = (int) Zstd.compressBound(size); - ffmCtx = new ZstdCompressContext().level(level); + ffmCtx = new ZstdCompressContext().level(new ZstdCompressionLevel(level)); ffmDst = new byte[bound]; arena = Arena.ofConfined(); diff --git a/benchmark/src/main/java/io/github/dfa1/zstd/bench/GoldenCorpusBenchmark.java b/benchmark/src/main/java/io/github/dfa1/zstd/bench/GoldenCorpusBenchmark.java index 4b77968..a0e4687 100644 --- a/benchmark/src/main/java/io/github/dfa1/zstd/bench/GoldenCorpusBenchmark.java +++ b/benchmark/src/main/java/io/github/dfa1/zstd/bench/GoldenCorpusBenchmark.java @@ -4,6 +4,7 @@ import io.github.dfa1.zstd.Zstd; import io.github.dfa1.zstd.ZstdCompressContext; +import io.github.dfa1.zstd.ZstdCompressionLevel; import io.github.dfa1.zstd.ZstdDecompressContext; import java.io.UncheckedIOException; import java.io.IOException; @@ -88,7 +89,7 @@ public void setup() { srcSize = src.length; frame = Zstd.compress(src); - cctx = new ZstdCompressContext().level(level); + cctx = new ZstdCompressContext().level(new ZstdCompressionLevel(level)); dctx = new ZstdDecompressContext(); bound = (int) Zstd.compressBound(srcSize); compressDst = new byte[bound]; diff --git a/benchmark/src/main/java/io/github/dfa1/zstd/bench/MultiThreadCompressBenchmark.java b/benchmark/src/main/java/io/github/dfa1/zstd/bench/MultiThreadCompressBenchmark.java index afd978f..7aec9f6 100644 --- a/benchmark/src/main/java/io/github/dfa1/zstd/bench/MultiThreadCompressBenchmark.java +++ b/benchmark/src/main/java/io/github/dfa1/zstd/bench/MultiThreadCompressBenchmark.java @@ -4,6 +4,7 @@ import io.github.dfa1.zstd.Zstd; import io.github.dfa1.zstd.ZstdCompressContext; +import io.github.dfa1.zstd.ZstdCompressionLevel; import io.github.dfa1.zstd.ZstdCompressParameter; import java.lang.foreign.Arena; import java.lang.foreign.MemorySegment; @@ -59,7 +60,7 @@ public void setup() { byte[] src = BenchData.generate(size); int bound = (int) Zstd.compressBound(size); - ctx = new ZstdCompressContext().level(level); + ctx = new ZstdCompressContext().level(new ZstdCompressionLevel(level)); if (nbWorkers > 0) { ctx.parameter(ZstdCompressParameter.NB_WORKERS, nbWorkers); } diff --git a/docs/how-to.md b/docs/how-to.md index 266c2d0..33db9e0 100644 --- a/docs/how-to.md +++ b/docs/how-to.md @@ -8,7 +8,7 @@ Task-focused recipes. Each assumes you have the library on the classpath (see th Reuse a context to amortize native allocation across many calls: ```java -try (ZstdCompressContext cctx = new ZstdCompressContext().level(19); +try (ZstdCompressContext cctx = new ZstdCompressContext().level(new ZstdCompressionLevel(19)); ZstdDecompressContext dctx = new ZstdDecompressContext()) { byte[] packed = cctx.compress(message); byte[] restored = dctx.decompress(packed, message.length); @@ -26,7 +26,7 @@ or to abort a half-written frame and start clean — without freeing and recreat it. Pick what to clear with `ZstdResetDirective`: ```java -try (ZstdCompressContext cctx = new ZstdCompressContext().level(19)) { +try (ZstdCompressContext cctx = new ZstdCompressContext().level(new ZstdCompressionLevel(19))) { byte[] a = cctx.compress(first); // Cheap: drop any unflushed frame state, keep the level and parameters. @@ -51,7 +51,7 @@ matching) set on the context. To combine the two, make the dictionary *sticky* with `loadDictionary` — then the normal `compress` path honors both: ```java -try (ZstdCompressContext cctx = new ZstdCompressContext().level(19).checksum(true)) { +try (ZstdCompressContext cctx = new ZstdCompressContext().level(new ZstdCompressionLevel(19)).checksum(true)) { cctx.loadDictionary(dict); // ZstdDictionary, or a native MemorySegment byte[] frame = cctx.compress(record); // dictionary + checksum, together } @@ -62,7 +62,7 @@ by reference — no per-call digesting, no copy. It pairs with `reset` for a pooled, recycled context: ```java -try (ZstdCompressDictionary cdict = dict.compressDict(19)) { +try (ZstdCompressDictionary cdict = dict.compressDict(new ZstdCompressionLevel(19))) { // one cctx per pooled worker, all sharing the one digested dictionary try (ZstdCompressContext cctx = new ZstdCompressContext()) { cctx.refDictionary(cdict); // borrowed; cdict must outlive cctx @@ -105,7 +105,7 @@ ZstdDictionary reloaded = ZstdDictionary.of(persisted); On a hot path, digest the dictionary once to skip per-call setup: ```java -try (ZstdCompressDictionary cdict = dict.compressDict(19); +try (ZstdCompressDictionary cdict = dict.compressDict(new ZstdCompressionLevel(19)); ZstdDecompressDictionary ddict = dict.decompressDict(); ZstdCompressContext cctx = new ZstdCompressContext(); ZstdDecompressContext dctx = new ZstdDecompressContext()) { @@ -231,7 +231,7 @@ can't size the arena (see [the explanation](zero-copy.md)). Tell the encoder the total up front and it stamps the content size into the header: ```java -try (var zout = ZstdOutputStream.withPledgedSize(sink, 6, data.length)) { +try (var zout = ZstdOutputStream.withPledgedSize(sink, new ZstdCompressionLevel(6), data.length)) { zout.write(data); // pledge must equal bytes written } MemorySegment src = MemorySegment.ofBuffer(mmap); // downstream, in a mapped reader diff --git a/docs/tutorial.md b/docs/tutorial.md index eff8a68..82639d6 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -116,7 +116,7 @@ incrementally, so memory stays flat no matter how big the payload. ```java // compress a file as you write it -try (var out = new ZstdOutputStream(Files.newOutputStream(packed), 9)) { +try (var out = new ZstdOutputStream(Files.newOutputStream(packed), new ZstdCompressionLevel(9))) { Files.copy(source, out); } diff --git a/integration-tests/src/test/java/io/github/dfa1/zstd/it/GoldenCorpusTest.java b/integration-tests/src/test/java/io/github/dfa1/zstd/it/GoldenCorpusTest.java index d8692c5..33be85c 100644 --- a/integration-tests/src/test/java/io/github/dfa1/zstd/it/GoldenCorpusTest.java +++ b/integration-tests/src/test/java/io/github/dfa1/zstd/it/GoldenCorpusTest.java @@ -3,6 +3,7 @@ import io.github.dfa1.zstd.ZstdDictionaryId; import io.github.dfa1.zstd.Zstd; import io.github.dfa1.zstd.ZstdCompressContext; +import io.github.dfa1.zstd.ZstdCompressionLevel; import io.github.dfa1.zstd.ZstdDecompressContext; import io.github.dfa1.zstd.ZstdDictionary; import io.github.dfa1.zstd.ZstdException; @@ -136,7 +137,7 @@ void javaCompressJniDecompress(String name, Path file) { byte[] data = read(file); // When - byte[] frame = Zstd.compress(data, Zstd.defaultCompressionLevel()); + byte[] frame = Zstd.compress(data, ZstdCompressionLevel.DEFAULT); // Then assertThat(com.github.luben.zstd.Zstd.decompress(frame, data.length)).isEqualTo(data); diff --git a/integration-tests/src/test/java/io/github/dfa1/zstd/it/ZstdInteropExtrasTest.java b/integration-tests/src/test/java/io/github/dfa1/zstd/it/ZstdInteropExtrasTest.java index 714bc22..434eae1 100644 --- a/integration-tests/src/test/java/io/github/dfa1/zstd/it/ZstdInteropExtrasTest.java +++ b/integration-tests/src/test/java/io/github/dfa1/zstd/it/ZstdInteropExtrasTest.java @@ -3,6 +3,7 @@ import com.github.luben.zstd.ZstdCompressCtx; import io.github.dfa1.zstd.ZstdDictionaryId; import io.github.dfa1.zstd.Zstd; +import io.github.dfa1.zstd.ZstdCompressionLevel; import io.github.dfa1.zstd.ZstdDictionary; import io.github.dfa1.zstd.ZstdException; import io.github.dfa1.zstd.ZstdFrame; @@ -127,7 +128,7 @@ void jniStreamSkipsJavaSkippableFrame() { byte[] payload = "after the skippable frame ".repeat(1000).getBytes(StandardCharsets.UTF_8); byte[] meta = "sidecar-metadata".getBytes(StandardCharsets.UTF_8); byte[] skippable = ZstdFrame.writeSkippableFrame(meta, 0); - byte[] real = Zstd.compress(payload, Zstd.defaultCompressionLevel()); + byte[] real = Zstd.compress(payload, ZstdCompressionLevel.DEFAULT); // When byte[] restored = jniStreamDecode(concat(skippable, real)); @@ -164,8 +165,8 @@ class MultiFrame { void javaFramesConcatReadByJniStream() { // Given byte[] joined = concat( - Zstd.compress(a, Zstd.defaultCompressionLevel()), - Zstd.compress(b, Zstd.defaultCompressionLevel())); + Zstd.compress(a, ZstdCompressionLevel.DEFAULT), + Zstd.compress(b, ZstdCompressionLevel.DEFAULT)); // When byte[] restored = jniStreamDecode(joined); @@ -266,7 +267,7 @@ void javaChunkedWriteJniRead(String name, byte[] data) throws IOException { ByteArrayOutputStream sink = new ByteArrayOutputStream(); // When - try (ZstdOutputStream zout = new ZstdOutputStream(sink, 7)) { + try (ZstdOutputStream zout = new ZstdOutputStream(sink, new ZstdCompressionLevel(7))) { writeInChunks(zout, data); } diff --git a/integration-tests/src/test/java/io/github/dfa1/zstd/it/ZstdJniInteropTest.java b/integration-tests/src/test/java/io/github/dfa1/zstd/it/ZstdJniInteropTest.java index 16d2c88..04173a0 100644 --- a/integration-tests/src/test/java/io/github/dfa1/zstd/it/ZstdJniInteropTest.java +++ b/integration-tests/src/test/java/io/github/dfa1/zstd/it/ZstdJniInteropTest.java @@ -5,6 +5,7 @@ import io.github.dfa1.zstd.Zstd; import io.github.dfa1.zstd.ZstdCompressContext; import io.github.dfa1.zstd.ZstdCompressDictionary; +import io.github.dfa1.zstd.ZstdCompressionLevel; import io.github.dfa1.zstd.ZstdCompressParameter; import io.github.dfa1.zstd.ZstdDecompressContext; import io.github.dfa1.zstd.ZstdDictionary; @@ -59,7 +60,7 @@ void jniCompressJavaDecompress(int level, byte[] data) { @ParameterizedTest @MethodSource("payloadsAndLevels") void javaCompressJniDecompress(int level, byte[] data) { - byte[] frame = Zstd.compress(data, level); + byte[] frame = Zstd.compress(data, new ZstdCompressionLevel(level)); assertThat(com.github.luben.zstd.Zstd.decompress(frame, data.length)).isEqualTo(data); } @@ -71,7 +72,7 @@ void javaStreamToJniStream() throws Exception { byte[] data = "interop streaming ".repeat(50_000).getBytes(StandardCharsets.UTF_8); ByteArrayOutputStream sink = new ByteArrayOutputStream(); - try (ZstdOutputStream zout = new ZstdOutputStream(sink, 7)) { + try (ZstdOutputStream zout = new ZstdOutputStream(sink, new ZstdCompressionLevel(7))) { zout.write(data); } byte[] restored; @@ -197,7 +198,7 @@ void javaReferencedDigestedDictJniDictDecompress() { byte[] sample = sample(44); byte[] frame; - try (ZstdCompressDictionary cdict = new ZstdCompressDictionary(dict, Zstd.defaultCompressionLevel()); + try (ZstdCompressDictionary cdict = new ZstdCompressDictionary(dict, ZstdCompressionLevel.DEFAULT); ZstdCompressContext ctx = new ZstdCompressContext()) { ctx.refDictionary(cdict); frame = ctx.compress(sample); diff --git a/zstd/src/main/java/io/github/dfa1/zstd/Zstd.java b/zstd/src/main/java/io/github/dfa1/zstd/Zstd.java index fa3b463..b18779b 100644 --- a/zstd/src/main/java/io/github/dfa1/zstd/Zstd.java +++ b/zstd/src/main/java/io/github/dfa1/zstd/Zstd.java @@ -29,23 +29,22 @@ public final class Zstd { /// @param src bytes to compress /// @return a self-describing zstd frame public static byte[] compress(byte[] src) { - return compress(src, defaultCompressionLevel()); + return compress(src, ZstdCompressionLevel.DEFAULT); } /// Compresses `src` at the given level. /// /// @param src bytes to compress - /// @param level compression level in [[#minCompressionLevel()], [#maxCompressionLevel()]]; - /// higher is smaller but slower + /// @param level the compression level to use; higher is smaller but slower /// @return a self-describing zstd frame - public static byte[] compress(byte[] src, int level) { + public static byte[] compress(byte[] src, ZstdCompressionLevel level) { Objects.requireNonNull(src, "src"); try (Arena arena = Arena.ofConfined()) { MemorySegment in = copyIn(arena, src); long bound = compressBound(src.length); MemorySegment out = arena.allocate(bound); long written = NativeCall.checkReturnValue(() -> (long) Bindings.COMPRESS.invokeExact( - out, bound, in, (long) src.length, level)); + out, bound, in, (long) src.length, level.value())); return copyOut(out, written); } } @@ -239,11 +238,11 @@ public static int defaultCompressionLevel() { /// Estimates the memory a compression context will use at `level`, before /// creating one — useful for budgeting. /// - /// @param level the compression level + /// @param level the compression level to use /// @return the estimated context size in bytes - public static long estimateCompressContextSize(int level) { + public static long estimateCompressContextSize(ZstdCompressionLevel level) { try { - return (long) Bindings.ESTIMATE_CCTX_SIZE.invokeExact(level); + return (long) Bindings.ESTIMATE_CCTX_SIZE.invokeExact(level.value()); } catch (Throwable t) { throw NativeCall.rethrow(t); } @@ -264,11 +263,11 @@ public static long estimateDecompressContextSize() { /// will use at `level`. /// /// @param dictSize the raw dictionary size in bytes - /// @param level the compression level + /// @param level the compression level to use /// @return the estimated digested-dictionary size in bytes - public static long estimateCompressDictSize(long dictSize, int level) { + public static long estimateCompressDictSize(long dictSize, ZstdCompressionLevel level) { try { - return (long) Bindings.ESTIMATE_CDICT_SIZE.invokeExact(dictSize, level); + return (long) Bindings.ESTIMATE_CDICT_SIZE.invokeExact(dictSize, level.value()); } catch (Throwable t) { throw NativeCall.rethrow(t); } diff --git a/zstd/src/main/java/io/github/dfa1/zstd/ZstdCompressContext.java b/zstd/src/main/java/io/github/dfa1/zstd/ZstdCompressContext.java index 5e8d4cf..684927a 100644 --- a/zstd/src/main/java/io/github/dfa1/zstd/ZstdCompressContext.java +++ b/zstd/src/main/java/io/github/dfa1/zstd/ZstdCompressContext.java @@ -17,7 +17,7 @@ /// done; never keep it in a long-lived pool. /// /// {@snippet : -/// try (ZstdCompressContext ctx = new ZstdCompressContext().level(19)) { +/// try (ZstdCompressContext ctx = new ZstdCompressContext().level(new ZstdCompressionLevel(19))) { /// for (byte[] msg : messages) { /// sink.accept(ctx.compress(msg)); /// } @@ -25,12 +25,12 @@ /// } public final class ZstdCompressContext extends NativeObject { - private int level; + private ZstdCompressionLevel level; /// Creates a new compression context at the default level. public ZstdCompressContext() { super(create()); - this.level = Zstd.defaultCompressionLevel(); + this.level = ZstdCompressionLevel.DEFAULT; } private static MemorySegment create() { @@ -41,9 +41,9 @@ private static MemorySegment create() { /// /// @param level the compression level to use /// @return `this`, for chaining - public ZstdCompressContext level(int level) { + public ZstdCompressContext level(ZstdCompressionLevel level) { this.level = level; - setParam(ZstdCompressParameter.COMPRESSION_LEVEL, level); + setParam(ZstdCompressParameter.COMPRESSION_LEVEL, level.value()); return this; } @@ -108,7 +108,7 @@ public ZstdCompressContext reset(ZstdResetDirective directive) { Objects.requireNonNull(directive, "directive"); NativeCall.checkReturnValue(() -> (long) Bindings.CCTX_RESET.invokeExact(ptr(), directive.value())); if (directive != ZstdResetDirective.SESSION_ONLY) { - this.level = Zstd.defaultCompressionLevel(); + this.level = ZstdCompressionLevel.DEFAULT; } return this; } @@ -255,7 +255,7 @@ public byte[] compress(byte[] src, ZstdDictionary dict) { long bound = Zstd.compressBound(src.length); MemorySegment out = arena.allocate(bound); long written = NativeCall.checkReturnValue(() -> (long) Bindings.COMPRESS_USING_DICT.invokeExact( - ptr(), out, bound, in, (long) src.length, dseg, (long) d.length, level)); + ptr(), out, bound, in, (long) src.length, dseg, (long) d.length, level.value())); return Zstd.copyOut(out, written); } } diff --git a/zstd/src/main/java/io/github/dfa1/zstd/ZstdCompressDictionary.java b/zstd/src/main/java/io/github/dfa1/zstd/ZstdCompressDictionary.java index f8e10bc..2d53d30 100644 --- a/zstd/src/main/java/io/github/dfa1/zstd/ZstdCompressDictionary.java +++ b/zstd/src/main/java/io/github/dfa1/zstd/ZstdCompressDictionary.java @@ -15,13 +15,13 @@ /// Immutable once built and safe to share across threads (the digested dictionary is read-only). public final class ZstdCompressDictionary extends NativeObject { - private final int level; + private final ZstdCompressionLevel level; /// Digests `dict` for compression at the given level. /// /// @param dict the dictionary to digest /// @param level the compression level to fix for this digested dictionary - public ZstdCompressDictionary(ZstdDictionary dict, int level) { + public ZstdCompressDictionary(ZstdDictionary dict, ZstdCompressionLevel level) { super(create(dict, level)); this.level = level; } @@ -30,7 +30,7 @@ public ZstdCompressDictionary(ZstdDictionary dict, int level) { /// /// @param dict the dictionary to digest public ZstdCompressDictionary(ZstdDictionary dict) { - this(dict, Zstd.defaultCompressionLevel()); + this(dict, ZstdCompressionLevel.DEFAULT); } /// Digests a native dictionary segment for compression at the given level, @@ -39,11 +39,11 @@ public ZstdCompressDictionary(ZstdDictionary dict) { /// `dict` must be a native (off-heap) [MemorySegment] — e.g. an mmap slice or /// an arena buffer. Its bytes are copied into the digested dictionary, so the /// segment may be released once the constructor returns. Heap-backed callers - /// should use [ZstdCompressDictionary(ZstdDictionary, int)] instead. + /// should use [ZstdCompressDictionary(ZstdDictionary, ZstdCompressionLevel)] instead. /// /// @param dict native dictionary content /// @param level the compression level to fix for this digested dictionary - public ZstdCompressDictionary(MemorySegment dict, int level) { + public ZstdCompressDictionary(MemorySegment dict, ZstdCompressionLevel level) { super(create(dict, level)); this.level = level; } @@ -53,29 +53,29 @@ public ZstdCompressDictionary(MemorySegment dict, int level) { /// /// @param dict native dictionary content public ZstdCompressDictionary(MemorySegment dict) { - this(dict, Zstd.defaultCompressionLevel()); + this(dict, ZstdCompressionLevel.DEFAULT); } - private static MemorySegment create(ZstdDictionary dict, int level) { + private static MemorySegment create(ZstdDictionary dict, ZstdCompressionLevel level) { Objects.requireNonNull(dict, "dict"); try (Arena arena = Arena.ofConfined()) { byte[] raw = dict.raw(); MemorySegment d = Zstd.copyIn(arena, raw); return NativeCall.createOrThrow("ZSTD_createCDict", - () -> (MemorySegment) Bindings.CREATE_CDICT.invokeExact(d, (long) raw.length, level)); + () -> (MemorySegment) Bindings.CREATE_CDICT.invokeExact(d, (long) raw.length, level.value())); } } - private static MemorySegment create(MemorySegment dict, int level) { + private static MemorySegment create(MemorySegment dict, ZstdCompressionLevel level) { NativeCall.requireNative(dict, "dict"); return NativeCall.createOrThrow("ZSTD_createCDict", - () -> (MemorySegment) Bindings.CREATE_CDICT.invokeExact(dict, dict.byteSize(), level)); + () -> (MemorySegment) Bindings.CREATE_CDICT.invokeExact(dict, dict.byteSize(), level.value())); } /// The level this dictionary was digested at. /// /// @return the fixed compression level - public int level() { + public ZstdCompressionLevel level() { return level; } diff --git a/zstd/src/main/java/io/github/dfa1/zstd/ZstdCompressParameter.java b/zstd/src/main/java/io/github/dfa1/zstd/ZstdCompressParameter.java index dd4fb73..1bc65da 100644 --- a/zstd/src/main/java/io/github/dfa1/zstd/ZstdCompressParameter.java +++ b/zstd/src/main/java/io/github/dfa1/zstd/ZstdCompressParameter.java @@ -9,7 +9,7 @@ /// are validated natively — an out-of-range value raises a [ZstdException]. public enum ZstdCompressParameter { - /// Compression level. Prefer [ZstdCompressContext#level(int)]. + /// Compression level. Prefer [ZstdCompressContext#level(ZstdCompressionLevel)]. COMPRESSION_LEVEL(100), /// Maximum back-reference distance, as a power of two (larger = better ratio, more memory). WINDOW_LOG(101), diff --git a/zstd/src/main/java/io/github/dfa1/zstd/ZstdCompressStream.java b/zstd/src/main/java/io/github/dfa1/zstd/ZstdCompressStream.java index 84aef8e..a776159 100644 --- a/zstd/src/main/java/io/github/dfa1/zstd/ZstdCompressStream.java +++ b/zstd/src/main/java/io/github/dfa1/zstd/ZstdCompressStream.java @@ -36,21 +36,21 @@ public final class ZstdCompressStream extends NativeObject { /// Creates a streaming compressor at the default level. public ZstdCompressStream() { - this(Zstd.defaultCompressionLevel()); + this(ZstdCompressionLevel.DEFAULT); } /// Creates a streaming compressor at `level`. /// - /// @param level the compression level - public ZstdCompressStream(int level) { + /// @param level the compression level to use + public ZstdCompressStream(ZstdCompressionLevel level) { this(level, null); } /// Creates a streaming compressor at `level` using `dictionary`. /// - /// @param level the compression level + /// @param level the compression level to use /// @param dictionary the dictionary to compress against, or `null` for none - public ZstdCompressStream(int level, ZstdDictionary dictionary) { + public ZstdCompressStream(ZstdCompressionLevel level, ZstdDictionary dictionary) { // Own the context first, so any failure setting it up is cleaned up by // close() — one release path, no leak on a half-built stream. super(createCctx()); @@ -59,7 +59,7 @@ public ZstdCompressStream(int level, ZstdDictionary dictionary) { this.out = new ZstdStreamBuffer(arena); try { NativeCall.checkReturnValue(() -> (long) Bindings.CCTX_SET_PARAMETER.invokeExact( - ptr(), ZstdCompressParameter.COMPRESSION_LEVEL.value(), level)); + ptr(), ZstdCompressParameter.COMPRESSION_LEVEL.value(), level.value())); if (dictionary != null) { loadDictionary(dictionary); } diff --git a/zstd/src/main/java/io/github/dfa1/zstd/ZstdCompressionLevel.java b/zstd/src/main/java/io/github/dfa1/zstd/ZstdCompressionLevel.java new file mode 100644 index 0000000..2e4bbf7 --- /dev/null +++ b/zstd/src/main/java/io/github/dfa1/zstd/ZstdCompressionLevel.java @@ -0,0 +1,43 @@ +package io.github.dfa1.zstd; + +/// A validated zstd compression level, replacing a naked `int` at every public +/// API boundary that takes one. +/// +/// zstd levels span two very different ranges under one number. `1..`[#MAX] are +/// the discrete, named levels most people mean by "zstd level"; everything from +/// [#FASTEST] up to (but not including) `1` is a continuous fast-mode +/// acceleration knob (`ZSTD_c_targetLength` under the hood), not discrete named +/// levels. This type does not attempt to model that split — it validates that a +/// raw level falls within the range the linked libzstd actually accepts +/// ([Zstd#minCompressionLevel()]..[Zstd#maxCompressionLevel()]), catching a bad +/// level in Java before it reaches native code, rather than relying on zstd's +/// own clamping/error behavior. +/// +/// Prefer the [#DEFAULT] / [#FASTEST] / [#MAX] constants for those specific +/// levels; construct directly for any other level. +/// +/// @param value the raw zstd compression level +public record ZstdCompressionLevel(int value) { + + // Queried once: the linked libzstd's accepted range is fixed for the life of + // the process, so caching it here saves two native calls + // (ZSTD_minCLevel/ZSTD_maxCLevel) on every construction. + private static final int MIN_ACCEPTED = Zstd.minCompressionLevel(); + private static final int MAX_ACCEPTED = Zstd.maxCompressionLevel(); + + /// The level [Zstd#compress(byte[])] uses when none is given. + public static final ZstdCompressionLevel DEFAULT = new ZstdCompressionLevel(Zstd.defaultCompressionLevel()); + + /// The fastest, lowest-ratio level this linked libzstd accepts. + public static final ZstdCompressionLevel FASTEST = new ZstdCompressionLevel(MIN_ACCEPTED); + + /// The slowest, highest-ratio level this linked libzstd accepts. + public static final ZstdCompressionLevel MAX = new ZstdCompressionLevel(MAX_ACCEPTED); + + /// Validates `value` against the linked libzstd's accepted range. + public ZstdCompressionLevel { + if (value < MIN_ACCEPTED || value > MAX_ACCEPTED) { + throw new IllegalArgumentException("level " + value + " outside [" + MIN_ACCEPTED + ", " + MAX_ACCEPTED + "]"); + } + } +} diff --git a/zstd/src/main/java/io/github/dfa1/zstd/ZstdDictionary.java b/zstd/src/main/java/io/github/dfa1/zstd/ZstdDictionary.java index c621366..30691c9 100644 --- a/zstd/src/main/java/io/github/dfa1/zstd/ZstdDictionary.java +++ b/zstd/src/main/java/io/github/dfa1/zstd/ZstdDictionary.java @@ -120,18 +120,20 @@ public static ZstdDictionary train(List samples, int maxDictBytes) { /// @return the trained dictionary /// @throws ZstdException if training fails public static ZstdDictionary trainCover(List samples, int maxDictBytes) { - return trainCover(samples, maxDictBytes, 0); + return optimize(samples, maxDictBytes, 0, false); } /// Trains a COVER dictionary optimized for a specific compression level. /// /// @param samples representative payloads to learn from /// @param maxDictBytes upper bound on the produced dictionary size - /// @param compressionLevel the level the dictionary will be used at (0 = default) + /// @param compressionLevel the level the dictionary will be used at (a level + /// whose [ZstdCompressionLevel#value()] is 0 = default) /// @return the trained dictionary /// @throws ZstdException if training fails - public static ZstdDictionary trainCover(List samples, int maxDictBytes, int compressionLevel) { - return optimize(samples, maxDictBytes, compressionLevel, false); + public static ZstdDictionary trainCover(List samples, int maxDictBytes, + ZstdCompressionLevel compressionLevel) { + return optimize(samples, maxDictBytes, compressionLevel.value(), false); } /// Trains a dictionary with the fast COVER algorithm, auto-tuning its @@ -143,18 +145,20 @@ public static ZstdDictionary trainCover(List samples, int maxDictBytes, /// @return the trained dictionary /// @throws ZstdException if training fails public static ZstdDictionary trainFastCover(List samples, int maxDictBytes) { - return trainFastCover(samples, maxDictBytes, 0); + return optimize(samples, maxDictBytes, 0, true); } /// Trains a fast COVER dictionary optimized for a specific compression level. /// /// @param samples representative payloads to learn from /// @param maxDictBytes upper bound on the produced dictionary size - /// @param compressionLevel the level the dictionary will be used at (0 = default) + /// @param compressionLevel the level the dictionary will be used at (a level + /// whose [ZstdCompressionLevel#value()] is 0 = default) /// @return the trained dictionary /// @throws ZstdException if training fails - public static ZstdDictionary trainFastCover(List samples, int maxDictBytes, int compressionLevel) { - return optimize(samples, maxDictBytes, compressionLevel, true); + public static ZstdDictionary trainFastCover(List samples, int maxDictBytes, + ZstdCompressionLevel compressionLevel) { + return optimize(samples, maxDictBytes, compressionLevel.value(), true); } private static ZstdDictionary optimize(List samples, int maxDictBytes, @@ -189,11 +193,12 @@ private static ZstdDictionary optimize(List samples, int maxDictBytes, /// @param content the raw dictionary content to wrap /// @param samples representative payloads to tune entropy tables on /// @param maxDictBytes upper bound on the produced dictionary size - /// @param compressionLevel the level the dictionary will be used at (0 = default) + /// @param compressionLevel the level the dictionary will be used at (a level + /// whose [ZstdCompressionLevel#value()] is 0 = default) /// @return the finalized dictionary /// @throws ZstdException if finalization fails public static ZstdDictionary finalizeFrom(byte[] content, List samples, - int maxDictBytes, int compressionLevel) { + int maxDictBytes, ZstdCompressionLevel compressionLevel) { Objects.requireNonNull(content, "content"); Objects.requireNonNull(samples, SAMPLES); requireNonEmpty(samples, "finalize"); @@ -201,7 +206,7 @@ public static ZstdDictionary finalizeFrom(byte[] content, List samples, FlatSamples in = flatten(arena, samples); MemorySegment contentSeg = Zstd.copyIn(arena, content); MemorySegment params = arena.allocate(Bindings.ZDICT_PARAMS_LAYOUT); - params.set(JAVA_INT, 0, compressionLevel); // compressionLevel; notificationLevel/dictID = 0 + params.set(JAVA_INT, 0, compressionLevel.value()); // compressionLevel; notificationLevel/dictID = 0 MemorySegment dictBuf = arena.allocate(maxDictBytes); long produced; try { @@ -307,12 +312,12 @@ public int size() { /// /// @param level the compression level to fix for the digested dictionary /// @return a digested compression dictionary the caller must close - public ZstdCompressDictionary compressDict(int level) { + public ZstdCompressDictionary compressDict(ZstdCompressionLevel level) { return new ZstdCompressDictionary(this, level); } /// Digests this dictionary for compression at the library default level. - /// Otherwise identical to [#compressDict(int)]. + /// Otherwise identical to [#compressDict(ZstdCompressionLevel)]. /// /// @return a digested compression dictionary the caller must close public ZstdCompressDictionary compressDict() { diff --git a/zstd/src/main/java/io/github/dfa1/zstd/ZstdOutputStream.java b/zstd/src/main/java/io/github/dfa1/zstd/ZstdOutputStream.java index 754e14d..b916a23 100644 --- a/zstd/src/main/java/io/github/dfa1/zstd/ZstdOutputStream.java +++ b/zstd/src/main/java/io/github/dfa1/zstd/ZstdOutputStream.java @@ -19,7 +19,7 @@ /// Closing finishes the frame and closes the underlying stream. /// /// {@snippet : -/// try (ZstdOutputStream zout = new ZstdOutputStream(Files.newOutputStream(path), 19)) { +/// try (ZstdOutputStream zout = new ZstdOutputStream(Files.newOutputStream(path), new ZstdCompressionLevel(19))) { /// source.transferTo(zout); /// } /// } @@ -50,14 +50,14 @@ public final class ZstdOutputStream extends OutputStream { /// /// @param out the stream to write the compressed frame to public ZstdOutputStream(OutputStream out) { - this(out, Zstd.defaultCompressionLevel()); + this(out, ZstdCompressionLevel.DEFAULT); } /// Wraps `out`, compressing at `level`. /// /// @param out the stream to write the compressed frame to - /// @param level the compression level - public ZstdOutputStream(OutputStream out, int level) { + /// @param level the compression level to use + public ZstdOutputStream(OutputStream out, ZstdCompressionLevel level) { this(out, level, null); } @@ -66,7 +66,7 @@ public ZstdOutputStream(OutputStream out, int level) { /// @param out the stream to write the compressed frame to /// @param dictionary the dictionary to compress against public ZstdOutputStream(OutputStream out, ZstdDictionary dictionary) { - this(out, Zstd.defaultCompressionLevel(), dictionary); + this(out, ZstdCompressionLevel.DEFAULT, dictionary); } /// Wraps `out` and declares the exact total number of bytes that will be @@ -75,10 +75,10 @@ public ZstdOutputStream(OutputStream out, ZstdDictionary dictionary) { /// bound. Writing a different total raises an error when the stream closes. /// /// @param out the stream to write the compressed frame to - /// @param level the compression level + /// @param level the compression level to use /// @param pledgedSrcSize the exact number of uncompressed bytes that will be written /// @return a stream that will stamp the content size into the frame - public static ZstdOutputStream withPledgedSize(OutputStream out, int level, long pledgedSrcSize) { + public static ZstdOutputStream withPledgedSize(OutputStream out, ZstdCompressionLevel level, long pledgedSrcSize) { ZstdOutputStream stream = new ZstdOutputStream(out, level); stream.setPledgedSrcSize(pledgedSrcSize); return stream; @@ -91,9 +91,9 @@ private void setPledgedSrcSize(long pledgedSrcSize) { /// Wraps `out`, compressing against `dictionary` at `level`. /// /// @param out the stream to write the compressed frame to - /// @param level the compression level + /// @param level the compression level to use /// @param dictionary the dictionary to compress against, or `null` for none - public ZstdOutputStream(OutputStream out, int level, ZstdDictionary dictionary) { + public ZstdOutputStream(OutputStream out, ZstdCompressionLevel level, ZstdDictionary dictionary) { this.out = Objects.requireNonNull(out, "out"); this.arena = Arena.ofConfined(); this.in = new ZstdStreamBuffer(arena); @@ -104,7 +104,7 @@ public ZstdOutputStream(OutputStream out, int level, ZstdDictionary dictionary) c = NativeCall.createOrThrow("ZSTD_createCCtx", () -> (MemorySegment) Bindings.CREATE_CCTX.invokeExact()); this.cctx = c; NativeCall.checkReturnValue(() -> (long) Bindings.CCTX_SET_PARAMETER.invokeExact( - cctx, ZstdCompressParameter.COMPRESSION_LEVEL.value(), level)); + cctx, ZstdCompressParameter.COMPRESSION_LEVEL.value(), level.value())); if (dictionary != null) { loadDictionary(dictionary); } diff --git a/zstd/src/test/java/io/github/dfa1/zstd/RefPrefixTest.java b/zstd/src/test/java/io/github/dfa1/zstd/RefPrefixTest.java index 31c9328..67e2678 100644 --- a/zstd/src/test/java/io/github/dfa1/zstd/RefPrefixTest.java +++ b/zstd/src/test/java/io/github/dfa1/zstd/RefPrefixTest.java @@ -52,11 +52,11 @@ void prefixIsAppliedAndRequiredToDecode() { MemorySegment src = segmentOf(arena, random); long baseline; - try (ZstdCompressContext cctx = new ZstdCompressContext().level(19)) { + try (ZstdCompressContext cctx = new ZstdCompressContext().level(new ZstdCompressionLevel(19))) { baseline = cctx.compress(arena, src).byteSize(); } byte[] frame; - try (ZstdCompressContext cctx = new ZstdCompressContext().level(19)) { + try (ZstdCompressContext cctx = new ZstdCompressContext().level(new ZstdCompressionLevel(19))) { cctx.refPrefix(prefix); MemorySegment f = cctx.compress(arena, src); frame = bytesOf(f, (int) f.byteSize()); @@ -118,7 +118,7 @@ void prefixIsSingleUseAndDoesNotStickAcrossFrames() { // and one context kept open across two compressions with the prefix set once. byte[] random = randomBytes(0xCAFE, 16384); try (Arena arena = Arena.ofConfined(); - ZstdCompressContext cctx = new ZstdCompressContext().level(19)) { + ZstdCompressContext cctx = new ZstdCompressContext().level(new ZstdCompressionLevel(19))) { MemorySegment prefix = segmentOf(arena, random); MemorySegment src = segmentOf(arena, random); MemorySegment first = arena.allocate(Zstd.compressBound(random.length)); diff --git a/zstd/src/test/java/io/github/dfa1/zstd/ZstdCompressionLevelTest.java b/zstd/src/test/java/io/github/dfa1/zstd/ZstdCompressionLevelTest.java new file mode 100644 index 0000000..0db05b3 --- /dev/null +++ b/zstd/src/test/java/io/github/dfa1/zstd/ZstdCompressionLevelTest.java @@ -0,0 +1,95 @@ +package io.github.dfa1.zstd; + +import org.assertj.core.api.ThrowableAssert.ThrowingCallable; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.stream.IntStream; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class ZstdCompressionLevelTest { + + @Nested + class Construction { + + @ParameterizedTest + @MethodSource("io.github.dfa1.zstd.ZstdCompressionLevelTest#inRangeLevels") + void acceptsAnyLevelWithinTheLibraryRange(int value) { + // Given a raw level inside the linked libzstd's accepted range + // When wrapped + ZstdCompressionLevel sut = new ZstdCompressionLevel(value); + + // Then it carries that raw value + assertThat(sut.value()).isEqualTo(value); + } + } + + @Nested + class Constants { + + @Test + void defaultMatchesTheLibraryDefault() { + // Then DEFAULT wraps the library's default level + assertThat(ZstdCompressionLevel.DEFAULT.value()).isEqualTo(Zstd.defaultCompressionLevel()); + } + + @Test + void fastestMatchesTheLibraryMinimum() { + // Then FASTEST wraps the library's minimum level + assertThat(ZstdCompressionLevel.FASTEST.value()).isEqualTo(Zstd.minCompressionLevel()); + } + + @Test + void maxMatchesTheLibraryMaximum() { + // Then MAX wraps the library's maximum level + assertThat(ZstdCompressionLevel.MAX.value()).isEqualTo(Zstd.maxCompressionLevel()); + } + } + + @Nested + class Validation { + + @Test + void rejectsOneBelowTheMinimum() { + // Given a level one below the accepted minimum + int belowMin = Zstd.minCompressionLevel() - 1; + + // When wrapped + ThrowingCallable result = () -> new ZstdCompressionLevel(belowMin); + + // Then it is rejected before reaching native code + assertThatThrownBy(result) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining(String.valueOf(belowMin)); + } + + @Test + void rejectsOneAboveTheMaximum() { + // Given a level one above the accepted maximum + int aboveMax = Zstd.maxCompressionLevel() + 1; + + // When wrapped + ThrowingCallable result = () -> new ZstdCompressionLevel(aboveMax); + + // Then it is rejected before reaching native code + assertThatThrownBy(result) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining(String.valueOf(aboveMax)); + } + } + + private static Stream inRangeLevels() { + return IntStream.of( + Zstd.minCompressionLevel(), + 0, + 1, + Zstd.defaultCompressionLevel(), + Zstd.maxCompressionLevel()) + .boxed(); + } +} diff --git a/zstd/src/test/java/io/github/dfa1/zstd/ZstdDictionaryTest.java b/zstd/src/test/java/io/github/dfa1/zstd/ZstdDictionaryTest.java index e52c151..26e097c 100644 --- a/zstd/src/test/java/io/github/dfa1/zstd/ZstdDictionaryTest.java +++ b/zstd/src/test/java/io/github/dfa1/zstd/ZstdDictionaryTest.java @@ -105,7 +105,8 @@ void finalizesRawContentIntoUsableDictionary() { byte[] content = "{\"user\":\"\",\"event\":\"click\",\"id\":}".repeat(40) .getBytes(StandardCharsets.UTF_8); - ZstdDictionary dict = ZstdDictionary.finalizeFrom(content, samples, 16 * 1024, 0); + ZstdDictionary dict = + ZstdDictionary.finalizeFrom(content, samples, 16 * 1024, new ZstdCompressionLevel(0)); // Then it carries a header and round-trips a sample assertThat(dict.size()).isGreaterThan(0); @@ -128,7 +129,8 @@ void trainedDictionaryHasHeader() { @Test void finalizeFailsWithoutSamples() { // When finalizing raw content with no tuning samples - ThrowingCallable result = () -> ZstdDictionary.finalizeFrom(new byte[]{1, 2, 3}, List.of(), 4096, 0); + ThrowingCallable result = + () -> ZstdDictionary.finalizeFrom(new byte[]{1, 2, 3}, List.of(), 4096, new ZstdCompressionLevel(0)); // Then it fails fast with the empty-samples guard assertThatThrownBy(result) @@ -221,10 +223,10 @@ void roundTripsViaCDictAndDDict() { byte[] sample = samples.get(999); byte[] restored; - int level; + ZstdCompressionLevel level; try (ZstdCompressContext cctx = new ZstdCompressContext(); ZstdDecompressContext dctx = new ZstdDecompressContext(); - ZstdCompressDictionary cdict = new ZstdCompressDictionary(sut, 19); + ZstdCompressDictionary cdict = new ZstdCompressDictionary(sut, new ZstdCompressionLevel(19)); ZstdDecompressDictionary ddict = new ZstdDecompressDictionary(sut)) { // When round-tripped through the digested dictionaries @@ -235,7 +237,7 @@ void roundTripsViaCDictAndDDict() { // Then the sample is recovered at the requested level assertThat(restored).isEqualTo(sample); - assertThat(level).isEqualTo(19); + assertThat(level).isEqualTo(new ZstdCompressionLevel(19)); } @Test @@ -273,9 +275,9 @@ class Factories { @Test void compressDictFixesTheRequestedLevel() { // When a digested compress dictionary is built via the factory - try (ZstdCompressDictionary cdict = sut.compressDict(19)) { + try (ZstdCompressDictionary cdict = sut.compressDict(new ZstdCompressionLevel(19))) { // Then it carries that level and the dictionary's id - assertThat(cdict.level()).isEqualTo(19); + assertThat(cdict.level()).isEqualTo(new ZstdCompressionLevel(19)); assertThat(cdict.id()).isEqualTo(sut.id()); } } @@ -285,7 +287,7 @@ void compressDictDefaultsToTheLibraryLevel() { // When built without a level try (ZstdCompressDictionary cdict = sut.compressDict()) { // Then it uses the library default - assertThat(cdict.level()).isEqualTo(Zstd.defaultCompressionLevel()); + assertThat(cdict.level()).isEqualTo(ZstdCompressionLevel.DEFAULT); } } @@ -306,7 +308,7 @@ void factoryDictionariesRoundTrip() { byte[] restored; try (ZstdCompressContext cctx = new ZstdCompressContext(); ZstdDecompressContext dctx = new ZstdDecompressContext(); - ZstdCompressDictionary cdict = sut.compressDict(19); + ZstdCompressDictionary cdict = sut.compressDict(new ZstdCompressionLevel(19)); ZstdDecompressDictionary ddict = sut.decompressDict()) { // When round-tripped through them @@ -353,11 +355,12 @@ void roundTripsViaSegmentBuiltCDictAndDDict() { byte[] raw = sut.toByteArray(); byte[] restored; - int level; + ZstdCompressionLevel level; try (Arena arena = Arena.ofConfined(); ZstdCompressContext cctx = new ZstdCompressContext(); ZstdDecompressContext dctx = new ZstdDecompressContext(); - ZstdCompressDictionary cdict = new ZstdCompressDictionary(segmentOf(arena, raw), 19); + ZstdCompressDictionary cdict = + new ZstdCompressDictionary(segmentOf(arena, raw), new ZstdCompressionLevel(19)); ZstdDecompressDictionary ddict = new ZstdDecompressDictionary(segmentOf(arena, raw))) { // When round-tripped through the segment-built dictionaries @@ -368,7 +371,7 @@ void roundTripsViaSegmentBuiltCDictAndDDict() { // Then the sample is recovered at the requested level assertThat(restored).isEqualTo(sample); - assertThat(level).isEqualTo(19); + assertThat(level).isEqualTo(new ZstdCompressionLevel(19)); } @Test @@ -467,7 +470,7 @@ void referencedDigestedDictionarySurvivesSessionReset() { byte[] second = samples.get(2); byte[] restoredFirst; byte[] restoredSecond; - try (ZstdCompressDictionary cdict = new ZstdCompressDictionary(sut, 19); + try (ZstdCompressDictionary cdict = new ZstdCompressDictionary(sut, new ZstdCompressionLevel(19)); ZstdDecompressDictionary ddict = new ZstdDecompressDictionary(sut); ZstdCompressContext cctx = new ZstdCompressContext(); ZstdDecompressContext dctx = new ZstdDecompressContext()) { @@ -583,7 +586,7 @@ void loadAndRefReturnTheSameCompressContext() { // Given a compress context and dictionaries to load and reference try (Arena arena = Arena.ofConfined(); ZstdCompressContext cctx = new ZstdCompressContext(); - ZstdCompressDictionary cdict = new ZstdCompressDictionary(sut, 19)) { + ZstdCompressDictionary cdict = new ZstdCompressDictionary(sut, new ZstdCompressionLevel(19))) { // Then every sticky-dictionary call returns the same context, for chaining assertThat(cctx.loadDictionary(sut)).isSameAs(cctx); diff --git a/zstd/src/test/java/io/github/dfa1/zstd/ZstdMemoryTest.java b/zstd/src/test/java/io/github/dfa1/zstd/ZstdMemoryTest.java index bcedf27..dab3738 100644 --- a/zstd/src/test/java/io/github/dfa1/zstd/ZstdMemoryTest.java +++ b/zstd/src/test/java/io/github/dfa1/zstd/ZstdMemoryTest.java @@ -17,19 +17,19 @@ class Estimates { @Test void contextEstimatesArePositive() { - assertThat(Zstd.estimateCompressContextSize(3)).isPositive(); + assertThat(Zstd.estimateCompressContextSize(new ZstdCompressionLevel(3))).isPositive(); assertThat(Zstd.estimateDecompressContextSize()).isPositive(); } @Test void higherLevelEstimatesAtLeastAsLarge() { - assertThat(Zstd.estimateCompressContextSize(19)) - .isGreaterThanOrEqualTo(Zstd.estimateCompressContextSize(1)); + assertThat(Zstd.estimateCompressContextSize(new ZstdCompressionLevel(19))) + .isGreaterThanOrEqualTo(Zstd.estimateCompressContextSize(new ZstdCompressionLevel(1))); } @Test void dictionaryEstimatesArePositive() { - assertThat(Zstd.estimateCompressDictSize(64 * 1024, 3)).isPositive(); + assertThat(Zstd.estimateCompressDictSize(64 * 1024, new ZstdCompressionLevel(3))).isPositive(); assertThat(Zstd.estimateDecompressDictSize(64 * 1024)).isPositive(); } } diff --git a/zstd/src/test/java/io/github/dfa1/zstd/ZstdParameterTest.java b/zstd/src/test/java/io/github/dfa1/zstd/ZstdParameterTest.java index 6ebb4e8..4f34bad 100644 --- a/zstd/src/test/java/io/github/dfa1/zstd/ZstdParameterTest.java +++ b/zstd/src/test/java/io/github/dfa1/zstd/ZstdParameterTest.java @@ -25,8 +25,9 @@ void addsFourByteTrailerAndStillRoundTrips() { // Given the same input compressed at the same level with and without a checksum byte[] plain; byte[] checksummed; - try (ZstdCompressContext noSum = new ZstdCompressContext().level(9); - ZstdCompressContext withSum = new ZstdCompressContext().level(9).checksum(true)) { + try (ZstdCompressContext noSum = new ZstdCompressContext().level(new ZstdCompressionLevel(9)); + ZstdCompressContext withSum = + new ZstdCompressContext().level(new ZstdCompressionLevel(9)).checksum(true)) { plain = noSum.compress(PAYLOAD); checksummed = withSum.compress(PAYLOAD); } @@ -59,7 +60,8 @@ class Ratio { @Test void longDistanceMatchingRoundTrips() { byte[] frame; - try (ZstdCompressContext ctx = new ZstdCompressContext().level(3).longDistanceMatching(true)) { + try (ZstdCompressContext ctx = + new ZstdCompressContext().level(new ZstdCompressionLevel(3)).longDistanceMatching(true)) { frame = ctx.compress(PAYLOAD); } assertThat(Zstd.decompress(frame)).isEqualTo(PAYLOAD); @@ -152,12 +154,12 @@ void sessionOnlyKeepsLevelAndParameters() { // Given a context used once, then reset for the session only byte[] reused; byte[] fresh; - try (ZstdCompressContext sut = new ZstdCompressContext().level(19)) { + try (ZstdCompressContext sut = new ZstdCompressContext().level(new ZstdCompressionLevel(19))) { sut.compress(PAYLOAD); sut.reset(ZstdResetDirective.SESSION_ONLY); reused = sut.compress(PAYLOAD); } - try (ZstdCompressContext ctx = new ZstdCompressContext().level(19)) { + try (ZstdCompressContext ctx = new ZstdCompressContext().level(new ZstdCompressionLevel(19))) { fresh = ctx.compress(PAYLOAD); } @@ -171,7 +173,7 @@ void parameterResetRestoresTheDefaultLevel(ZstdResetDirective directive) { // Given a level-19 context reset with parameters cleared byte[] afterReset; byte[] atDefault; - try (ZstdCompressContext sut = new ZstdCompressContext().level(19)) { + try (ZstdCompressContext sut = new ZstdCompressContext().level(new ZstdCompressionLevel(19))) { sut.compress(PAYLOAD); sut.reset(directive); afterReset = sut.compress(PAYLOAD); @@ -190,7 +192,7 @@ void dictionaryRoundTripsAfterParameterReset() { ZstdDictionary dict = ZstdDictionary.of("dictionary sample payload ".repeat(64).getBytes(StandardCharsets.UTF_8)); byte[] frame; - try (ZstdCompressContext sut = new ZstdCompressContext().level(19)) { + try (ZstdCompressContext sut = new ZstdCompressContext().level(new ZstdCompressionLevel(19))) { sut.compress(PAYLOAD, dict); sut.reset(ZstdResetDirective.SESSION_AND_PARAMETERS); @@ -235,13 +237,13 @@ void sessionOnlyKeepsTheCachedLevelForTheLegacyDictionaryPath() { ZstdDictionary dict = ZstdDictionary.of("dictionary sample payload ".repeat(64).getBytes(StandardCharsets.UTF_8)); byte[] afterSessionReset; - try (ZstdCompressContext sut = new ZstdCompressContext().level(19)) { + try (ZstdCompressContext sut = new ZstdCompressContext().level(new ZstdCompressionLevel(19))) { sut.compress(PAYLOAD, dict); sut.reset(ZstdResetDirective.SESSION_ONLY); afterSessionReset = sut.compress(PAYLOAD, dict); } byte[] freshLevel19; - try (ZstdCompressContext ctx = new ZstdCompressContext().level(19)) { + try (ZstdCompressContext ctx = new ZstdCompressContext().level(new ZstdCompressionLevel(19))) { freshLevel19 = ctx.compress(PAYLOAD, dict); } @@ -256,7 +258,7 @@ void parameterResetClearsTheCachedLevelForTheLegacyDictionaryPath() { ZstdDictionary dict = ZstdDictionary.of("dictionary sample payload ".repeat(64).getBytes(StandardCharsets.UTF_8)); byte[] afterParameterReset; - try (ZstdCompressContext sut = new ZstdCompressContext().level(19)) { + try (ZstdCompressContext sut = new ZstdCompressContext().level(new ZstdCompressionLevel(19))) { sut.compress(PAYLOAD, dict); sut.reset(ZstdResetDirective.PARAMETERS); afterParameterReset = sut.compress(PAYLOAD, dict); @@ -292,7 +294,7 @@ void levelViaParameterMatchesLevelMethod() { byte[] viaParam; byte[] viaMethod; try (ZstdCompressContext a = new ZstdCompressContext().parameter(ZstdCompressParameter.COMPRESSION_LEVEL, 17); - ZstdCompressContext b = new ZstdCompressContext().level(17)) { + ZstdCompressContext b = new ZstdCompressContext().level(new ZstdCompressionLevel(17))) { viaParam = a.compress(PAYLOAD); viaMethod = b.compress(PAYLOAD); } @@ -322,8 +324,8 @@ void levelActuallyAppliesToTheNativeCompressionPath() { byte[] atMax; // When compressing via level() at the minimum and the maximum level - try (ZstdCompressContext low = new ZstdCompressContext().level(Zstd.minCompressionLevel()); - ZstdCompressContext high = new ZstdCompressContext().level(Zstd.maxCompressionLevel())) { + try (ZstdCompressContext low = new ZstdCompressContext().level(ZstdCompressionLevel.FASTEST); + ZstdCompressContext high = new ZstdCompressContext().level(ZstdCompressionLevel.MAX)) { atMin = low.compress(data); atMax = high.compress(data); } diff --git a/zstd/src/test/java/io/github/dfa1/zstd/ZstdSegmentStreamTest.java b/zstd/src/test/java/io/github/dfa1/zstd/ZstdSegmentStreamTest.java index 5e66eb0..22f1bbf 100644 --- a/zstd/src/test/java/io/github/dfa1/zstd/ZstdSegmentStreamTest.java +++ b/zstd/src/test/java/io/github/dfa1/zstd/ZstdSegmentStreamTest.java @@ -123,7 +123,7 @@ void roundTripsAgainstDictionary() { byte[] sample = "{\"id\":1,\"user\":\"u\",\"event\":\"x\"}".getBytes(StandardCharsets.UTF_8); try (Arena arena = Arena.ofConfined(); - ZstdCompressStream cs = new ZstdCompressStream(3, dict); + ZstdCompressStream cs = new ZstdCompressStream(new ZstdCompressionLevel(3), dict); ZstdDecompressStream ds = new ZstdDecompressStream(dict)) { MemorySegment src = segmentOf(arena, sample); diff --git a/zstd/src/test/java/io/github/dfa1/zstd/ZstdStreamTest.java b/zstd/src/test/java/io/github/dfa1/zstd/ZstdStreamTest.java index d9700b9..5f15ad6 100644 --- a/zstd/src/test/java/io/github/dfa1/zstd/ZstdStreamTest.java +++ b/zstd/src/test/java/io/github/dfa1/zstd/ZstdStreamTest.java @@ -36,7 +36,7 @@ void preservesPayloadsAcrossBufferBoundaries(int size) throws IOException { byte[] original = randomBytes(7, size); // When streamed through compress then decompress - byte[] restored = streamDecompress(streamCompress(original, 3)); + byte[] restored = streamDecompress(streamCompress(original, new ZstdCompressionLevel(3))); // Then it is recovered byte for byte assertThat(restored).isEqualTo(original); @@ -46,7 +46,7 @@ void preservesPayloadsAcrossBufferBoundaries(int size) throws IOException { void compressesIncompressibleAndCompressibleAlike() throws IOException { byte[] text = "structured log line ".repeat(100_000).getBytes(StandardCharsets.UTF_8); - byte[] frame = streamCompress(text, 9); + byte[] frame = streamCompress(text, new ZstdCompressionLevel(9)); assertThat(frame).hasSizeLessThan(text.length); assertThat(streamDecompress(frame)).isEqualTo(text); @@ -75,7 +75,7 @@ class Interop { void streamOutputDecodesWithOneShot() throws IOException { // Given a frame produced by the streaming compressor byte[] original = "interop payload ".repeat(1000).getBytes(StandardCharsets.UTF_8); - byte[] frame = streamCompress(original, 6); + byte[] frame = streamCompress(original, new ZstdCompressionLevel(6)); // Then the one-shot decompressor reads it (frame stores no size -> give a bound) assertThat(Zstd.decompress(frame, original.length)).isEqualTo(original); @@ -126,7 +126,7 @@ void dictionaryShrinksStreamedRecord() throws IOException { } // a dictionary frame of a tiny sample is smaller than a plain stream frame - assertThat(withDict.size()).isLessThan(streamCompress(sample, Zstd.defaultCompressionLevel()).length); + assertThat(withDict.size()).isLessThan(streamCompress(sample, ZstdCompressionLevel.DEFAULT).length); } @Test @@ -156,7 +156,7 @@ void throwsWhenFinalFrameIsCutShort(int bytesDropped) throws IOException { // Given a valid frame with its tail bytes lopped off (random data so the // frame stays large enough to drop bytes from) byte[] original = randomBytes(7, 50_000); - byte[] frame = streamCompress(original, 6); + byte[] frame = streamCompress(original, new ZstdCompressionLevel(6)); byte[] cut = java.util.Arrays.copyOf(frame, frame.length - bytesDropped); try (ZstdInputStream sut = new ZstdInputStream(new ByteArrayInputStream(cut))) { @@ -188,7 +188,7 @@ void recordsContentSizeInTheFrame() throws IOException { // Given a stream told the exact total up front byte[] original = "pledged payload ".repeat(300).getBytes(StandardCharsets.UTF_8); ByteArrayOutputStream sink = new ByteArrayOutputStream(); - try (ZstdOutputStream zout = ZstdOutputStream.withPledgedSize(sink, 6, original.length)) { + try (ZstdOutputStream zout = ZstdOutputStream.withPledgedSize(sink, new ZstdCompressionLevel(6), original.length)) { zout.write(original); } byte[] frame = sink.toByteArray(); @@ -202,7 +202,7 @@ void recordsContentSizeInTheFrame() throws IOException { void plainStreamFrameCannotBeSizedForZeroCopyDecode() throws IOException { // Given a streamed frame with no pledged size byte[] original = "no pledge ".repeat(500).getBytes(StandardCharsets.UTF_8); - byte[] frame = streamCompress(original, 6); + byte[] frame = streamCompress(original, new ZstdCompressionLevel(6)); // When the zero-copy decoder asks the frame how big the output is try (Arena arena = Arena.ofConfined()) { @@ -221,7 +221,7 @@ void pledgedFrameDecodesZeroCopyIntoArenaInOneShot() throws IOException { // Given a streamed frame that pledged its total up front byte[] original = "pledge enables zero-copy ".repeat(500).getBytes(StandardCharsets.UTF_8); ByteArrayOutputStream sink = new ByteArrayOutputStream(); - try (ZstdOutputStream zout = ZstdOutputStream.withPledgedSize(sink, 6, original.length)) { + try (ZstdOutputStream zout = ZstdOutputStream.withPledgedSize(sink, new ZstdCompressionLevel(6), original.length)) { zout.write(original); } byte[] frame = sink.toByteArray(); @@ -254,7 +254,7 @@ void flushPushesBufferedBytesThroughToTheSink() throws IOException { // Given a payload written but not yet closed byte[] original = "flushed payload ".repeat(2000).getBytes(StandardCharsets.UTF_8); CountingOutputStream sink = new CountingOutputStream(); - try (ZstdOutputStream zout = new ZstdOutputStream(sink, 3)) { + try (ZstdOutputStream zout = new ZstdOutputStream(sink, new ZstdCompressionLevel(3))) { zout.write(original); // When flushed mid-stream @@ -278,7 +278,7 @@ void closeFlushesAndClosesTheUnderlyingStreamExactlyOnce() throws IOException { // Given a payload written to a stream that tracks flush/close on its sink byte[] original = "epilogue payload ".repeat(1000).getBytes(StandardCharsets.UTF_8); CountingOutputStream sink = new CountingOutputStream(); - ZstdOutputStream zout = new ZstdOutputStream(sink, 3); + ZstdOutputStream zout = new ZstdOutputStream(sink, new ZstdCompressionLevel(3)); zout.write(original); // When closed twice @@ -316,7 +316,7 @@ class InputStreamLifecycle { void singleByteReadReturnsTheUnsignedValue() throws IOException { // Given a frame whose first decoded byte has its high bit set byte[] original = {(byte) 0xFF, (byte) 0x80, 0x01}; - byte[] frame = streamCompress(original, 3); + byte[] frame = streamCompress(original, new ZstdCompressionLevel(3)); // When read one byte at a time try (ZstdInputStream zin = new ZstdInputStream(new ByteArrayInputStream(frame))) { @@ -331,7 +331,7 @@ void singleByteReadReturnsTheUnsignedValue() throws IOException { @Test void readPastEndOfStreamStaysMinusOne() throws IOException { // Given a compressed frame - byte[] frame = streamCompress("done".getBytes(StandardCharsets.UTF_8), 3); + byte[] frame = streamCompress("done".getBytes(StandardCharsets.UTF_8), new ZstdCompressionLevel(3)); try (ZstdInputStream zin = new ZstdInputStream(new ByteArrayInputStream(frame))) { // When the stream is drained to the end, then read again past EOF byte[] all = zin.readAllBytes(); @@ -348,7 +348,7 @@ void readPastEndOfStreamStaysMinusOne() throws IOException { @Test void closeClosesTheUnderlyingStreamExactlyOnce() throws IOException { // Given an input stream over a source that tracks close() - byte[] frame = streamCompress("payload".getBytes(StandardCharsets.UTF_8), 3); + byte[] frame = streamCompress("payload".getBytes(StandardCharsets.UTF_8), new ZstdCompressionLevel(3)); CountingInputStream source = new CountingInputStream(frame); ZstdInputStream zin = new ZstdInputStream(source); @@ -366,7 +366,7 @@ void firstSingleByteIsCorrectEvenWhenInputDribblesInOneByteAtATime() throws IOEx // byte per read so the first decode calls produce nothing until the header // is complete — the decoder must keep refilling before returning a byte byte[] original = {(byte) 0xFF, 0x10, 0x20}; - byte[] frame = streamCompress(original, 3); + byte[] frame = streamCompress(original, new ZstdCompressionLevel(3)); // When the very first byte is read try (ZstdInputStream zin = new ZstdInputStream(new DribbleInputStream(frame))) { @@ -381,7 +381,7 @@ void readsCorrectlyWhenInputArrivesOneByteAtATime(int size) throws IOException { // Given a frame fed through a source that yields a single byte per read, // forcing the decoder across many refill/no-progress iterations byte[] original = randomBytes(7, size); - byte[] frame = streamCompress(original, 3); + byte[] frame = streamCompress(original, new ZstdCompressionLevel(3)); // When drained byte[] restored; @@ -407,7 +407,7 @@ private static Stream closedStreamOperations() { return stream::flush; }), Arguments.of("read after close", (ClosedStreamOperation) () -> { - byte[] frame = streamCompress("payload".getBytes(StandardCharsets.UTF_8), 3); + byte[] frame = streamCompress("payload".getBytes(StandardCharsets.UTF_8), new ZstdCompressionLevel(3)); ZstdInputStream stream = new ZstdInputStream(new ByteArrayInputStream(frame)); stream.close(); return stream::read; @@ -482,7 +482,7 @@ public int read(byte[] b, int off, int len) { } } - private static byte[] streamCompress(byte[] data, int level) throws IOException { + private static byte[] streamCompress(byte[] data, ZstdCompressionLevel level) throws IOException { ByteArrayOutputStream sink = new ByteArrayOutputStream(); try (ZstdOutputStream zout = new ZstdOutputStream(sink, level)) { zout.write(data); diff --git a/zstd/src/test/java/io/github/dfa1/zstd/ZstdTest.java b/zstd/src/test/java/io/github/dfa1/zstd/ZstdTest.java index 2c618d8..b856941 100644 --- a/zstd/src/test/java/io/github/dfa1/zstd/ZstdTest.java +++ b/zstd/src/test/java/io/github/dfa1/zstd/ZstdTest.java @@ -62,7 +62,7 @@ class Levels { @ParameterizedTest @MethodSource("io.github.dfa1.zstd.ZstdTestSupport#levels") - void roundTripAtEveryLevel(int level) { + void roundTripAtEveryLevel(ZstdCompressionLevel level) { // Given a payload compressed at the given level byte[] original = "payload-data-".repeat(500).getBytes(StandardCharsets.UTF_8); byte[] frame = Zstd.compress(original, level); @@ -71,7 +71,7 @@ void roundTripAtEveryLevel(int level) { byte[] restored = Zstd.decompress(frame); // Then the original is recovered - assertThat(restored).as("level %d", level).isEqualTo(original); + assertThat(restored).as("level %s", level).isEqualTo(original); } @Test diff --git a/zstd/src/test/java/io/github/dfa1/zstd/ZstdTestSupport.java b/zstd/src/test/java/io/github/dfa1/zstd/ZstdTestSupport.java index bf2dc55..d43fcf8 100644 --- a/zstd/src/test/java/io/github/dfa1/zstd/ZstdTestSupport.java +++ b/zstd/src/test/java/io/github/dfa1/zstd/ZstdTestSupport.java @@ -8,7 +8,6 @@ import java.util.ArrayList; import java.util.List; import java.util.Random; -import java.util.stream.IntStream; import java.util.stream.Stream; /// Shared low-level helpers for the segment-based tests: copy a `byte[]` into a @@ -89,9 +88,12 @@ static Stream bytes() { } /// The compression levels worth exercising: both extremes, default, and a low one. - static IntStream levels() { - return IntStream.of( - Zstd.minCompressionLevel(), 1, Zstd.defaultCompressionLevel(), Zstd.maxCompressionLevel()); + static Stream levels() { + return Stream.of( + ZstdCompressionLevel.FASTEST, + new ZstdCompressionLevel(1), + ZstdCompressionLevel.DEFAULT, + ZstdCompressionLevel.MAX); } private static byte[] random(Random r, int size) {