Skip to content

Commit f94bad6

Browse files
committed
Add unit tests for CompilerUtils and Java file management
1 parent 567b980 commit f94bad6

5 files changed

Lines changed: 1013 additions & 13 deletions

File tree

README.adoc

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
Chronicle Software
33
:css-signature: demo
44
:toc: macro
5+
:sectnums:
56
:source-highlighter: rouge
67

78
image:https://maven-badges.herokuapp.com/maven-central/net.openhft/compiler/badge.svg[]
@@ -13,7 +14,7 @@ toc::[]
1314

1415
This library lets you feed _plain Java source as a_ `String`, compile it in-memory and immediately load the resulting `Class<?>` - perfect for hot-swapping logic while the JVM is still running.
1516

16-
== 1 Quick-Start
17+
== Quick-Start
1718

1819
[source,xml,subs=+quotes]
1920
----
@@ -50,9 +51,9 @@ Class<?> clazz = CompilerUtils.CACHED_COMPILER.loadFromJava(className, src);
5051
((Runnable) clazz.getDeclaredConstructor().newInstance()).run();
5152
----
5253

53-
== 2 Installation
54+
== Installation
5455

55-
* Requires a **full JDK** (8, 11, 17 or 21 LTS), _not_ a slim JRE.
56+
* Requires a *full JDK* (8, 11, 17 or 21 LTS), _not_ a slim JRE.
5657
* On Java 11 + supply these flags (copy-paste safe):
5758

5859
[source,bash]
@@ -65,7 +66,7 @@ Class<?> clazz = CompilerUtils.CACHED_COMPILER.loadFromJava(className, src);
6566
** unpack Chronicle jars:
6667
`bootJar { requiresUnpack("**/chronicle-*.jar") }`
6768

68-
== 3 Feature Highlights
69+
== Feature Highlights
6970

7071
|===
7172
| Feature | Benefit
@@ -86,39 +87,43 @@ Class<?> clazz = CompilerUtils.CACHED_COMPILER.loadFromJava(className, src);
8687
| Build helper hierarchy in a single call
8788
|===
8889

89-
== 4 Advanced Usage & Patterns
90+
== Advanced Usage & Patterns
9091

91-
* Hot-swappable *strategy interface* for trading engines
92+
* Hot-swappable _strategy interface_ for trading engines
9293
** Rule-engine: compile business rules implementing `Rule`
9394
*** Supports validator hook to vet user code
9495
** Replace reflection: generate POJO accessors, 10 x faster
9596
** Off-heap accessors with Chronicle Bytes / Map
9697

97-
== 5 Operational Notes
98+
== Operational Notes
9899

99100
* Compile on a background thread at start-up; then swap instances.
100101
** Re-use class names _or_ child classloaders to control Metaspace.
101102
*** Use `CompilerUtils.DEBUGGING = true` during dev; remember to prune artefacts.
102103
** SLF4J categories: `net.openhft.compiler` (INFO), compilation errors at ERROR.
103104
** Micrometer timers/counters: `compiler.compiles`, `compiler.failures`.
104105

105-
== 6 FAQ / Troubleshooting
106+
== Documentation & Requirements
106107

107-
* *`ToolProvider.getSystemJavaCompiler() == null`*
108+
* link:src/main/docs/project-requirements.adoc[Project requirements] outline functional, non-functional, and compliance obligations.
109+
110+
== FAQ / Troubleshooting
111+
112+
* _`ToolProvider.getSystemJavaCompiler() == null`_
108113
* You are running on a JRE; use a JDK.
109-
* *`ClassNotFoundException: com.sun.tools.javac.api.JavacTool`*
114+
* _`ClassNotFoundException: com.sun.tools.javac.api.JavacTool`_
110115
* tools.jar is required on JDK <= 8. Newer JDKs need the `--add-exports` and `--add-opens` flags.
111116
* Classes never unload
112117
* Generate with a unique `ClassLoader` per version so classes can unload; each loader uses Metaspace.
113118
* Illegal-reflective-access warning
114119
* Add the `--add-opens` & `--add-exports` flags shown above.
115120

116-
== 7 CI / Build & Test
121+
== CI / Build & Test
117122

118123
* GitHub Actions workflow runs `mvn verify`, unit & race-condition tests.
119124
** Code-coverage report published to SonarCloud badge above.
120125

121-
== 8 Contributing & License
126+
== Contributing & License
122127

123128
* Fork -> feature branch -> PR; run `mvn spotless:apply` before pushing.
124-
** All code under the *Apache License 2.0* - see `LICENSE`.
129+
** All code under the _Apache License 2.0_ - see `LICENSE`.
Lines changed: 279 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
1+
/*
2+
* Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0
3+
*/
4+
package net.openhft.compiler;
5+
6+
import org.junit.Test;
7+
8+
import java.util.*;
9+
import java.util.concurrent.ConcurrentHashMap;
10+
import java.util.concurrent.atomic.AtomicInteger;
11+
12+
import static org.junit.Assert.*;
13+
14+
public class AiRuntimeGuardrailsTest {
15+
16+
@Test
17+
public void validatorStopsCompilationAndRecordsFailure() {
18+
AtomicInteger compileInvocations = new AtomicInteger();
19+
TelemetryProbe telemetry = new TelemetryProbe();
20+
GuardrailedCompilerPipeline pipeline = new GuardrailedCompilerPipeline(
21+
Arrays.asList(
22+
source -> {
23+
// basic guard: ban java.lang.System exit calls
24+
if (source.contains("System.exit")) {
25+
throw new ValidationException("System.exit is not allowed");
26+
}
27+
},
28+
source -> {
29+
if (source.contains("java.io.File")) {
30+
throw new ValidationException("File IO is not permitted");
31+
}
32+
}
33+
),
34+
telemetry,
35+
(className, source) -> {
36+
compileInvocations.incrementAndGet();
37+
try {
38+
return Class.forName("java.lang.Object");
39+
} catch (ClassNotFoundException e) {
40+
throw new AssertionError("JDK runtime missing java.lang.Object", e);
41+
}
42+
}
43+
);
44+
45+
try {
46+
pipeline.compile("agent-A", "BadClass", "class BadClass { void x() { System.exit(0); } }");
47+
fail("Expected validation failure");
48+
} catch (ValidationException expected) {
49+
// expected
50+
} catch (Exception unexpected) {
51+
fail("Unexpected checked exception: " + unexpected.getMessage());
52+
}
53+
54+
assertEquals("Compilation must not run after validation rejection", 0, compileInvocations.get());
55+
assertEquals(1, telemetry.compileAttempts("agent-A"));
56+
assertEquals(1, telemetry.validationFailures("agent-A"));
57+
assertEquals(0, telemetry.successes("agent-A"));
58+
assertEquals(0, telemetry.compileFailures("agent-A"));
59+
assertFalse("Latency should not be recorded for rejected source", telemetry.hasLatency("agent-A"));
60+
}
61+
62+
@Test
63+
public void successfulCompilationRecordsMetrics() throws Exception {
64+
TelemetryProbe telemetry = new TelemetryProbe();
65+
GuardrailedCompilerPipeline pipeline = new GuardrailedCompilerPipeline(
66+
Collections.singletonList(source -> {
67+
if (!source.contains("class")) {
68+
throw new ValidationException("Missing class keyword");
69+
}
70+
}),
71+
telemetry,
72+
new CachedCompilerInvoker()
73+
);
74+
75+
Class<?> clazz = pipeline.compile("agent-B", "OkClass",
76+
"public class OkClass { public int add(int a, int b) { return a + b; } }");
77+
78+
assertEquals("agent-B should see exactly one attempt", 1, telemetry.compileAttempts("agent-B"));
79+
assertEquals(0, telemetry.validationFailures("agent-B"));
80+
assertEquals(1, telemetry.successes("agent-B"));
81+
assertEquals(0, telemetry.compileFailures("agent-B"));
82+
assertTrue("Latency must be captured for successful compilation", telemetry.hasLatency("agent-B"));
83+
84+
Object instance = clazz.getDeclaredConstructor().newInstance();
85+
int sum = (int) clazz.getMethod("add", int.class, int.class).invoke(instance, 2, 3);
86+
assertEquals(5, sum);
87+
}
88+
89+
@Test
90+
public void cacheHitDoesNotRecompileButRecordsMetric() throws Exception {
91+
AtomicInteger rawCompileCount = new AtomicInteger();
92+
TelemetryProbe telemetry = new TelemetryProbe();
93+
GuardrailedCompilerPipeline pipeline = new GuardrailedCompilerPipeline(
94+
Collections.emptyList(),
95+
telemetry,
96+
(className, source) -> {
97+
rawCompileCount.incrementAndGet();
98+
return CompilerUtils.CACHED_COMPILER.loadFromJava(className, source);
99+
}
100+
);
101+
102+
String source = "public class CacheCandidate { public String id() { return \"ok\"; } }";
103+
Class<?> first = pipeline.compile("agent-C", "CacheCandidate", source);
104+
Class<?> second = pipeline.compile("agent-C", "CacheCandidate", source);
105+
106+
assertEquals("Underlying compiler should only run once thanks to caching", 1, rawCompileCount.get());
107+
assertEquals(2, telemetry.compileAttempts("agent-C"));
108+
assertEquals(0, telemetry.validationFailures("agent-C"));
109+
assertEquals(1, telemetry.successes("agent-C"));
110+
assertEquals(0, telemetry.compileFailures("agent-C"));
111+
assertEquals("Cache hit count should be tracked", 1, telemetry.cacheHits("agent-C"));
112+
assertTrue(first == second);
113+
}
114+
115+
@Test
116+
public void compilerFailureRecordedSeparately() {
117+
TelemetryProbe telemetry = new TelemetryProbe();
118+
GuardrailedCompilerPipeline pipeline = new GuardrailedCompilerPipeline(
119+
Collections.singletonList(source -> {
120+
if (source.contains("forbidden")) {
121+
throw new ValidationException("Forbidden token");
122+
}
123+
}),
124+
telemetry,
125+
(className, source) -> {
126+
throw new ClassNotFoundException("Simulated compiler failure");
127+
}
128+
);
129+
130+
try {
131+
pipeline.compile("agent-D", "Broken", "public class Broken { }");
132+
fail("Expected compiler failure");
133+
} catch (ClassNotFoundException expected) {
134+
// expected
135+
} catch (Exception unexpected) {
136+
fail("Unexpected exception: " + unexpected.getMessage());
137+
}
138+
139+
assertEquals(1, telemetry.compileAttempts("agent-D"));
140+
assertEquals(0, telemetry.validationFailures("agent-D"));
141+
assertEquals(0, telemetry.successes("agent-D"));
142+
assertEquals(1, telemetry.compileFailures("agent-D"));
143+
assertFalse("Failure should not record cache hits", telemetry.hasCacheHits("agent-D"));
144+
}
145+
146+
private static final class GuardrailedCompilerPipeline {
147+
private final List<SourceValidator> validators;
148+
private final TelemetryProbe telemetry;
149+
private final CompilerInvoker compilerInvoker;
150+
private final Map<String, Class<?>> cache = new ConcurrentHashMap<>();
151+
152+
GuardrailedCompilerPipeline(List<SourceValidator> validators,
153+
TelemetryProbe telemetry,
154+
CompilerInvoker compilerInvoker) {
155+
this.validators = new ArrayList<>(validators);
156+
this.telemetry = telemetry;
157+
this.compilerInvoker = compilerInvoker;
158+
}
159+
160+
Class<?> compile(String agentId, String className, String source) throws Exception {
161+
telemetry.recordAttempt(agentId);
162+
for (SourceValidator validator : validators) {
163+
try {
164+
validator.validate(source);
165+
} catch (ValidationException e) {
166+
telemetry.recordValidationFailure(agentId);
167+
throw e;
168+
}
169+
}
170+
171+
Class<?> cached = cache.get(className);
172+
if (cached != null) {
173+
telemetry.recordCacheHit(agentId);
174+
return cached;
175+
}
176+
177+
long start = System.nanoTime();
178+
try {
179+
Class<?> compiled = compilerInvoker.compile(className, source);
180+
cache.put(className, compiled);
181+
telemetry.recordSuccess(agentId, System.nanoTime() - start);
182+
return compiled;
183+
} catch (ClassNotFoundException | RuntimeException e) {
184+
telemetry.recordCompileFailure(agentId);
185+
throw e;
186+
}
187+
}
188+
}
189+
190+
@FunctionalInterface
191+
private interface CompilerInvoker {
192+
Class<?> compile(String className, String source) throws Exception;
193+
}
194+
195+
@FunctionalInterface
196+
private interface SourceValidator {
197+
void validate(String source);
198+
}
199+
200+
private static final class ValidationException extends RuntimeException {
201+
ValidationException(String message) {
202+
super(message);
203+
}
204+
}
205+
206+
private static final class TelemetryProbe {
207+
private final Map<String, AtomicInteger> attempts = new HashMap<>();
208+
private final Map<String, AtomicInteger> successes = new HashMap<>();
209+
private final Map<String, AtomicInteger> validationFailures = new HashMap<>();
210+
private final Map<String, AtomicInteger> compileFailures = new HashMap<>();
211+
private final Map<String, AtomicInteger> cacheHits = new HashMap<>();
212+
private final Map<String, Long> latencyNanos = new HashMap<>();
213+
214+
void recordAttempt(String agentId) {
215+
increment(attempts, agentId);
216+
}
217+
218+
void recordSuccess(String agentId, long durationNanos) {
219+
increment(successes, agentId);
220+
latencyNanos.put(agentId, durationNanos);
221+
}
222+
223+
void recordValidationFailure(String agentId) {
224+
increment(validationFailures, agentId);
225+
}
226+
227+
void recordCompileFailure(String agentId) {
228+
increment(compileFailures, agentId);
229+
}
230+
231+
void recordCacheHit(String agentId) {
232+
increment(cacheHits, agentId);
233+
}
234+
235+
int compileAttempts(String agentId) {
236+
return read(attempts, agentId);
237+
}
238+
239+
int successes(String agentId) {
240+
return read(successes, agentId);
241+
}
242+
243+
int validationFailures(String agentId) {
244+
return read(validationFailures, agentId);
245+
}
246+
247+
int compileFailures(String agentId) {
248+
return read(compileFailures, agentId);
249+
}
250+
251+
int cacheHits(String agentId) {
252+
return read(cacheHits, agentId);
253+
}
254+
255+
boolean hasLatency(String agentId) {
256+
return latencyNanos.containsKey(agentId) && latencyNanos.get(agentId) > 0;
257+
}
258+
259+
boolean hasCacheHits(String agentId) {
260+
return cacheHits.containsKey(agentId) && cacheHits.get(agentId).get() > 0;
261+
}
262+
263+
private void increment(Map<String, AtomicInteger> map, String agentId) {
264+
map.computeIfAbsent(agentId, key -> new AtomicInteger()).incrementAndGet();
265+
}
266+
267+
private int read(Map<String, AtomicInteger> map, String agentId) {
268+
AtomicInteger value = map.get(agentId);
269+
return value == null ? 0 : value.get();
270+
}
271+
}
272+
273+
private static final class CachedCompilerInvoker implements CompilerInvoker {
274+
@Override
275+
public Class<?> compile(String className, String source) throws Exception {
276+
return CompilerUtils.CACHED_COMPILER.loadFromJava(className, source);
277+
}
278+
}
279+
}

0 commit comments

Comments
 (0)