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
35 changes: 35 additions & 0 deletions zstd/src/test/java/io/github/dfa1/zstd/NativeLibraryTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;

import java.lang.foreign.FunctionDescriptor;
import java.lang.invoke.MethodHandle;

import static java.lang.foreign.ValueLayout.JAVA_INT;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

Expand Down Expand Up @@ -48,6 +52,37 @@ void rejectsUnsupportedArchitecture() {
}
}

@Nested
class Lookup {

@Test
void bindsAnExistingSymbolToAnInvokableHandle() throws Throwable {
// Given the descriptor for unsigned ZSTD_versionNumber(void)
FunctionDescriptor fd = FunctionDescriptor.of(JAVA_INT);

// When the symbol is looked up
MethodHandle handle = NativeLibrary.lookup("ZSTD_versionNumber", fd);

// Then a real, callable handle comes back (not null) and reports a version
assertThat(handle).isNotNull();
assertThat((int) handle.invokeExact()).isPositive();
}

@Test
void rejectsAMissingSymbol() {
// Given a symbol the library does not export
FunctionDescriptor fd = FunctionDescriptor.of(JAVA_INT);

// When it is looked up
ThrowingCallable result = () -> NativeLibrary.lookup("ZSTD_no_such_symbol_xyz", fd);

// Then it fails fast naming the missing symbol, rather than returning null
assertThatThrownBy(result)
.isInstanceOf(UnsatisfiedLinkError.class)
.hasMessageContaining("Symbol not found");
}
}

@Nested
class LibraryExtension {

Expand Down
80 changes: 80 additions & 0 deletions zstd/src/test/java/io/github/dfa1/zstd/RefPrefixTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,86 @@ void decompressRefPrefixRejectsAHeapSegment() {
.hasMessageContaining("prefix");
}

@Test
void compressLoadDictionaryRejectsAHeapSegment() {
// Given a heap-backed segment
MemorySegment heap = MemorySegment.ofArray("dictionary".getBytes());

// When loaded as a zero-copy dictionary
ThrowingCallable result = () -> {
try (ZstdCompressCtx cctx = new ZstdCompressCtx()) {
cctx.loadDictionary(heap);
}
};

// Then it is rejected naming the offending argument
assertThatThrownBy(result)
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("dict");
}

@Test
void decompressLoadDictionaryRejectsAHeapSegment() {
// Given a heap-backed segment
MemorySegment heap = MemorySegment.ofArray("dictionary".getBytes());

// When loaded as a zero-copy dictionary
ThrowingCallable result = () -> {
try (ZstdDecompressCtx dctx = new ZstdDecompressCtx()) {
dctx.loadDictionary(heap);
}
};

// Then it is rejected naming the offending argument
assertThatThrownBy(result)
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("dict");
}

@Test
void refPrefixReturnsTheSameContextForChaining() {
// Given both contexts and a native prefix
try (Arena arena = Arena.ofConfined();
ZstdCompressCtx cctx = new ZstdCompressCtx();
ZstdDecompressCtx dctx = new ZstdDecompressCtx()) {
MemorySegment prefix = copy(arena, "a prior version".getBytes());

// When setting and then clearing a prefix on both contexts
ZstdCompressCtx cSet = cctx.refPrefix(prefix);
ZstdCompressCtx cCleared = cctx.refPrefix((MemorySegment) null);
ZstdDecompressCtx dSet = dctx.refPrefix(prefix);
ZstdDecompressCtx dCleared = dctx.refPrefix((MemorySegment) null);

// Then every call returns the same instance, for chaining
assertThat(cSet).isSameAs(cctx);
assertThat(cCleared).isSameAs(cctx);
assertThat(dSet).isSameAs(dctx);
assertThat(dCleared).isSameAs(dctx);
}
}

@Test
void loadDictionaryFromSegmentReturnsTheSameContextForChaining() {
// Given both contexts and a native dictionary segment
try (Arena arena = Arena.ofConfined();
ZstdCompressCtx cctx = new ZstdCompressCtx();
ZstdDecompressCtx dctx = new ZstdDecompressCtx()) {
MemorySegment dict = copy(arena, "dictionary sample payload ".repeat(64).getBytes());

// When loading and then clearing a segment dictionary on both contexts
ZstdCompressCtx cSet = cctx.loadDictionary(dict);
ZstdCompressCtx cCleared = cctx.loadDictionary(MemorySegment.NULL);
ZstdDecompressCtx dSet = dctx.loadDictionary(dict);
ZstdDecompressCtx dCleared = dctx.loadDictionary(MemorySegment.NULL);

// Then every call returns the same instance, for chaining
assertThat(cSet).isSameAs(cctx);
assertThat(cCleared).isSameAs(cctx);
assertThat(dSet).isSameAs(dctx);
assertThat(dCleared).isSameAs(dctx);
}
}

private static MemorySegment copy(Arena arena, byte[] src) {
MemorySegment seg = arena.allocate(src.length);
MemorySegment.copy(src, 0, seg, JAVA_BYTE, 0, src.length);
Expand Down
12 changes: 12 additions & 0 deletions zstd/src/test/java/io/github/dfa1/zstd/ZstdErrorTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,18 @@ void reportsDstSizeTooSmall() {
assertThat(ex.code()).isEqualTo(ZstdErrorCode.DST_SIZE_TOO_SMALL);
}

@Test
void carriesTheNativeErrorNameAsTheMessage() {
// Given a frame decompressed into too small a buffer
byte[] frame = Zstd.compress(PAYLOAD);

// When it fails
ZstdException ex = catchThrowableOfType(ZstdException.class, () -> Zstd.decompress(frame, 1));

// Then the message is the descriptive native error name, not an empty string
assertThat(ex).hasMessageContaining("too small");
}

@Test
void reportsParameterOutOfBound() {
try (ZstdCompressCtx ctx = new ZstdCompressCtx()) {
Expand Down
30 changes: 30 additions & 0 deletions zstd/src/test/java/io/github/dfa1/zstd/ZstdParameterTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import org.junit.jupiter.params.provider.EnumSource;

import java.nio.charset.StandardCharsets;
import java.util.Random;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
Expand Down Expand Up @@ -311,5 +312,34 @@ void rejectsOutOfRangeValue() {
assertThatThrownBy(result).isInstanceOf(ZstdException.class);
}
}

@Test
void levelActuallyAppliesToTheNativeCompressionPath() {
// Given a level-sensitive payload (enough entropy that the level changes
// the ratio, so a no-op level() would betray itself)
byte[] data = levelSensitivePayload();
byte[] atMin;
byte[] atMax;

// When compressing via level() at the minimum and the maximum level
try (ZstdCompressCtx low = new ZstdCompressCtx().level(Zstd.minCompressionLevel());
ZstdCompressCtx high = new ZstdCompressCtx().level(Zstd.maxCompressionLevel())) {
atMin = low.compress(data);
atMax = high.compress(data);
}

// Then the higher level produces a strictly smaller frame — proving level()
// sets the native parameter rather than silently leaving the default
assertThat(atMax.length).isLessThan(atMin.length);
}

private static byte[] levelSensitivePayload() {
byte[] b = new byte[64 * 1024];
Random r = new Random(0xA11CE);
for (int i = 0; i < b.length; i++) {
b[i] = (byte) ((i % 17 == 0) ? r.nextInt(256) : 'a' + (i % 8));
}
return b;
}
}
}
25 changes: 25 additions & 0 deletions zstd/src/test/java/io/github/dfa1/zstd/ZstdSegmentTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,31 @@ void roundTripsWithDigestedDictionary() {
assertThat(bytesOf(out, written)).isEqualTo(record);
}
}

@Test
void arenaAllocatingDecompressSizesOutputFromTheDigestedDictionaryFrame() {
// Given a digested-dictionary frame that stores its decompressed size
ZstdDictionary dict = trainSmallDictionary();
byte[] record = "{\"id\":99,\"user\":\"u\",\"active\":false}".getBytes(StandardCharsets.UTF_8);

try (Arena arena = Arena.ofConfined();
ZstdCompressCtx cctx = new ZstdCompressCtx();
ZstdDecompressCtx dctx = new ZstdDecompressCtx();
ZstdCompressDict cdict = new ZstdCompressDict(dict);
ZstdDecompressDict ddict = new ZstdDecompressDict(dict)) {

MemorySegment src = segmentOf(arena, record);
MemorySegment frame = cctx.compress(arena, src, cdict);

// When decoded through the arena-allocating ddict overload
MemorySegment out = dctx.decompress(arena, frame, ddict);

// Then it allocates the exact size and returns the record (a non-null segment)
assertThat(out).isNotNull();
assertThat(out.byteSize()).isEqualTo(record.length);
assertThat(bytesOf(out, out.byteSize())).isEqualTo(record);
}
}
}

@Nested
Expand Down
Loading
Loading