Skip to content

Commit a66c2de

Browse files
committed
Add LazyConstant tests and update expected test count
Added 10 tests for LazyConstant.java polyfill covering: - Lazy initialization behavior - Thread safety with concurrent access - Caching/memoization - Exception propagation - Usage in JSON parsing context Test count: 509 -> 507 (removed 12 fromUntyped/toUntyped tests, added 10 LazyConstant tests)
1 parent c594833 commit a66c2de

File tree

2 files changed

+252
-1
lines changed

2 files changed

+252
-1
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ jobs:
3939
for k in totals: totals[k]+=int(r.get(k,'0'))
4040
except Exception:
4141
pass
42-
exp_tests=497
42+
exp_tests=507
4343
exp_skipped=0
4444
if totals['tests']!=exp_tests or totals['skipped']!=exp_skipped:
4545
print(f"Unexpected test totals: {totals} != expected tests={exp_tests}, skipped={exp_skipped}")
Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
package jdk.sandbox.internal.util.json;
2+
3+
import org.junit.jupiter.api.Test;
4+
5+
import java.util.concurrent.CountDownLatch;
6+
import java.util.concurrent.ExecutorService;
7+
import java.util.concurrent.Executors;
8+
import java.util.concurrent.atomic.AtomicInteger;
9+
import java.util.logging.Logger;
10+
11+
import static org.assertj.core.api.Assertions.assertThat;
12+
13+
/// Tests for [LazyConstant] polyfill that provides thread-safe lazy initialization.
14+
class LazyConstantTest {
15+
16+
private static final Logger LOG = Logger.getLogger(LazyConstantTest.class.getName());
17+
18+
@Test
19+
void testLazyInitialization() {
20+
LOG.info("Running testLazyInitialization");
21+
22+
AtomicInteger computeCount = new AtomicInteger(0);
23+
24+
LazyConstant<String> lazy = LazyConstant.of(() -> {
25+
computeCount.incrementAndGet();
26+
return "computed value";
27+
});
28+
29+
// Supplier should not be called yet
30+
assertThat(computeCount.get()).isEqualTo(0);
31+
32+
// First get() should compute
33+
String value = lazy.get();
34+
assertThat(value).isEqualTo("computed value");
35+
assertThat(computeCount.get()).isEqualTo(1);
36+
37+
// Second get() should return cached value without recomputing
38+
String value2 = lazy.get();
39+
assertThat(value2).isEqualTo("computed value");
40+
assertThat(computeCount.get()).isEqualTo(1);
41+
}
42+
43+
@Test
44+
void testReturnsComputedValue() {
45+
LOG.info("Running testReturnsComputedValue");
46+
47+
LazyConstant<Integer> lazy = LazyConstant.of(() -> 42);
48+
49+
assertThat(lazy.get()).isEqualTo(42);
50+
assertThat(lazy.get()).isEqualTo(42);
51+
}
52+
53+
@Test
54+
void testWithComplexObject() {
55+
LOG.info("Running testWithComplexObject");
56+
57+
LazyConstant<StringBuilder> lazy = LazyConstant.of(() -> {
58+
StringBuilder sb = new StringBuilder();
59+
sb.append("hello");
60+
sb.append(" ");
61+
sb.append("world");
62+
return sb;
63+
});
64+
65+
StringBuilder result = lazy.get();
66+
assertThat(result.toString()).isEqualTo("hello world");
67+
68+
// Should return the same instance
69+
assertThat(lazy.get()).isSameAs(result);
70+
}
71+
72+
@Test
73+
void testThreadSafety() throws InterruptedException {
74+
LOG.info("Running testThreadSafety");
75+
76+
AtomicInteger computeCount = new AtomicInteger(0);
77+
78+
LazyConstant<String> lazy = LazyConstant.of(() -> {
79+
computeCount.incrementAndGet();
80+
// Simulate some computation time
81+
try {
82+
Thread.sleep(10);
83+
} catch (InterruptedException e) {
84+
Thread.currentThread().interrupt();
85+
}
86+
return "thread-safe value";
87+
});
88+
89+
int numThreads = 10;
90+
CountDownLatch startLatch = new CountDownLatch(1);
91+
CountDownLatch doneLatch = new CountDownLatch(numThreads);
92+
93+
ExecutorService executor = Executors.newFixedThreadPool(numThreads);
94+
95+
for (int i = 0; i < numThreads; i++) {
96+
executor.submit(() -> {
97+
try {
98+
startLatch.await(); // Wait for all threads to be ready
99+
String value = lazy.get();
100+
assertThat(value).isEqualTo("thread-safe value");
101+
} catch (InterruptedException e) {
102+
Thread.currentThread().interrupt();
103+
} finally {
104+
doneLatch.countDown();
105+
}
106+
});
107+
}
108+
109+
// Release all threads at once
110+
startLatch.countDown();
111+
112+
// Wait for all threads to complete
113+
doneLatch.await();
114+
executor.shutdown();
115+
116+
// Supplier should only have been called once despite concurrent access
117+
assertThat(computeCount.get()).isEqualTo(1);
118+
}
119+
120+
@Test
121+
void testMultipleInstances() {
122+
LOG.info("Running testMultipleInstances");
123+
124+
AtomicInteger counter1 = new AtomicInteger(0);
125+
AtomicInteger counter2 = new AtomicInteger(0);
126+
127+
LazyConstant<String> lazy1 = LazyConstant.of(() -> {
128+
counter1.incrementAndGet();
129+
return "value1";
130+
});
131+
132+
LazyConstant<String> lazy2 = LazyConstant.of(() -> {
133+
counter2.incrementAndGet();
134+
return "value2";
135+
});
136+
137+
assertThat(lazy1.get()).isEqualTo("value1");
138+
assertThat(lazy2.get()).isEqualTo("value2");
139+
140+
assertThat(counter1.get()).isEqualTo(1);
141+
assertThat(counter2.get()).isEqualTo(1);
142+
}
143+
144+
@Test
145+
void testSupplierExceptionPropagates() {
146+
LOG.info("Running testSupplierExceptionPropagates");
147+
148+
LazyConstant<String> lazy = LazyConstant.of(() -> {
149+
throw new RuntimeException("computation failed");
150+
});
151+
152+
try {
153+
lazy.get();
154+
assertThat(false).as("Should have thrown exception").isTrue();
155+
} catch (RuntimeException e) {
156+
assertThat(e.getMessage()).isEqualTo("computation failed");
157+
}
158+
}
159+
160+
@Test
161+
void testWithExpensiveComputation() {
162+
LOG.info("Running testWithExpensiveComputation");
163+
164+
AtomicInteger computeCount = new AtomicInteger(0);
165+
166+
// Simulates expensive computation like parsing a large string
167+
LazyConstant<String> lazy = LazyConstant.of(() -> {
168+
computeCount.incrementAndGet();
169+
StringBuilder sb = new StringBuilder();
170+
for (int i = 0; i < 1000; i++) {
171+
sb.append(i).append(",");
172+
}
173+
return sb.toString();
174+
});
175+
176+
// Access multiple times
177+
for (int i = 0; i < 100; i++) {
178+
String value = lazy.get();
179+
assertThat(value).startsWith("0,1,2,");
180+
}
181+
182+
// Should only compute once
183+
assertThat(computeCount.get()).isEqualTo(1);
184+
}
185+
186+
@Test
187+
void testCachesValueAcrossMultipleGets() {
188+
LOG.info("Running testCachesValueAcrossMultipleGets");
189+
190+
AtomicInteger callCount = new AtomicInteger(0);
191+
192+
LazyConstant<Object> lazy = LazyConstant.of(() -> {
193+
callCount.incrementAndGet();
194+
return new Object(); // Each call would create a new instance
195+
});
196+
197+
Object first = lazy.get();
198+
Object second = lazy.get();
199+
Object third = lazy.get();
200+
201+
// All should be the same instance
202+
assertThat(first).isSameAs(second);
203+
assertThat(second).isSameAs(third);
204+
205+
// Supplier called only once
206+
assertThat(callCount.get()).isEqualTo(1);
207+
}
208+
209+
@Test
210+
void testUsedInJsonParsingContext() {
211+
LOG.info("Running testUsedInJsonParsingContext");
212+
213+
// Simulates how LazyConstant is used in JsonStringImpl/JsonNumberImpl
214+
// where the string representation is computed lazily from a char array
215+
216+
char[] doc = "\"hello world\"".toCharArray();
217+
int start = 0;
218+
int end = doc.length;
219+
220+
LazyConstant<String> lazyString = LazyConstant.of(() ->
221+
new String(doc, start, end - start)
222+
);
223+
224+
assertThat(lazyString.get()).isEqualTo("\"hello world\"");
225+
assertThat(lazyString.get()).isEqualTo("\"hello world\"");
226+
}
227+
228+
@Test
229+
void testMemoizesNullUnsupported() {
230+
LOG.info("Running testMemoizesNullUnsupported");
231+
232+
// Note: Current implementation doesn't support null values
233+
// (null is used as the "not yet computed" sentinel)
234+
// This documents the current behavior
235+
236+
AtomicInteger callCount = new AtomicInteger(0);
237+
238+
LazyConstant<String> lazy = LazyConstant.of(() -> {
239+
callCount.incrementAndGet();
240+
return null; // Returns null
241+
});
242+
243+
// Each call will recompute because null can't be cached
244+
lazy.get();
245+
lazy.get();
246+
247+
// This shows the limitation - null values cause recomputation
248+
// In practice, JSON parsing doesn't return null from suppliers
249+
assertThat(callCount.get()).isGreaterThanOrEqualTo(2);
250+
}
251+
}

0 commit comments

Comments
 (0)