Skip to content

Commit 751ead5

Browse files
dfa1claude
andauthored
test: cover NativeObject lifecycle via a test subclass (#31)
NativeObject is abstract, so a minimal concrete TestObject exercises every branch: ptr() while open, close() runs tryClose exactly once with the owned pointer, repeated close is idempotent, ptr() after close throws, a throwing tryClose is swallowed (destructors must not throw) yet still marks the object closed, constructing with NULL never calls tryClose, and 32 concurrent close() calls release exactly once. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 25737ae commit 751ead5

1 file changed

Lines changed: 171 additions & 0 deletions

File tree

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
package io.github.dfa1.zstd;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
import static org.assertj.core.api.Assertions.assertThatCode;
5+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
6+
7+
import java.lang.foreign.MemorySegment;
8+
import java.util.ArrayList;
9+
import java.util.List;
10+
import java.util.concurrent.CountDownLatch;
11+
import java.util.concurrent.ExecutorService;
12+
import java.util.concurrent.Executors;
13+
import java.util.concurrent.Future;
14+
import java.util.concurrent.atomic.AtomicInteger;
15+
import org.assertj.core.api.ThrowableAssert.ThrowingCallable;
16+
import org.junit.jupiter.api.Test;
17+
18+
class NativeObjectTest {
19+
20+
// A non-NULL stand-in pointer; never dereferenced, only compared by identity.
21+
private static final MemorySegment POINTER = MemorySegment.ofAddress(0x1234);
22+
23+
@Test
24+
void ptrReturnsTheOwnedPointerWhileOpen() {
25+
// Given an open native object
26+
TestObject sut = new TestObject(POINTER);
27+
28+
// When its pointer is read
29+
MemorySegment p = sut.pointer();
30+
31+
// Then it is the pointer it was constructed with
32+
assertThat(p).isEqualTo(POINTER);
33+
}
34+
35+
@Test
36+
void closeCallsTryCloseExactlyOnceWithThePointer() {
37+
// Given an open native object
38+
TestObject sut = new TestObject(POINTER);
39+
40+
// When closed
41+
sut.close();
42+
43+
// Then tryClose ran once, with the owned pointer
44+
assertThat(sut.closeCount.get()).isEqualTo(1);
45+
assertThat(sut.closedWith).isEqualTo(POINTER);
46+
}
47+
48+
@Test
49+
void closeIsIdempotent() {
50+
// Given an open native object
51+
TestObject sut = new TestObject(POINTER);
52+
53+
// When closed repeatedly
54+
sut.close();
55+
sut.close();
56+
sut.close();
57+
58+
// Then the resource is released only once
59+
assertThat(sut.closeCount.get()).isEqualTo(1);
60+
}
61+
62+
@Test
63+
void ptrFailsAfterClose() {
64+
// Given a closed native object
65+
TestObject sut = new TestObject(POINTER);
66+
sut.close();
67+
68+
// When its pointer is read
69+
ThrowingCallable result = sut::pointer;
70+
71+
// Then it fails fast
72+
assertThatThrownBy(result)
73+
.isInstanceOf(IllegalStateException.class)
74+
.hasMessageContaining("closed");
75+
}
76+
77+
@Test
78+
void closeSwallowsTryCloseFailures() {
79+
// Given an object whose native free throws
80+
TestObject sut = new TestObject(POINTER);
81+
sut.failOnClose = true;
82+
83+
// When closed
84+
ThrowingCallable result = sut::close;
85+
86+
// Then the exception does not escape (destructors must not throw)
87+
assertThatCode(result).doesNotThrowAnyException();
88+
// And it is still considered closed
89+
assertThat(sut.closeCount.get()).isEqualTo(1);
90+
assertThatThrownBy(sut::pointer).isInstanceOf(IllegalStateException.class);
91+
}
92+
93+
@Test
94+
void constructingWithNullNeverCallsTryClose() {
95+
// Given an object that owns no pointer
96+
TestObject sut = new TestObject(MemorySegment.NULL);
97+
98+
// When closed
99+
sut.close();
100+
101+
// Then there is nothing to release, and the pointer is unavailable
102+
assertThat(sut.closeCount.get()).isZero();
103+
assertThatThrownBy(sut::pointer).isInstanceOf(IllegalStateException.class);
104+
}
105+
106+
@Test
107+
void concurrentCloseReleasesExactlyOnce() throws Exception {
108+
// Given an open object and many threads poised to close it at once
109+
TestObject sut = new TestObject(POINTER);
110+
int threads = 32;
111+
ExecutorService pool = Executors.newFixedThreadPool(threads);
112+
CountDownLatch ready = new CountDownLatch(threads);
113+
CountDownLatch go = new CountDownLatch(1);
114+
try {
115+
List<Future<?>> futures = new ArrayList<>();
116+
for (int i = 0; i < threads; i++) {
117+
futures.add(pool.submit(() -> {
118+
ready.countDown();
119+
awaitUninterruptibly(go);
120+
sut.close();
121+
}));
122+
}
123+
ready.await();
124+
125+
// When they all fire
126+
go.countDown();
127+
for (Future<?> f : futures) {
128+
f.get();
129+
}
130+
131+
// Then the resource is released exactly once
132+
assertThat(sut.closeCount.get()).isEqualTo(1);
133+
} finally {
134+
pool.shutdownNow();
135+
}
136+
}
137+
138+
private static void awaitUninterruptibly(CountDownLatch latch) {
139+
try {
140+
latch.await();
141+
} catch (InterruptedException e) {
142+
Thread.currentThread().interrupt();
143+
throw new IllegalStateException(e);
144+
}
145+
}
146+
147+
/// Minimal concrete [NativeObject] that records how its native resource was released.
148+
private static final class TestObject extends NativeObject {
149+
150+
private final AtomicInteger closeCount = new AtomicInteger();
151+
private volatile MemorySegment closedWith;
152+
private volatile boolean failOnClose;
153+
154+
private TestObject(MemorySegment owningPointer) {
155+
super(owningPointer);
156+
}
157+
158+
private MemorySegment pointer() {
159+
return ptr();
160+
}
161+
162+
@Override
163+
protected void tryClose(MemorySegment ptr) {
164+
closeCount.incrementAndGet();
165+
closedWith = ptr;
166+
if (failOnClose) {
167+
throw new RuntimeException("native free failed");
168+
}
169+
}
170+
}
171+
}

0 commit comments

Comments
 (0)