Skip to content

Commit 65da474

Browse files
authored
Merge pull request #32 from simbo1905/fix/jsonschema-metrics-31
2 parents 4671f2b + 2b5bdfe commit 65da474

File tree

4 files changed

+245
-3
lines changed

4 files changed

+245
-3
lines changed

README.md

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,27 @@ var result = schema.validate(
104104
// result.valid() => true
105105
```
106106

107-
Compatibility: runs the official 2020‑12 JSON Schema Test Suite on `verify`; in strict mode it currently passes about 71% of applicable cases.
107+
Compatibility: runs the official 2020‑12 JSON Schema Test Suite on `verify`; **measured compatibility is 63.3%** (1,153 of 1,822 tests pass) with comprehensive metrics reporting.
108+
109+
### JSON Schema Test Suite Metrics
110+
111+
The validator now provides defensible compatibility statistics:
112+
113+
```bash
114+
# Run with console metrics (default)
115+
mvn verify -pl json-java21-schema
116+
117+
# Export detailed JSON metrics
118+
mvn verify -pl json-java21-schema -Djson.schema.metrics=json
119+
120+
# Export CSV metrics for analysis
121+
mvn verify -pl json-java21-schema -Djson.schema.metrics=csv
122+
```
123+
124+
**Current measured compatibility**:
125+
- **Overall**: 63.3% (1,153 of 1,822 tests pass)
126+
- **Test coverage**: 420 test groups, 1,657 validation attempts
127+
- **Skip breakdown**: 70 unsupported schema groups, 2 test exceptions, 504 lenient mismatches
108128

109129
## Building
110130

json-java21-schema/AGENTS.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,32 @@ The project uses `java.util.logging` with levels:
4747
- **JSON Schema Test Suite**: Official tests from json-schema-org
4848
- **Real-world schemas**: Complex nested validation scenarios
4949
- **Performance tests**: Large schema compilation
50+
- **Metrics reporting**: Comprehensive compatibility statistics with detailed skip categorization
51+
52+
### JSON Schema Test Suite Metrics
53+
54+
The integration test now provides defensible compatibility metrics:
55+
56+
```bash
57+
# Run with console metrics (default)
58+
mvnd verify -pl json-java21-schema
59+
60+
# Export detailed JSON metrics
61+
mvnd verify -pl json-java21-schema -Djson.schema.metrics=json
62+
63+
# Export CSV metrics for analysis
64+
mvnd verify -pl json-java21-schema -Djson.schema.metrics=csv
65+
```
66+
67+
**Current measured compatibility** (as of implementation):
68+
- **Overall**: 63.3% (1,153 of 1,822 tests pass)
69+
- **Test coverage**: 420 test groups, 1,657 validation attempts
70+
- **Skip breakdown**: 70 unsupported schema groups, 2 test exceptions, 504 lenient mismatches
71+
72+
The metrics distinguish between:
73+
- **unsupportedSchemaGroup**: Whole groups skipped due to unsupported features (e.g., $ref, anchors)
74+
- **testException**: Individual tests that threw exceptions during validation
75+
- **lenientMismatch**: Expected≠actual results in lenient mode (counted as failures in strict mode)
5076

5177
#### OpenRPC Validation (`OpenRPCSchemaValidationIT.java`)
5278
- **Location**: `json-java21-schema/src/test/java/io/github/simbo1905/json/schema/OpenRPCSchemaValidationIT.java`

json-java21-schema/README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,10 @@ Compatibility and verify
2222

2323
- The module runs the official JSON Schema Test Suite during Maven verify.
2424
- Default mode is lenient: unsupported groups/tests are skipped to avoid build breaks while still logging.
25-
- Strict mode: enable with -Djson.schema.strict=true to enforce full assertions. In strict mode it currently passes about 71% of applicable cases.
25+
- Strict mode: enable with -Djson.schema.strict=true to enforce full assertions.
26+
- **Measured compatibility**: 63.3% (1,153 of 1,822 tests pass in lenient mode)
27+
- **Test coverage**: 420 test groups, 1,657 validation attempts, 70 unsupported schema groups, 2 test exceptions
28+
- Detailed metrics available via `-Djson.schema.metrics=json|csv`
2629

2730
How to run
2831

json-java21-schema/src/test/java/io/github/simbo1905/json/schema/JsonSchemaCheckIT.java

Lines changed: 194 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,14 @@
55
import jdk.sandbox.java.util.json.Json;
66
import org.junit.jupiter.api.DynamicTest;
77
import org.junit.jupiter.api.TestFactory;
8+
import org.junit.jupiter.api.AfterAll;
89
import org.junit.jupiter.api.Assumptions;
910

1011
import java.io.File;
1112
import java.nio.file.Files;
1213
import java.nio.file.Path;
14+
import java.util.concurrent.ConcurrentHashMap;
15+
import java.util.concurrent.atomic.LongAdder;
1316
import java.util.stream.Stream;
1417
import java.util.stream.StreamSupport;
1518

@@ -25,6 +28,8 @@ public class JsonSchemaCheckIT {
2528
new File("target/json-schema-test-suite/tests/draft2020-12");
2629
private static final ObjectMapper MAPPER = new ObjectMapper();
2730
private static final boolean STRICT = Boolean.getBoolean("json.schema.strict");
31+
private static final String METRICS_FMT = System.getProperty("json.schema.metrics", "").trim();
32+
private static final SuiteMetrics METRICS = new SuiteMetrics();
2833

2934
@SuppressWarnings("resource")
3035
@TestFactory
@@ -37,6 +42,19 @@ Stream<DynamicTest> runOfficialSuite() throws Exception {
3742
private Stream<DynamicTest> testsFromFile(Path file) {
3843
try {
3944
JsonNode root = MAPPER.readTree(file.toFile());
45+
46+
// Count groups and tests discovered
47+
int groupCount = root.size();
48+
METRICS.groupsDiscovered.add(groupCount);
49+
perFile(file).groups.add(groupCount);
50+
51+
int testCount = 0;
52+
for (JsonNode group : root) {
53+
testCount += group.get("tests").size();
54+
}
55+
METRICS.testsDiscovered.add(testCount);
56+
perFile(file).tests.add(testCount);
57+
4058
return StreamSupport.stream(root.spliterator(), false)
4159
.flatMap(group -> {
4260
String groupDesc = group.get("description").asText();
@@ -55,29 +73,62 @@ private Stream<DynamicTest> testsFromFile(Path file) {
5573
try {
5674
actual = schema.validate(
5775
Json.parse(test.get("data").toString())).valid();
76+
77+
// Count validation attempt
78+
METRICS.validationsRun.increment();
79+
perFile(file).run.increment();
5880
} catch (Exception e) {
5981
String reason = e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage();
6082
System.err.println("[JsonSchemaCheckIT] Skipping test due to exception: "
6183
+ groupDesc + " — " + reason + " (" + file.getFileName() + ")");
84+
85+
// Count exception skip
86+
METRICS.skipTestException.increment();
87+
perFile(file).skipException.increment();
88+
6289
if (STRICT) throw e;
6390
Assumptions.assumeTrue(false, "Skipped: " + reason);
6491
return; // not reached when strict
6592
}
6693

6794
if (STRICT) {
68-
assertEquals(expected, actual);
95+
try {
96+
assertEquals(expected, actual);
97+
// Count pass in strict mode
98+
METRICS.passed.increment();
99+
perFile(file).pass.increment();
100+
} catch (AssertionError e) {
101+
// Count failure in strict mode
102+
METRICS.failed.increment();
103+
perFile(file).fail.increment();
104+
throw e;
105+
}
69106
} else if (expected != actual) {
70107
System.err.println("[JsonSchemaCheckIT] Mismatch (ignored): "
71108
+ groupDesc + " — expected=" + expected + ", actual=" + actual
72109
+ " (" + file.getFileName() + ")");
110+
111+
// Count lenient mismatch skip
112+
METRICS.skipLenientMismatch.increment();
113+
perFile(file).skipMismatch.increment();
114+
73115
Assumptions.assumeTrue(false, "Mismatch ignored");
116+
} else {
117+
// Count pass in lenient mode
118+
METRICS.passed.increment();
119+
perFile(file).pass.increment();
74120
}
75121
}));
76122
} catch (Exception ex) {
77123
// Unsupported schema for this group; emit a single skipped test for visibility
78124
String reason = ex.getMessage() == null ? ex.getClass().getSimpleName() : ex.getMessage();
79125
System.err.println("[JsonSchemaCheckIT] Skipping group due to unsupported schema: "
80126
+ groupDesc + " — " + reason + " (" + file.getFileName() + ")");
127+
128+
// Count unsupported group skip
129+
METRICS.skipUnsupportedGroup.increment();
130+
perFile(file).skipUnsupported.increment();
131+
81132
return Stream.of(DynamicTest.dynamicTest(
82133
groupDesc + " – SKIPPED: " + reason,
83134
() -> { if (STRICT) throw ex; Assumptions.assumeTrue(false, "Unsupported schema: " + reason); }
@@ -88,4 +139,146 @@ private Stream<DynamicTest> testsFromFile(Path file) {
88139
throw new RuntimeException("Failed to process " + file, ex);
89140
}
90141
}
142+
143+
private static SuiteMetrics.FileCounters perFile(Path file) {
144+
return METRICS.perFile.computeIfAbsent(file.getFileName().toString(), k -> new SuiteMetrics.FileCounters());
145+
}
146+
147+
@AfterAll
148+
static void printAndPersistMetrics() throws Exception {
149+
var strict = STRICT;
150+
var totalRun = METRICS.validationsRun.sum();
151+
var passed = METRICS.passed.sum();
152+
var failed = METRICS.failed.sum();
153+
var skippedU = METRICS.skipUnsupportedGroup.sum();
154+
var skippedE = METRICS.skipTestException.sum();
155+
var skippedM = METRICS.skipLenientMismatch.sum();
156+
157+
System.out.printf(
158+
"JSON-SCHEMA SUITE (%s): groups=%d testsScanned=%d run=%d passed=%d failed=%d skipped={unsupported=%d, exception=%d, lenientMismatch=%d}%n",
159+
strict ? "STRICT" : "LENIENT",
160+
METRICS.groupsDiscovered.sum(),
161+
METRICS.testsDiscovered.sum(),
162+
totalRun, passed, failed, skippedU, skippedE, skippedM
163+
);
164+
165+
if (!METRICS_FMT.isEmpty()) {
166+
var outDir = java.nio.file.Path.of("target");
167+
java.nio.file.Files.createDirectories(outDir);
168+
var ts = java.time.OffsetDateTime.now().toString();
169+
if ("json".equalsIgnoreCase(METRICS_FMT)) {
170+
var json = buildJsonSummary(strict, ts);
171+
java.nio.file.Files.writeString(outDir.resolve("json-schema-compat.json"), json);
172+
} else if ("csv".equalsIgnoreCase(METRICS_FMT)) {
173+
var csv = buildCsvSummary(strict, ts);
174+
java.nio.file.Files.writeString(outDir.resolve("json-schema-compat.csv"), csv);
175+
}
176+
}
177+
}
178+
179+
private static String buildJsonSummary(boolean strict, String timestamp) {
180+
var totals = new StringBuilder();
181+
totals.append("{\n");
182+
totals.append(" \"mode\": \"").append(strict ? "STRICT" : "LENIENT").append("\",\n");
183+
totals.append(" \"timestamp\": \"").append(timestamp).append("\",\n");
184+
totals.append(" \"totals\": {\n");
185+
totals.append(" \"groupsDiscovered\": ").append(METRICS.groupsDiscovered.sum()).append(",\n");
186+
totals.append(" \"testsDiscovered\": ").append(METRICS.testsDiscovered.sum()).append(",\n");
187+
totals.append(" \"validationsRun\": ").append(METRICS.validationsRun.sum()).append(",\n");
188+
totals.append(" \"passed\": ").append(METRICS.passed.sum()).append(",\n");
189+
totals.append(" \"failed\": ").append(METRICS.failed.sum()).append(",\n");
190+
totals.append(" \"skipped\": {\n");
191+
totals.append(" \"unsupportedSchemaGroup\": ").append(METRICS.skipUnsupportedGroup.sum()).append(",\n");
192+
totals.append(" \"testException\": ").append(METRICS.skipTestException.sum()).append(",\n");
193+
totals.append(" \"lenientMismatch\": ").append(METRICS.skipLenientMismatch.sum()).append("\n");
194+
totals.append(" }\n");
195+
totals.append(" },\n");
196+
totals.append(" \"perFile\": [\n");
197+
198+
var files = new java.util.ArrayList<String>(METRICS.perFile.keySet());
199+
java.util.Collections.sort(files);
200+
var first = true;
201+
for (String file : files) {
202+
var counters = METRICS.perFile.get(file);
203+
if (!first) totals.append(",\n");
204+
first = false;
205+
totals.append(" {\n");
206+
totals.append(" \"file\": \"").append(file).append("\",\n");
207+
totals.append(" \"groups\": ").append(counters.groups.sum()).append(",\n");
208+
totals.append(" \"tests\": ").append(counters.tests.sum()).append(",\n");
209+
totals.append(" \"run\": ").append(counters.run.sum()).append(",\n");
210+
totals.append(" \"pass\": ").append(counters.pass.sum()).append(",\n");
211+
totals.append(" \"fail\": ").append(counters.fail.sum()).append(",\n");
212+
totals.append(" \"skipUnsupported\": ").append(counters.skipUnsupported.sum()).append(",\n");
213+
totals.append(" \"skipException\": ").append(counters.skipException.sum()).append(",\n");
214+
totals.append(" \"skipMismatch\": ").append(counters.skipMismatch.sum()).append("\n");
215+
totals.append(" }");
216+
}
217+
totals.append("\n ]\n");
218+
totals.append("}\n");
219+
return totals.toString();
220+
}
221+
222+
private static String buildCsvSummary(boolean strict, String timestamp) {
223+
var csv = new StringBuilder();
224+
csv.append("mode,timestamp,groupsDiscovered,testsDiscovered,validationsRun,passed,failed,skipUnsupportedGroup,skipTestException,skipLenientMismatch\n");
225+
csv.append(strict ? "STRICT" : "LENIENT").append(",");
226+
csv.append(timestamp).append(",");
227+
csv.append(METRICS.groupsDiscovered.sum()).append(",");
228+
csv.append(METRICS.testsDiscovered.sum()).append(",");
229+
csv.append(METRICS.validationsRun.sum()).append(",");
230+
csv.append(METRICS.passed.sum()).append(",");
231+
csv.append(METRICS.failed.sum()).append(",");
232+
csv.append(METRICS.skipUnsupportedGroup.sum()).append(",");
233+
csv.append(METRICS.skipTestException.sum()).append(",");
234+
csv.append(METRICS.skipLenientMismatch.sum()).append("\n");
235+
236+
csv.append("\nperFile breakdown:\n");
237+
csv.append("file,groups,tests,run,pass,fail,skipUnsupported,skipException,skipMismatch\n");
238+
239+
var files = new java.util.ArrayList<String>(METRICS.perFile.keySet());
240+
java.util.Collections.sort(files);
241+
for (String file : files) {
242+
var counters = METRICS.perFile.get(file);
243+
csv.append(file).append(",");
244+
csv.append(counters.groups.sum()).append(",");
245+
csv.append(counters.tests.sum()).append(",");
246+
csv.append(counters.run.sum()).append(",");
247+
csv.append(counters.pass.sum()).append(",");
248+
csv.append(counters.fail.sum()).append(",");
249+
csv.append(counters.skipUnsupported.sum()).append(",");
250+
csv.append(counters.skipException.sum()).append(",");
251+
csv.append(counters.skipMismatch.sum()).append("\n");
252+
}
253+
return csv.toString();
254+
}
255+
}
256+
257+
/**
258+
* Thread-safe metrics container for the JSON Schema Test Suite run.
259+
*/
260+
final class SuiteMetrics {
261+
final LongAdder groupsDiscovered = new LongAdder();
262+
final LongAdder testsDiscovered = new LongAdder();
263+
264+
final LongAdder validationsRun = new LongAdder(); // attempted validations
265+
final LongAdder passed = new LongAdder();
266+
final LongAdder failed = new LongAdder();
267+
268+
final LongAdder skipUnsupportedGroup = new LongAdder();
269+
final LongAdder skipTestException = new LongAdder(); // lenient only
270+
final LongAdder skipLenientMismatch = new LongAdder(); // lenient only
271+
272+
final ConcurrentHashMap<String, FileCounters> perFile = new ConcurrentHashMap<>();
273+
274+
static final class FileCounters {
275+
final LongAdder groups = new LongAdder();
276+
final LongAdder tests = new LongAdder();
277+
final LongAdder run = new LongAdder();
278+
final LongAdder pass = new LongAdder();
279+
final LongAdder fail = new LongAdder();
280+
final LongAdder skipUnsupported = new LongAdder();
281+
final LongAdder skipException = new LongAdder();
282+
final LongAdder skipMismatch = new LongAdder();
283+
}
91284
}

0 commit comments

Comments
 (0)