diff --git a/.gitignore b/.gitignore index e057a3c..cecdad6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ + .env repomix-output* target/ @@ -10,6 +11,8 @@ CLAUDE.md # Symlinks to ignore CLAUDE.md json-java21-schema/CLAUDE.md +json-java21-schema/mvn-test-no-boilerplate.sh +.jqwik-database WISDOM.md .vscode/ diff --git a/AGENTS.md b/AGENTS.md index 83f2a01..9a2c732 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,14 +2,6 @@ ## Purpose & Scope - Operational guidance for human and AI agents working in this repository. This revision preserves all existing expectations while improving structure and wording in line with agents.md best practices. -- Prefer the Maven Daemon for performance: alias `mvn` to `mvnd` when available so every command below automatically benefits from the daemon. - -```bash -# Use mvnd everywhere if available; otherwise falls back to regular mvn -if command -v mvnd >/dev/null 2>&1; then alias mvn=mvnd; fi -``` - -- Always run `mvn verify` (or `mvnd verify` once aliased) before pushing to ensure unit and integration coverage across every module. ## Operating Principles - Follow the sequence plan → implement → verify; do not pivot without restating the plan. @@ -22,36 +14,6 @@ if command -v mvnd >/dev/null 2>&1; then alias mvn=mvnd; fi - Never commit unverified mass changes—compile or test first. - Do not use Perl or sed for multi-line structural edits; rely on Python 3.2-friendly heredocs. -## Tooling Discipline -- Prefer `python3` heredocs for non-trivial text transforms and target Python 3.2-safe syntax (no f-strings or modern dependencies). - -```bash -python3 - <<'PY' -import os, sys, re -src = 'updates/2025-09-04/upstream/jdk.internal.util.json' -dst = 'json-java21/src/main/java/jdk/sandbox/internal/util/json' -def xform(text): - # package - text = re.sub(r'^package\s+jdk\.internal\.util\.json;', 'package jdk.sandbox.internal.util.json;', text, flags=re.M) - # imports for public API - text = re.sub(r'^(\s*import\s+)java\.util\.json\.', r'\1jdk.sandbox.java.util.json.', text, flags=re.M) - # annotations - text = re.sub(r'^\s*@(?:jdk\.internal\..*|ValueBased|StableValue).*\n', '', text, flags=re.M) - return text -for name in os.listdir(src): - if not name.endswith('.java') or name == 'StableValue.java': - continue - data = open(os.path.join(src,name),'r').read() - out = xform(data) - target = os.path.join(dst,name) - tmp = target + '.tmp' - open(tmp,'w').write(out) - if os.path.getsize(tmp) == 0: - sys.stderr.write('Refusing to overwrite 0-byte: '+target+'\n'); sys.exit(1) - os.rename(tmp, target) -print('OK') -PY -``` ## Testing & Logging Discipline @@ -61,33 +23,51 @@ PY - You MUST NOT add ad-hoc "temporary logging"; only the defined JUL levels above are acceptable. - You SHOULD NOT delete logging. Adjust levels downward (finer granularity) instead of removing statements. - You MUST add a JUL log statement at INFO level at the top of every test method announcing execution. -- You MUST have all new tests extend a helper such as `JsonSchemaLoggingConfig` so environment variables configure JUL levels compatibly with `./mvn-test-no-boilerplate.sh`. +- You MUST have all new tests extend a helper such as `JsonSchemaLoggingConfig` so environment variables configure JUL levels compatibly with `$(command -v mvnd || command -v mvn || command -v ./mvnw)`. - You MUST NOT guess root causes; add targeted logging or additional tests. Treat observability as the path to the fix. - YOU MUST Use exactly one logger for the JSON Schema subsystem and use appropriate logging to debug as below. - -### Script Usage (Required) -- You MUST prefer the `./mvn-test-no-boilerplate.sh` wrapper for every Maven invocation. Direct `mvn` or `mvnd` calls require additional authorization and skip the curated output controls. +- YOU MUST honour official JUL logging levels: + - SEVERE (1000): Critical errors—application will likely abort. + - WARNING (900): Indications of potential problems or recoverable issues. + - INFO (800): Routine events or significant application milestones. + - CONFIG (700): Static configuration messages (startup configs, environment details). + - FINE (500): General tracing of program flow (basic debug info). + - FINER (400): More detailed tracing than FINE (algorithm steps, loop iterations). + - FINEST (300): Highly detailed debugging, including variable values and low-level logic. + +### Run Tests With Valid Logging + +- You MUST prefer the `$(command -v mvnd || command -v mvn || command -v ./mvnw)` wrapper for every Maven invocation. +- You MUST pass in a `java.util.logging.ConsoleHandler.level` of INFO or more low-level. +- You SHOULD run all tests in all models or a given `-pl mvn_moduue` passing `-Djava.util.logging.ConsoleHandler.level=INFO` to see which tests run and which tests might hang +- You SHOULD run a single test class using `-Dtest=BlahTest -Djava.util.logging.ConsoleHandler.level=FINE` as fine will show you basic debug info +- You SHOULD run a single failing test method using `-Dtest=BlahTest -Djava.util.logging.ConsoleHandler.level=FINER` +- If you have run a test more than once and about to start guessing you MAY run a single failing test method using `-Dtest=BlahTest -Djava.util.logging.ConsoleHandler.level=FINEST` after ensuring you have added in detail logging of the data structures. +- You MUST not remove logging yet you may move it to be a finer level. ```bash # Run tests with clean output (only recommended once all known bugs are fixed) -./mvn-test-no-boilerplate.sh +$(command -v mvnd || command -v mvn || command -v ./mvnw) test -Djava.util.logging.ConsoleHandler.level=INFO -# Run specific test class -./mvn-test-no-boilerplate.sh -Dtest=BlahTest -Djava.util.logging.ConsoleHandler.level=FINE +# Run specific test class you should use FINE +$(command -v mvnd || command -v mvn || command -v ./mvnw) -Dtest=BlahTest -Djava.util.logging.ConsoleHandler.level=FINE # Run specific test method -./mvn-test-no-boilerplate.sh -Dtest=BlahTest#testSomething -Djava.util.logging.ConsoleHandler.level=FINEST +$(command -v mvnd || command -v mvn || command -v ./mvnw) -Dtest=BlahTest#testSomething -Djava.util.logging.ConsoleHandler.level=FINEST # Run tests in a specific module -./mvn-test-no-boilerplate.sh -pl json-java21-api-tracker -Dtest=ApiTrackerTest -Djava.util.logging.ConsoleHandler.level=FINE +$(command -v mvnd || command -v mvn || command -v ./mvnw) -pl json-java21-api-tracker -Dtest=ApiTrackerTest -Djava.util.logging.ConsoleHandler.level=FINE ``` -- The script resides in the repository root. Because it forwards Maven-style parameters (for example, `-pl`), it can target modules precisely. +IMPORTANT: Fix the method with FINEST logging, then fix the test class with FINER logging, then fix the module with FINE logging, then run the whole suite with INFO logging. THERE IS NO TRIVIAL LOGIC LEFT IN THIS PROJECT TO BE SYSTEMATIC. ### Output Visibility Requirements -- You MUST NEVER pipe build or test output to tools (head, tail, grep, etc.) that reduce visibility. Logging uncovers the unexpected; piping hides it. + +- You MUST NEVER pipe build or test output to tools (head, tail, grep, etc.) that reduce visibility. Logging uncovers the unexpected; piping hides it. Use the instructions above to zoom in on what you want to see using `-Dtest=BlahTest` and `-Dtest=BlahTest#testSomething` passing the appropriate `Djava.util.logging.ConsoleHandler.level=XXX` to avoid too much outputs - You MAY log full data structures at FINEST for deep tracing. Run a single test method at that granularity. - If output volume becomes unbounded (for example, due to inadvertent infinite loops), this is the only time head/tail is allowed. Even then, you MUST inspect a sufficiently large sample (thousands of lines) to capture the real issue and avoid focusing on Maven startup noise. +- My time is far more precious than yours do not error on the side of less information and thrash around guessing. You MUST add more logging and look harder! +- Deep debugging employs the same FINE/FINEST discipline: log data structures at FINEST for one test method at a time and expand coverage with additional logging or tests instead of guessing. ### Logging Practices - JUL logging is used for safety and performance. Many consumers rely on SLF4J bridges and search for the literal `ERROR`, not `SEVERE`. When logging at `SEVERE`, prefix the message with `ERROR` to keep cloud log filters effective: @@ -104,17 +84,8 @@ LOG.severe(() -> "ERROR: Remote references disabled but computeIfAbsent called f LOG.fine(() -> "PERFORMANCE WARNING: Validation stack processing " + count + ... ); ``` -### Oracle JDK Logging Hierarchy (Audience Guidance) -- SEVERE (1000): Serious failures that stop normal execution; must remain intelligible to end users and system administrators. -- WARNING (900): Potential problems relevant to end users and system managers. -- INFO (800): Reasonably significant operational messages; use sparingly. -- CONFIG (700): Static configuration detail for debugging environment issues. -- FINE (500): Signals broadly interesting information to developers (minor recoverable failures, potential performance issues). -- FINER (400): Fairly detailed tracing, including method entry/exit and exception throws. -- FINEST (300): Highly detailed tracing for deep debugging. - ### Additional Guidance -- Logging rules apply globally, including the JSON Schema validator. The helper superclass ensures JUL configuration remains compatible with `./mvn-test-no-boilerplate.sh`. +- Logging rules apply globally, including the JSON Schema validator. The helper superclass ensures JUL configuration remains compatible with `$(command -v mvnd || command -v mvn || command -v ./mvnw)`. ## JSON Compatibility Suite ```bash @@ -162,11 +133,6 @@ mvn exec:java -pl json-compatibility-suite -Dexec.args="--json" ### Testing Approach - Prefer JUnit 5 with AssertJ for fluent assertions. -- Test organization: - - `JsonParserTests`: Parser-specific coverage. - - `JsonTypedUntypedTests`: Conversion behaviour. - - `JsonRecordMappingTests`: Record mapping validation. - - `ReadmeDemoTests`: Documentation example verification. ### Code Style - Follow JEP 467 for documentation (`///` triple-slash comments). @@ -196,7 +162,6 @@ mvn exec:java -pl json-compatibility-suite -Dexec.args="--json" ### json-compatibility-suite - Automatically downloads the JSON Test Suite from GitHub. -- Currently reports 99.3% standard conformance. - Surfaces known vulnerabilities (for example, StackOverflowError under deep nesting). - Intended for education and testing, not production deployment. @@ -210,20 +175,13 @@ mvn exec:java -pl json-compatibility-suite -Dexec.args="--json" ### json-java21-schema (JSON Schema Validator) - Inherits all repository-wide logging and testing rules described above. - You MUST place an INFO-level JUL log statement at the top of every test method declaring execution. -- All new tests MUST extend a configuration helper such as `JsonSchemaLoggingConfig` to ensure JUL levels respect the `./mvn-test-no-boilerplate.sh` environment variables. -- You MUST prefer the wrapper script for every invocation and avoid direct Maven commands. -- Deep debugging employs the same FINE/FINEST discipline: log data structures at FINEST for one test method at a time and expand coverage with additional logging or tests instead of guessing. +- All new tests MUST extend a configuration helper such as `JsonSchemaLoggingConfig` to ensure JUL levels respected. +- WARNING: you cannot run `mvn -pl xxxx verify` at the top level it will not work. +- You must run `cd -Djson.schema.strict=true -Djson.schema.metrics=csv -Djava.util.logging.ConsoleHandler.level=INFO` #### Running Tests (Schema Module) - All prohibitions on output filtering apply. Do not pipe logs unless you must constrain an infinite stream, and even then examine a large sample (thousands of lines). -- Remote location of `./mvn-test-no-boilerplate.sh` is the repository root; pass module selectors through it for schema-only runs. - -#### JUL Logging -- For SEVERE logs, prefix the message with `ERROR` to align with SLF4J-centric filters. -- Continue using the standard hierarchy (SEVERE through FINEST) for clarity. -- You MUST Use exactly one logger for the JSON Schema subsystem and use appropriate logging to debug as below. -- You MUST NOT create per-class loggers. Collaborating classes must reuse the same logger. -- Potential performance issues log at FINE with the `PERFORMANCE WARNING:` prefix shown earlier. +- Remote location of `$(command -v mvnd || command -v mvn || command -v ./mvnw)` is the repository root; pass module selectors through it for schema-only runs. ## Security Notes - Deep nesting can trigger StackOverflowError (stack exhaustion attacks). @@ -447,3 +405,17 @@ flowchart LR - “The path is legacy-free: no recursion; compile-time and runtime both leverage explicit stacks.” - Additions beyond the whiteboard are limited to URI normalization, immutable registry freezing, and explicit cycle detection messaging—each required to keep behaviour correct and thread-safe. - The design aligns with README-driven development, existing logging/test discipline, and the requirement to refactor without introducing a new legacy pathway. + +## Tooling Discipline +- Prefer `python3` heredocs for non-trivial text transforms and target Python 3.2-safe syntax (no f-strings or modern dependencies). + +```bash +python3 - <<'PY' +import os, sys, re +src = 'updates/2025-09-04/upstream/jdk.internal.util.json' +dst = 'json-java21/src/main/java/jdk/sandbox/internal/util/json' +def xform(text): + # old old python3 stuff here +print('OK') +PY +``` diff --git a/CODING_STYLE_LLM.md b/CODING_STYLE_LLM.md index 48853b3..dc3ce11 100644 --- a/CODING_STYLE_LLM.md +++ b/CODING_STYLE_LLM.md @@ -102,11 +102,11 @@ Here is an example of the correct format for documentation comments: - **Check Compiles**: Focusing on the correct mvn module run without verbose logging and do not grep the output to see compile errors: ```bash - ./mvn-test-no-boilerplate.sh -pl json-java21-api-tracker -Djava.util.logging.ConsoleHandler.level=SEVERE + $(command -v mvnd || command -v mvn || command -v ./mvnw) -pl json-java21-api-tracker -Djava.util.logging.ConsoleHandler.level=SEVERE ``` - **Debug with Verbose Logs**: Use `-Dtest=` to focus on just one or two test methods, or one class, using more logging to debug the code: ```bash - ./mvn-test-no-boilerplate.sh -pl json-java21-api-tracker -Dtest=XXX -Djava.util.logging.ConsoleHandler.level=FINER + $(command -v mvnd || command -v mvn || command -v ./mvnw) -pl json-java21-api-tracker -Dtest=XXX -Djava.util.logging.ConsoleHandler.level=FINER ``` - **No Grep Filtering**: Use logging levels to filter output, do not grep the output for compile errors, just run less test methods with the correct logging to reduce the output to a manageable size. Filtering hides problems and needs more test excution to find the same problems which wastes time. diff --git a/README.md b/README.md index a549c45..5c3a493 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,9 @@ JsonValue backToJson = Json.fromUntyped(Map.of( "age", user.age(), "active", user.active() )); + +// Covert back to a JSON string +String jsonString = backToJson.toString(); ``` ## Backport Project Goals @@ -74,8 +77,8 @@ The original proposal and design rationale can be found in the included PDF: [To ## Modifications This is a simplified backport with the following changes from the original: -- Replaced StableValue with double-checked locking pattern. -- Removed value-based class annotations. +- Replaced `StableValue.of()` with double-checked locking pattern. +- Removed `@ValueBased` annotations. - Compatible with JDK 21. ## Security Considerations @@ -112,13 +115,13 @@ The validator now provides defensible compatibility statistics: ```bash # Run with console metrics (default) -./mvn-test-no-boilerplate.sh -pl json-java21-schema +$(command -v mvnd || command -v mvn || command -v ./mvnw) -pl json-java21-schema # Export detailed JSON metrics -./mvn-test-no-boilerplate.sh -pl json-java21-schema -Djson.schema.metrics=json +$(command -v mvnd || command -v mvn || command -v ./mvnw) -pl json-java21-schema -Djson.schema.metrics=json # Export CSV metrics for analysis -./mvn-test-no-boilerplate.sh -pl json-java21-schema -Djson.schema.metrics=csv +$(command -v mvnd || command -v mvn || command -v ./mvnw) -pl json-java21-schema -Djson.schema.metrics=csv ``` **Current measured compatibility**: diff --git a/json-compatibility-suite/pom.xml b/json-compatibility-suite/pom.xml index 03409d8..342f8e2 100644 --- a/json-compatibility-suite/pom.xml +++ b/json-compatibility-suite/pom.xml @@ -55,7 +55,7 @@ download-json-test-suite - generate-test-resources + pre-integration-test wget diff --git a/json-compatibility-suite/src/main/java/jdk/sandbox/compatibility/JsonTestSuiteSummary.java b/json-compatibility-suite/src/main/java/jdk/sandbox/compatibility/JsonTestSuiteSummary.java index aeab23a..b53f295 100644 --- a/json-compatibility-suite/src/main/java/jdk/sandbox/compatibility/JsonTestSuiteSummary.java +++ b/json-compatibility-suite/src/main/java/jdk/sandbox/compatibility/JsonTestSuiteSummary.java @@ -34,6 +34,7 @@ public static void main(String[] args) throws Exception { } void generateConformanceReport() throws Exception { + LOGGER.fine(() -> "Starting conformance report generation"); TestResults results = runTests(); System.out.println("\n=== JSON Test Suite Conformance Report ==="); @@ -59,11 +60,13 @@ void generateConformanceReport() throws Exception { System.out.printf("Overall Conformance: %.1f%%%n", conformance); if (!results.shouldPassButFailed.isEmpty()) { + LOGGER.fine(() -> "Valid JSON that failed to parse count=" + results.shouldPassButFailed.size()); System.out.println("\n⚠️ Valid JSON that failed to parse:"); results.shouldPassButFailed.forEach(f -> System.out.println(" - " + f)); } - + if (!results.shouldFailButPassed.isEmpty()) { + LOGGER.fine(() -> "Invalid JSON that was incorrectly accepted count=" + results.shouldFailButPassed.size()); System.out.println("\n⚠️ Invalid JSON that was incorrectly accepted:"); results.shouldFailButPassed.forEach(f -> System.out.println(" - " + f)); } @@ -74,12 +77,14 @@ void generateConformanceReport() throws Exception { } void generateJsonReport() throws Exception { + LOGGER.fine(() -> "Starting JSON report generation"); TestResults results = runTests(); JsonObject report = createJsonReport(results); System.out.println(Json.toDisplayString(report, 2)); } private TestResults runTests() throws Exception { + LOGGER.fine(() -> "Walking test files under: " + TEST_DIR.toAbsolutePath()); if (!Files.exists(TEST_DIR)) { throw new RuntimeException("Test suite not downloaded. Run: ./mvnw clean compile generate-test-resources -pl json-compatibility-suite"); } @@ -92,31 +97,39 @@ private TestResults runTests() throws Exception { int nPass = 0, nFail = 0; int iAccept = 0, iReject = 0; - var files = Files.walk(TEST_DIR) - .filter(p -> p.toString().endsWith(".json")) - .sorted() - .toList(); + List files; + try (var stream = Files.walk(TEST_DIR)) { + files = stream + .filter(p -> p.toString().endsWith(".json")) + .sorted() + .toList(); + } + LOGGER.fine(() -> "Discovered JSON test files: " + files.size()); for (Path file : files) { String filename = file.getFileName().toString(); - String content = null; - char[] charContent = null; + String content; + char[] charContent; + final Path filePathForLog = file; + LOGGER.fine(() -> "Processing file: " + filePathForLog); try { content = Files.readString(file, StandardCharsets.UTF_8); charContent = content.toCharArray(); } catch (MalformedInputException e) { - LOGGER.warning("UTF-8 failed for " + filename + ", using robust encoding detection"); + LOGGER.finer(()->"UTF-8 failed for " + filename + ", using robust encoding detection"); try { byte[] rawBytes = Files.readAllBytes(file); charContent = RobustCharDecoder.decodeToChars(rawBytes, filename); } catch (Exception ex) { - throw new RuntimeException("Failed to read test file " + filename + " - this is a fundamental I/O failure, not an encoding issue: " + ex.getMessage(), ex); + skippedFiles.add(filename); + LOGGER.fine(() -> "Skipping unreadable file: " + filename + " due to: " + ex.getClass().getSimpleName() + ": " + ex.getMessage()); + continue; } } // Test with char[] API (always available) - boolean parseSucceeded = false; + boolean parseSucceeded; try { Json.parse(charContent); parseSucceeded = true; @@ -126,6 +139,8 @@ private TestResults runTests() throws Exception { LOGGER.warning("StackOverflowError on file: " + filename); parseSucceeded = false; // Treat as parse failure } + final boolean parseResultForLog = parseSucceeded; + LOGGER.fine(() -> "Parsed " + filename + ": " + (parseResultForLog ? "SUCCESS" : "FAIL")); // Update counters based on results if (parseSucceeded) { @@ -148,7 +163,13 @@ private TestResults runTests() throws Exception { } } } - + final int yPassF = yPass; + final int yFailF = yFail; + final int nPassF = nPass; + final int nFailF = nFail; + final int iAcceptF = iAccept; + final int iRejectF = iReject; + LOGGER.fine(() -> "Finished processing files. yPass=" + yPassF + ", yFail=" + yFailF + ", nPass=" + nPassF + ", nFail=" + nFailF + ", iAccept=" + iAcceptF + ", iReject=" + iRejectF); return new TestResults(files.size(), skippedFiles.size(), yPass, yFail, nPass, nFail, iAccept, iReject, shouldPassButFailed, shouldFailButPassed, skippedFiles); diff --git a/json-java21-schema/README.md b/json-java21-schema/README.md index ada5ccd..641411e 100644 --- a/json-java21-schema/README.md +++ b/json-java21-schema/README.md @@ -33,10 +33,10 @@ How to run ```bash # Run unit + integration tests (includes official suite) -./mvn-test-no-boilerplate.sh -pl json-java21-schema +$(command -v mvnd || command -v mvn || command -v ./mvnw) -pl json-java21-schema # Strict mode -./mvn-test-no-boilerplate.sh -pl json-java21-schema -Djson.schema.strict=true +$(command -v mvnd || command -v mvn || command -v ./mvnw) -pl json-java21-schema -Djson.schema.strict=true ``` OpenRPC validation diff --git a/json-java21-schema/mvn-test-no-boilerplate.sh b/json-java21-schema/mvn-test-no-boilerplate.sh deleted file mode 100755 index 2732d31..0000000 --- a/json-java21-schema/mvn-test-no-boilerplate.sh +++ /dev/null @@ -1,71 +0,0 @@ -#!/bin/bash - -# Strip Maven test boilerplate - show compile errors and test results only -# Usage: ./mvn-test-no-boilerplate.sh [maven test arguments] -# -# Examples: -# ./mvn-test-no-boilerplate.sh -Dtest=RefactorTests -# ./mvn-test-no-boilerplate.sh -Dtest=RefactorTests#testList -Djava.util.logging.ConsoleHandler.level=INFO -# ./mvn-test-no-boilerplate.sh -Dtest=RefactorTests#testList -Djava.util.logging.ConsoleHandler.level=FINER -# -# For running tests in a specific module: -# ./mvn-test-no-boilerplate.sh -pl json-java21-api-tracker -Dtest=CompilerApiLearningTest -# -# The script automatically detects if mvnd is available, otherwise falls back to mvn - -# Detect if mvnd is available, otherwise use mvn -if command -v mvnd &> /dev/null; then - MVN_CMD="mvnd" -else - MVN_CMD="mvn" -fi - -timeout 120 $MVN_CMD test "$@" 2>&1 | awk ' -BEGIN { - scanning_started = 0 - compilation_section = 0 - test_section = 0 -} - -# Skip all WARNING lines before project scanning starts -/INFO.*Scanning for projects/ { - scanning_started = 1 - print - next -} - -# Before scanning starts, skip WARNING lines -!scanning_started && /^WARNING:/ { next } - -# Show compilation errors -/COMPILATION ERROR/ { compilation_section = 1 } -/BUILD FAILURE/ && compilation_section { compilation_section = 0 } - -# Show test section -/INFO.*T E S T S/ { - test_section = 1 - print "-------------------------------------------------------" - print " T E S T S" - print "-------------------------------------------------------" - next -} - -# In compilation error section, show everything -compilation_section { print } - -# In test section, show everything - let user control logging with -D arguments -test_section { - print -} - -# Before test section starts, show important lines only -!test_section && scanning_started { - if (/INFO.*Scanning|INFO.*Building|INFO.*resources|INFO.*compiler|INFO.*surefire|ERROR|FAILURE/) { - print - } - # Show compilation warnings/errors - if (/WARNING.*COMPILATION|ERROR.*/) { - print - } -} -' \ No newline at end of file diff --git a/json-java21-schema/pom.xml b/json-java21-schema/pom.xml index e196628..043a720 100644 --- a/json-java21-schema/pom.xml +++ b/json-java21-schema/pom.xml @@ -46,6 +46,11 @@ junit-jupiter-engine test + + org.junit.jupiter + junit-jupiter-params + test + org.assertj assertj-core @@ -59,6 +64,14 @@ 2.15.0 test + + + net.jqwik + jqwik + 1.9.3 + test + + @@ -92,7 +105,7 @@ fetch-json-schema-suite - generate-test-resources + pre-integration-test run diff --git a/json-java21-schema/src/main/java/io/github/simbo1905/json/schema/JsonSchema.java b/json-java21-schema/src/main/java/io/github/simbo1905/json/schema/JsonSchema.java index e8f84d0..461f0cc 100644 --- a/json-java21-schema/src/main/java/io/github/simbo1905/json/schema/JsonSchema.java +++ b/json-java21-schema/src/main/java/io/github/simbo1905/json/schema/JsonSchema.java @@ -422,6 +422,7 @@ static CompiledRegistry compileWorkStack(JsonValue initialJson, Deque workStack = new ArrayDeque<>(); Map built = new NormalizedUriMap(new LinkedHashMap<>()); Set active = new HashSet<>(); + Map parentMap = new HashMap<>(); LOG.finest(() -> "compileWorkStack: initialized workStack=" + workStack + ", built=" + built + ", active=" + active); @@ -481,7 +482,7 @@ static CompiledRegistry compileWorkStack(JsonValue initialJson, if (refToken instanceof RefToken.RemoteRef remoteRef) { LOG.finest(() -> "compileWorkStack: processing RemoteRef object=" + remoteRef + ", base=" + remoteRef.baseUri() + ", target=" + remoteRef.targetUri()); java.net.URI targetDocUri = normalizeUri(finalCurrentUri, remoteRef.targetUri().toString()); - boolean scheduled = scheduleRemoteIfUnseen(finalWorkStack, finalBuilt, targetDocUri); + boolean scheduled = scheduleRemoteIfUnseen(finalWorkStack, finalBuilt, parentMap, finalCurrentUri, targetDocUri); LOG.finer(() -> "compileWorkStack: remote ref scheduled=" + scheduled + ", target=" + targetDocUri); } }, built, options, compileOptions); @@ -650,12 +651,23 @@ public String pointer() { } /// Schedule remote document for compilation if not seen before - static boolean scheduleRemoteIfUnseen(Deque workStack, Map built, java.net.URI targetDocUri) { + static boolean scheduleRemoteIfUnseen(Deque workStack, + Map built, + Map parentMap, + java.net.URI currentDocUri, + java.net.URI targetDocUri) { LOG.finer(() -> "scheduleRemoteIfUnseen: target=" + targetDocUri + ", workStack.size=" + workStack.size() + ", built.size=" + built.size()); LOG.finest(() -> "scheduleRemoteIfUnseen: targetDocUri object=" + targetDocUri + ", scheme=" + targetDocUri.getScheme() + ", host=" + targetDocUri.getHost() + ", path=" + targetDocUri.getPath()); LOG.finest(() -> "scheduleRemoteIfUnseen: workStack object=" + workStack + ", contents=" + workStack.stream().map(Object::toString).collect(java.util.stream.Collectors.joining(", ", "[", "]"))); LOG.finest(() -> "scheduleRemoteIfUnseen: built map object=" + built + ", keys=" + built.keySet() + ", size=" + built.size()); + // Detect remote cycles by walking parent chain + if (formsRemoteCycle(parentMap, currentDocUri, targetDocUri)) { + String cycleMessage = "ERROR: CYCLE: remote $ref cycle current=" + currentDocUri + ", target=" + targetDocUri; + LOG.severe(() -> cycleMessage); + throw new IllegalArgumentException(cycleMessage); + } + // Check if already built or already in work stack boolean alreadyBuilt = built.containsKey(targetDocUri); boolean inWorkStack = workStack.contains(targetDocUri); @@ -667,6 +679,9 @@ static boolean scheduleRemoteIfUnseen(Deque workStack, Map "scheduleRemoteIfUnseen: scheduled remote document: " + targetDocUri); @@ -674,6 +689,27 @@ static boolean scheduleRemoteIfUnseen(Deque workStack, Map parentMap, + java.net.URI currentDocUri, + java.net.URI targetDocUri) { + if (currentDocUri.equals(targetDocUri)) { + return true; + } + + java.net.URI cursor = currentDocUri; + while (cursor != null) { + java.net.URI parent = parentMap.get(cursor); + if (parent == null) { + break; + } + if (parent.equals(targetDocUri)) { + return true; + } + cursor = parent; + } + return false; + } + /// Detect and throw on compile-time cycles static void detectAndThrowCycle(Set active, java.net.URI docUri, String pathTrail) { LOG.finest(() -> "detectAndThrowCycle: active set=" + active + ", docUri=" + docUri + ", pathTrail='" + pathTrail + "'"); @@ -1464,6 +1500,7 @@ private static final class Session { final Map definitions = new LinkedHashMap<>(); final Map compiledByPointer = new LinkedHashMap<>(); final Map rawByPointer = new LinkedHashMap<>(); + final Map parentMap = new LinkedHashMap<>(); JsonSchema currentRootSchema; Options currentOptions; long totalFetchedBytes; @@ -1473,7 +1510,8 @@ private static final class Session { private static java.net.URI stripFragment(java.net.URI uri) { String s = uri.toString(); int i = s.indexOf('#'); - return i >= 0 ? java.net.URI.create(s.substring(0, i)) : uri; + java.net.URI base = i >= 0 ? java.net.URI.create(s.substring(0, i)) : uri; + return base.normalize(); } // removed static mutable state; state now lives in Session @@ -1953,13 +1991,26 @@ private static JsonSchema compileInternalWithContext(Session session, JsonValue // Handle remote refs by adding to work stack if (refToken instanceof RefToken.RemoteRef remoteRef) { LOG.finer(() -> "Remote ref detected: " + remoteRef.targetUri()); - // Get document URI without fragment java.net.URI targetDocUri = stripFragment(remoteRef.targetUri()); - if (!seenUris.contains(targetDocUri)) { + LOG.fine(() -> "Remote ref scheduling from docUri=" + docUri + " to target=" + targetDocUri); + LOG.finest(() -> "Remote ref parentMap before cycle check: " + session.parentMap); + if (formsRemoteCycle(session.parentMap, docUri, targetDocUri)) { + String cycleMessage = "ERROR: CYCLE: remote $ref cycle current=" + docUri + ", target=" + targetDocUri; + LOG.severe(() -> cycleMessage); + throw new IllegalArgumentException(cycleMessage); + } + boolean alreadySeen = seenUris.contains(targetDocUri); + LOG.finest(() -> "Remote ref alreadySeen=" + alreadySeen + " for target=" + targetDocUri); + if (!alreadySeen) { workStack.push(new WorkItem(targetDocUri)); seenUris.add(targetDocUri); + session.parentMap.putIfAbsent(targetDocUri, docUri); LOG.finer(() -> "Added to work stack: " + targetDocUri); + } else { + session.parentMap.putIfAbsent(targetDocUri, docUri); + LOG.finer(() -> "Remote ref already scheduled or compiled: " + targetDocUri); } + LOG.finest(() -> "Remote ref parentMap after scheduling: " + session.parentMap); LOG.finest(() -> "compileInternalWithContext: Creating RefSchema for remote ref " + remoteRef.targetUri()); LOG.fine(() -> "Creating RefSchema for remote ref " + remoteRef.targetUri() + diff --git a/json-java21-schema/src/test/java/io/github/simbo1905/json/schema/JsonSamples.java b/json-java21-schema/src/test/java/io/github/simbo1905/json/schema/JsonSamples.java new file mode 100644 index 0000000..4cdffa6 --- /dev/null +++ b/json-java21-schema/src/test/java/io/github/simbo1905/json/schema/JsonSamples.java @@ -0,0 +1,34 @@ +package io.github.simbo1905.json.schema; + +import org.junit.jupiter.params.provider.Arguments; + +import java.util.stream.Stream; + +final class JsonSamples { + private JsonSamples() {} + + static Stream simpleTypes() { + return Stream.of( + Arguments.of("string", "\"hello\"", true), + Arguments.of("string", "42", false), + Arguments.of("number", "42", true), + Arguments.of("number", "3.14", true), + Arguments.of("number", "\"x\"", false), + Arguments.of("boolean", "true", true), + Arguments.of("boolean", "false", true), + Arguments.of("boolean", "1", false), + Arguments.of("null", "null", true), + Arguments.of("null", "\"null\"", false) + ); + } + + static Stream patterns() { + return Stream.of( + Arguments.of("^abc$", "\"abc\"", true), + Arguments.of("^abc$", "\"ab\"", false), + Arguments.of("^[0-9]+$", "\"123\"", true), + Arguments.of("^[0-9]+$", "\"12a\"", false) + ); + } +} + diff --git a/json-java21-schema/src/test/java/io/github/simbo1905/json/schema/JsonSchemaAnnotationsTest.java b/json-java21-schema/src/test/java/io/github/simbo1905/json/schema/JsonSchemaAnnotationsTest.java index 7c826db..943c57d 100644 --- a/json-java21-schema/src/test/java/io/github/simbo1905/json/schema/JsonSchemaAnnotationsTest.java +++ b/json-java21-schema/src/test/java/io/github/simbo1905/json/schema/JsonSchemaAnnotationsTest.java @@ -8,7 +8,7 @@ /// Covers annotation-only keywords from JSON Schema such as /// title, description, $comment, and examples. These MUST NOT /// affect validation (they are informational). -class JsonSchemaAnnotationsTest extends JsonSchemaLoggingConfig { +class JsonSchemaAnnotationsTest extends JsonSchemaTestBase { @Test void examplesDoNotAffectValidation() { @@ -69,4 +69,3 @@ void unknownAnnotationKeywordsAreIgnored() { assertThat(schema.validate(Json.parse("123"))).extracting("valid").isEqualTo(false); } } - diff --git a/json-java21-schema/src/test/java/io/github/simbo1905/json/schema/JsonSchemaArrayKeywordsTest.java b/json-java21-schema/src/test/java/io/github/simbo1905/json/schema/JsonSchemaArrayKeywordsTest.java index 13bf277..5993043 100644 --- a/json-java21-schema/src/test/java/io/github/simbo1905/json/schema/JsonSchemaArrayKeywordsTest.java +++ b/json-java21-schema/src/test/java/io/github/simbo1905/json/schema/JsonSchemaArrayKeywordsTest.java @@ -4,7 +4,7 @@ import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.*; -class JsonSchemaArrayKeywordsTest extends JsonSchemaLoggingConfig { +class JsonSchemaArrayKeywordsTest extends JsonSchemaTestBase { @Test void testContains_only_defaults() { @@ -361,4 +361,4 @@ void testContains_minContainsZero() { /// Valid: mixed with booleans assertThat(schema.validate(Json.parse("[1,true,2]")).valid()).isTrue(); } -} \ No newline at end of file +} diff --git a/json-java21-schema/src/test/java/io/github/simbo1905/json/schema/JsonSchemaDependenciesAndOneOfTest.java b/json-java21-schema/src/test/java/io/github/simbo1905/json/schema/JsonSchemaDependenciesAndOneOfTest.java index d548ad7..e39f75a 100644 --- a/json-java21-schema/src/test/java/io/github/simbo1905/json/schema/JsonSchemaDependenciesAndOneOfTest.java +++ b/json-java21-schema/src/test/java/io/github/simbo1905/json/schema/JsonSchemaDependenciesAndOneOfTest.java @@ -5,7 +5,7 @@ import static org.assertj.core.api.Assertions.*; -class JsonSchemaDependenciesAndOneOfTest extends JsonSchemaLoggingConfig { +class JsonSchemaDependenciesAndOneOfTest extends JsonSchemaTestBase { @Test void testDependentRequiredBasics() { @@ -302,4 +302,4 @@ void testComplexDependenciesAndOneOf() { assertThat(missingRouting.valid()).isFalse(); assertThat(missingRouting.errors().getFirst().message()).contains("Property 'accountNumber' requires property 'routingNumber' (dependentRequired)"); } -} \ No newline at end of file +} diff --git a/json-java21-schema/src/test/java/io/github/simbo1905/json/schema/JsonSchemaErrorMessagesTest.java b/json-java21-schema/src/test/java/io/github/simbo1905/json/schema/JsonSchemaErrorMessagesTest.java index 1b8d5ed..ce89113 100644 --- a/json-java21-schema/src/test/java/io/github/simbo1905/json/schema/JsonSchemaErrorMessagesTest.java +++ b/json-java21-schema/src/test/java/io/github/simbo1905/json/schema/JsonSchemaErrorMessagesTest.java @@ -5,7 +5,7 @@ import static org.assertj.core.api.Assertions.*; -class JsonSchemaErrorMessagesTest extends JsonSchemaLoggingConfig { +class JsonSchemaErrorMessagesTest extends JsonSchemaTestBase { @Test void typeMismatchMessages() { diff --git a/json-java21-schema/src/test/java/io/github/simbo1905/json/schema/JsonSchemaFormatTest.java b/json-java21-schema/src/test/java/io/github/simbo1905/json/schema/JsonSchemaFormatTest.java index f7fcbcf..f99bcb5 100644 --- a/json-java21-schema/src/test/java/io/github/simbo1905/json/schema/JsonSchemaFormatTest.java +++ b/json-java21-schema/src/test/java/io/github/simbo1905/json/schema/JsonSchemaFormatTest.java @@ -8,7 +8,7 @@ import static org.assertj.core.api.Assertions.*; -class JsonSchemaFormatTest extends JsonSchemaLoggingConfig { +class JsonSchemaFormatTest extends JsonSchemaTestBase { @Test void testCommonFormats_whenAssertionOn_invalidsFail_validsPass() { // Toggle "assert formats" ON (wire however your implementation exposes it). diff --git a/json-java21-schema/src/test/java/io/github/simbo1905/json/schema/JsonSchemaLoggingConfig.java b/json-java21-schema/src/test/java/io/github/simbo1905/json/schema/JsonSchemaLoggingConfig.java index 148eb19..26922c4 100644 --- a/json-java21-schema/src/test/java/io/github/simbo1905/json/schema/JsonSchemaLoggingConfig.java +++ b/json-java21-schema/src/test/java/io/github/simbo1905/json/schema/JsonSchemaLoggingConfig.java @@ -17,7 +17,7 @@ static void enableJulDebug() { try { targetLevel = Level.parse(levelProp.trim().toUpperCase(Locale.ROOT)); } catch (IllegalArgumentException ignored) { - targetLevel = Level.INFO; + System.err.println("Unrecognized logging level from 'java.util.logging.ConsoleHandler.level': " + levelProp); } } } @@ -41,4 +41,5 @@ static void enableJulDebug() { () -> "json.schema.test.resources set to " + base); } } + } diff --git a/json-java21-schema/src/test/java/io/github/simbo1905/json/schema/JsonSchemaObjectKeywordsTest.java b/json-java21-schema/src/test/java/io/github/simbo1905/json/schema/JsonSchemaObjectKeywordsTest.java index 1601b24..c9fa4fd 100644 --- a/json-java21-schema/src/test/java/io/github/simbo1905/json/schema/JsonSchemaObjectKeywordsTest.java +++ b/json-java21-schema/src/test/java/io/github/simbo1905/json/schema/JsonSchemaObjectKeywordsTest.java @@ -5,7 +5,7 @@ import static org.assertj.core.api.Assertions.*; -class JsonSchemaObjectKeywordsTest extends JsonSchemaLoggingConfig { +class JsonSchemaObjectKeywordsTest extends JsonSchemaTestBase { @Test void additionalPropertiesFalseDisallowsUnknown() { diff --git a/json-java21-schema/src/test/java/io/github/simbo1905/json/schema/JsonSchemaPatternParamTest.java b/json-java21-schema/src/test/java/io/github/simbo1905/json/schema/JsonSchemaPatternParamTest.java new file mode 100644 index 0000000..218ec4e --- /dev/null +++ b/json-java21-schema/src/test/java/io/github/simbo1905/json/schema/JsonSchemaPatternParamTest.java @@ -0,0 +1,25 @@ +package io.github.simbo1905.json.schema; + +import jdk.sandbox.java.util.json.Json; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; + +class JsonSchemaPatternParamTest extends JsonSchemaTestBase { + + static Stream providePatterns() { return JsonSamples.patterns(); } + + @ParameterizedTest(name = "pattern={0} value={1} -> {2}") + @MethodSource("providePatterns") + void validatesPatterns(String pattern, String json, boolean expected) { + String schema = "{\"type\":\"string\",\"pattern\":\"" + pattern.replace("\\", "\\\\") + "\"}"; + var compiled = JsonSchema.compile(Json.parse(schema)); + var result = compiled.validate(Json.parse(json)); + assertThat(result.valid()).isEqualTo(expected); + } +} + diff --git a/json-java21-schema/src/test/java/io/github/simbo1905/json/schema/JsonSchemaPatternTest.java b/json-java21-schema/src/test/java/io/github/simbo1905/json/schema/JsonSchemaPatternTest.java index 48da182..9ff9b0d 100644 --- a/json-java21-schema/src/test/java/io/github/simbo1905/json/schema/JsonSchemaPatternTest.java +++ b/json-java21-schema/src/test/java/io/github/simbo1905/json/schema/JsonSchemaPatternTest.java @@ -4,7 +4,7 @@ import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.*; -class JsonSchemaPatternTest extends JsonSchemaLoggingConfig { +class JsonSchemaPatternTest extends JsonSchemaTestBase { @Test void testPattern_unanchored_singleChar_findVsMatches() { // Unanchored semantics: pattern "a" must validate any string that CONTAINS 'a', diff --git a/json-java21-schema/src/test/java/io/github/simbo1905/json/schema/JsonSchemaRefLocalTest.java b/json-java21-schema/src/test/java/io/github/simbo1905/json/schema/JsonSchemaRefLocalTest.java index cdba74a..fbaf072 100644 --- a/json-java21-schema/src/test/java/io/github/simbo1905/json/schema/JsonSchemaRefLocalTest.java +++ b/json-java21-schema/src/test/java/io/github/simbo1905/json/schema/JsonSchemaRefLocalTest.java @@ -14,7 +14,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; /// Test local reference resolution for JSON Schema 2020-12 -class JsonSchemaRefLocalTest extends JsonSchemaLoggingConfig { +class JsonSchemaRefLocalTest extends JsonSchemaTestBase { @Test void testRootReference() { diff --git a/json-java21-schema/src/test/java/io/github/simbo1905/json/schema/JsonSchemaRemoteRefTest.java b/json-java21-schema/src/test/java/io/github/simbo1905/json/schema/JsonSchemaRemoteRefTest.java index 9e3c45d..136d58f 100644 --- a/json-java21-schema/src/test/java/io/github/simbo1905/json/schema/JsonSchemaRemoteRefTest.java +++ b/json-java21-schema/src/test/java/io/github/simbo1905/json/schema/JsonSchemaRemoteRefTest.java @@ -16,7 +16,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -final class JsonSchemaRemoteRefTest extends JsonSchemaLoggingConfig { +final class JsonSchemaRemoteRefTest extends JsonSchemaTestBase { @Test void resolves_http_ref_to_pointer_inside_remote_doc() { @@ -295,18 +295,18 @@ void detects_cross_document_cycle() { )); final var options = JsonSchema.CompileOptions.remoteDefaults(fetcher); - LOG.finer(() -> "Compiling schema expecting cycle resolution"); - final var schema = JsonSchema.compile( - toJson(""" - {"$ref":"file:///JsonSchemaRemoteRefTest/a.json"} - """), - JsonSchema.Options.DEFAULT, - options - ); - - final var result = schema.validate(toJson("true")); - logResult("validate-true", result); - assertThat(result.valid()).isTrue(); + LOG.finer(() -> "Compiling schema expecting cycle detection"); + try (CapturedLogs logs = captureLogs(java.util.logging.Level.SEVERE)) { + assertThatThrownBy(() -> JsonSchema.compile( + toJson(""" + {"$ref":"file:///JsonSchemaRemoteRefTest/a.json"} + """), + JsonSchema.Options.DEFAULT, + options + )).isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("ERROR: CYCLE: remote $ref cycle"); + assertThat(logs.lines().stream().anyMatch(line -> line.startsWith("ERROR: CYCLE:"))).isTrue(); + } } @Test diff --git a/json-java21-schema/src/test/java/io/github/simbo1905/json/schema/JsonSchemaRemoteServerRefTest.java b/json-java21-schema/src/test/java/io/github/simbo1905/json/schema/JsonSchemaRemoteServerRefTest.java new file mode 100644 index 0000000..3c34228 --- /dev/null +++ b/json-java21-schema/src/test/java/io/github/simbo1905/json/schema/JsonSchemaRemoteServerRefTest.java @@ -0,0 +1,40 @@ +package io.github.simbo1905.json.schema; + +import jdk.sandbox.java.util.json.Json; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class JsonSchemaRemoteServerRefTest extends JsonSchemaTestBase { + + @RegisterExtension + static final RemoteSchemaServerRule SERVER = new RemoteSchemaServerRule(); + + @Test + void resolves_pointer_inside_remote_doc_via_http() { + var policy = JsonSchema.FetchPolicy.defaults().withAllowedSchemes(Set.of("http","https")); + var options = JsonSchema.CompileOptions.remoteDefaults(new VirtualThreadHttpFetcher()).withFetchPolicy(policy); + var schema = Json.parse("{\"$ref\":\"" + SERVER.url("/a.json") + "#/$defs/X\"}"); + var compiled = JsonSchema.compile(schema, JsonSchema.Options.DEFAULT, options); + assertThat(compiled.validate(Json.parse("1")).valid()).isTrue(); + assertThat(compiled.validate(Json.parse("0")).valid()).isFalse(); + } + + @Test + void remote_cycle_handles_gracefully() { + var policy = JsonSchema.FetchPolicy.defaults().withAllowedSchemes(Set.of("http","https")); + var options = JsonSchema.CompileOptions.remoteDefaults(new VirtualThreadHttpFetcher()).withFetchPolicy(policy); + + // Compilation should succeed despite the cycle + var compiled = JsonSchema.compile(Json.parse("{\"$ref\":\"" + SERVER.url("/cycle1.json") + "#\"}"), JsonSchema.Options.DEFAULT, options); + + // Validation should succeed by gracefully handling the cycle + var result = compiled.validate(Json.parse("\"test\"")); + assertThat(result.valid()).isTrue(); + } +} + diff --git a/json-java21-schema/src/test/java/io/github/simbo1905/json/schema/JsonSchemaTest.java b/json-java21-schema/src/test/java/io/github/simbo1905/json/schema/JsonSchemaTest.java index 04e006f..a3de1b0 100644 --- a/json-java21-schema/src/test/java/io/github/simbo1905/json/schema/JsonSchemaTest.java +++ b/json-java21-schema/src/test/java/io/github/simbo1905/json/schema/JsonSchemaTest.java @@ -4,7 +4,7 @@ import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.*; -class JsonSchemaTest extends JsonSchemaLoggingConfig { +class JsonSchemaTest extends JsonSchemaTestBase { @Test void testStringTypeValidation() { diff --git a/json-java21-schema/src/test/java/io/github/simbo1905/json/schema/JsonSchemaTestBase.java b/json-java21-schema/src/test/java/io/github/simbo1905/json/schema/JsonSchemaTestBase.java new file mode 100644 index 0000000..7bda607 --- /dev/null +++ b/json-java21-schema/src/test/java/io/github/simbo1905/json/schema/JsonSchemaTestBase.java @@ -0,0 +1,98 @@ +package io.github.simbo1905.json.schema; + +import jdk.sandbox.java.util.json.Json; +import jdk.sandbox.java.util.json.JsonValue; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.TestInfo; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import static io.github.simbo1905.json.schema.SchemaLogging.LOG; + +/// Base class for all schema tests. +/// - Emits an INFO banner per test. +/// - Provides common helpers for loading resources and assertions. +class JsonSchemaTestBase extends JsonSchemaLoggingConfig { + + @BeforeEach + void announce(TestInfo testInfo) { + final String cls = testInfo.getTestClass().map(Class::getSimpleName).orElse("UnknownTest"); + final String name = testInfo.getTestMethod().map(java.lang.reflect.Method::getName) + .orElseGet(testInfo::getDisplayName); + LOG.info(() -> "TEST: " + cls + "#" + name); + } + + protected final JsonValue readJson(String resourcePath) { + return Json.parse(readText(resourcePath)); + } + + protected final String readText(String resourcePath) { + try { + Path p = Path.of(Objects.requireNonNull( + getClass().getClassLoader().getResource(resourcePath), resourcePath + ).toURI()); + return Files.readString(p, StandardCharsets.UTF_8); + } catch (URISyntaxException | IOException e) { + throw new RuntimeException("Failed to read resource: " + resourcePath, e); + } + } + + protected final URI uriOf(String relativeResourcePath) { + return TestResourceUtils.getTestResourceUri(relativeResourcePath); + } + + protected final JsonSchema.ValidationResult validate(JsonSchema schema, JsonValue instance) { + return schema.validate(instance); + } + + protected final void assertValid(JsonSchema schema, String instanceJson) { + final var res = schema.validate(Json.parse(instanceJson)); + org.assertj.core.api.Assertions.assertThat(res.valid()).isTrue(); + } + + protected final void assertInvalid(JsonSchema schema, String instanceJson) { + final var res = schema.validate(Json.parse(instanceJson)); + org.assertj.core.api.Assertions.assertThat(res.valid()).isFalse(); + } + + protected static CapturedLogs captureLogs(java.util.logging.Level level) { + return new CapturedLogs(level); + } + + static final class CapturedLogs implements AutoCloseable { + private final java.util.logging.Handler handler; + private final List lines = new ArrayList<>(); + private final java.util.logging.Level original; + + CapturedLogs(java.util.logging.Level level) { + original = LOG.getLevel(); + LOG.setLevel(level); + handler = new java.util.logging.Handler() { + @Override public void publish(java.util.logging.LogRecord record) { + if (record.getLevel().intValue() >= level.intValue()) { + lines.add(record.getMessage()); + } + } + @Override public void flush() { } + @Override public void close() throws SecurityException { } + }; + LOG.addHandler(handler); + } + + List lines() { return List.copyOf(lines); } + + @Override + public void close() { + LOG.removeHandler(handler); + LOG.setLevel(original); + } + } +} diff --git a/json-java21-schema/src/test/java/io/github/simbo1905/json/schema/JsonSchemaTypeAndEnumParamTest.java b/json-java21-schema/src/test/java/io/github/simbo1905/json/schema/JsonSchemaTypeAndEnumParamTest.java new file mode 100644 index 0000000..f84a790 --- /dev/null +++ b/json-java21-schema/src/test/java/io/github/simbo1905/json/schema/JsonSchemaTypeAndEnumParamTest.java @@ -0,0 +1,25 @@ +package io.github.simbo1905.json.schema; + +import jdk.sandbox.java.util.json.Json; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; + +class JsonSchemaTypeAndEnumParamTest extends JsonSchemaTestBase { + + static Stream provideSimpleTypes() { return JsonSamples.simpleTypes(); } + + @ParameterizedTest(name = "type={0} value={1} -> {2}") + @MethodSource("provideSimpleTypes") + void validatesSimpleTypes(String type, String json, boolean expected) { + String schema = "{\"type\":\"" + type + "\"}"; + var compiled = JsonSchema.compile(Json.parse(schema)); + var result = compiled.validate(Json.parse(json)); + assertThat(result.valid()).isEqualTo(expected); + } +} + diff --git a/json-java21-schema/src/test/java/io/github/simbo1905/json/schema/JsonSchemaTypeAndEnumTest.java b/json-java21-schema/src/test/java/io/github/simbo1905/json/schema/JsonSchemaTypeAndEnumTest.java index 7b48639..6c5dbc3 100644 --- a/json-java21-schema/src/test/java/io/github/simbo1905/json/schema/JsonSchemaTypeAndEnumTest.java +++ b/json-java21-schema/src/test/java/io/github/simbo1905/json/schema/JsonSchemaTypeAndEnumTest.java @@ -4,7 +4,7 @@ import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.*; -class JsonSchemaTypeAndEnumTest extends JsonSchemaLoggingConfig { +class JsonSchemaTypeAndEnumTest extends JsonSchemaTestBase { @Test void testEnum_strict_noTypeCoercion_edgeCases() { diff --git a/json-java21-schema/src/test/java/io/github/simbo1905/json/schema/OpenRPCCompileOnlyTest.java b/json-java21-schema/src/test/java/io/github/simbo1905/json/schema/OpenRPCCompileOnlyTest.java new file mode 100644 index 0000000..6cb7e34 --- /dev/null +++ b/json-java21-schema/src/test/java/io/github/simbo1905/json/schema/OpenRPCCompileOnlyTest.java @@ -0,0 +1,60 @@ +package io.github.simbo1905.json.schema; + +import jdk.sandbox.java.util.json.Json; +import jdk.sandbox.java.util.json.JsonValue; +import org.junit.jupiter.api.Test; + +import java.net.URI; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/// Compile-only posture: deny all remote fetches to reveal which fragments +/// compile locally. This is a unit-level gate prior to the full OpenRPC IT. +class OpenRPCCompileOnlyTest extends JsonSchemaLoggingConfig { + + @Test + void compile_local_fragment_succeeds_with_remote_denied() { + final var fragment = "{" + + "\"$defs\":{\"X\":{\"type\":\"integer\"}}," + + "\"$ref\":\"#/$defs/X\"" + + "}"; + + final var fetcher = new MapRemoteFetcher(Map.of()); + final var policy = JsonSchema.FetchPolicy.defaults().withAllowedSchemes(Set.of("file")); + final var options = JsonSchema.CompileOptions.remoteDefaults(fetcher).withFetchPolicy(policy); + + final var schema = JsonSchema.compile(Json.parse(fragment), JsonSchema.Options.DEFAULT, options); + assertThat(schema.validate(Json.parse("1")).valid()).isTrue(); + assertThat(schema.validate(Json.parse("\"x\""))).extracting("valid").isEqualTo(false); + } + + @Test + void compile_remote_ref_is_denied_by_policy() { + final var fragment = "{" + + "\"$ref\":\"http://example.com/openrpc.json#/$defs/X\"" + + "}"; + + final var fetcher = new MapRemoteFetcher(Map.of()); + final var policy = JsonSchema.FetchPolicy.defaults().withAllowedSchemes(Set.of("file")); + final var options = JsonSchema.CompileOptions.remoteDefaults(fetcher).withFetchPolicy(policy); + + assertThatThrownBy(() -> JsonSchema.compile(Json.parse(fragment), JsonSchema.Options.DEFAULT, options)) + .isInstanceOf(JsonSchema.RemoteResolutionException.class) + .hasFieldOrPropertyWithValue("reason", JsonSchema.RemoteResolutionException.Reason.POLICY_DENIED) + .hasMessageContaining("http://example.com/openrpc.json"); + } + + private static final class MapRemoteFetcher implements JsonSchema.RemoteFetcher { + private final Map documents; + private MapRemoteFetcher(Map documents) { this.documents = Map.copyOf(documents); } + @Override public FetchResult fetch(URI uri, JsonSchema.FetchPolicy policy) { + throw new JsonSchema.RemoteResolutionException(uri, + JsonSchema.RemoteResolutionException.Reason.NOT_FOUND, + "No remote document registered for " + uri); + } + } +} diff --git a/json-java21-schema/src/test/java/io/github/simbo1905/json/schema/OpenRPCFragmentsUnitTest.java b/json-java21-schema/src/test/java/io/github/simbo1905/json/schema/OpenRPCFragmentsUnitTest.java new file mode 100644 index 0000000..c6417d8 --- /dev/null +++ b/json-java21-schema/src/test/java/io/github/simbo1905/json/schema/OpenRPCFragmentsUnitTest.java @@ -0,0 +1,120 @@ +package io.github.simbo1905.json.schema; + +import jdk.sandbox.java.util.json.Json; +import org.junit.jupiter.api.Test; + +import static io.github.simbo1905.json.schema.SchemaLogging.LOG; +import static org.assertj.core.api.Assertions.assertThat; + +/// Unit tests that exercise OpenRPC-like schema fragments using only +/// keywords currently supported by the validator. These build confidence +/// incrementally before the larger IT that validates full documents. +class OpenRPCFragmentsUnitTest extends JsonSchemaTestBase { + + @Test + void info_object_minimal_required_fields() { + LOG.info(() -> "TEST: " + getClass().getSimpleName() + "#info_object_minimal_required_fields"); + final var schema = JsonSchema.compile(Json.parse( + "{" + + "\"type\":\"object\"," + + "\"required\":[\"title\",\"version\"]," + + "\"properties\":{\"title\":{\"type\":\"string\"},\"version\":{\"type\":\"string\"}}," + + "\"additionalProperties\":true" + + "}" + )); + + final var good = schema.validate(Json.parse("{\"title\":\"X\",\"version\":\"1.0\"}")); + assertThat(good.valid()).isTrue(); + + final var bad = schema.validate(Json.parse("{\"title\":\"X\"}")); + assertThat(bad.valid()).isFalse(); + } + + @Test + void method_object_requires_name_and_params() { + LOG.info(() -> "TEST: " + getClass().getSimpleName() + "#method_object_requires_name_and_params"); + final var schema = JsonSchema.compile(Json.parse(""" + { + "type":"object", + "required":["name","params"], + "properties":{ + "name":{"type":"string","minLength":1}, + "params":{"type":"array"} + }, + "additionalProperties": true + } + """)); + + final var ok = schema.validate(Json.parse("{\"name\":\"op\",\"params\":[]}")); + assertThat(ok.valid()).isTrue(); + + final var missing = schema.validate(Json.parse("{\"name\":\"op\"}")); + assertThat(missing.valid()).isFalse(); + } + + @Test + void openrpc_field_is_non_empty_string() { + final var schema = JsonSchema.compile(Json.parse( + "{" + + "\"type\":\"object\"," + + "\"required\":[\"openrpc\"]," + + "\"properties\":{\"openrpc\":{\"type\":\"string\",\"minLength\":1}}" + + "}" + )); + assertThat(schema.validate(Json.parse("{\"openrpc\":\"1.3.0\"}"))).extracting("valid").isEqualTo(true); + assertThat(schema.validate(Json.parse("{\"openrpc\":\"\"}"))).extracting("valid").isEqualTo(false); + assertThat(schema.validate(Json.parse("{\"openrpc\":1}"))).extracting("valid").isEqualTo(false); + } + + @Test + void servers_array_items_are_objects() { + final var schema = JsonSchema.compile(Json.parse( + "{" + + "\"type\":\"object\"," + + "\"properties\":{\"servers\":{\"type\":\"array\",\"items\":{\"type\":\"object\"}}}" + + "}" + )); + assertThat(schema.validate(Json.parse("{\"servers\":[{}]}"))).extracting("valid").isEqualTo(true); + assertThat(schema.validate(Json.parse("{\"servers\":[1,2,3]}"))).extracting("valid").isEqualTo(false); + } + + @Test + void components_object_accepts_any_members() { + final var schema = JsonSchema.compile(Json.parse( + "{" + + "\"type\":\"object\"," + + "\"properties\":{\"components\":{\"type\":\"object\"}}," + + "\"additionalProperties\":true" + + "}" + )); + assertThat(schema.validate(Json.parse("{\"components\":{\"x\":1}}"))).extracting("valid").isEqualTo(true); + assertThat(schema.validate(Json.parse("{\"components\":1}"))).extracting("valid").isEqualTo(false); + } + + @Test + void param_object_requires_name_and_allows_schema_object() { + LOG.info(() -> "TEST: " + getClass().getSimpleName() + "#param_object_requires_name_and_allows_schema_object"); + final var schema = JsonSchema.compile(Json.parse( + "{" + + "\"type\":\"object\"," + + "\"required\":[\"name\"]," + + "\"properties\":{\"name\":{\"type\":\"string\",\"minLength\":1},\"schema\":{\"type\":\"object\"}}," + + "\"additionalProperties\":true" + + "}" + )); + + final var ok = schema.validate(Json.parse("{\"name\":\"n\",\"schema\":{}}")); + assertThat(ok.valid()).isTrue(); + + final var bad = schema.validate(Json.parse("{\"schema\":{}}")); + assertThat(bad.valid()).isFalse(); + } + + @Test + void uri_format_example_as_used_by_openrpc_examples() { + LOG.info(() -> "TEST: " + getClass().getSimpleName() + "#uri_format_example_as_used_by_openrpc_examples"); + final var schema = JsonSchema.compile(Json.parse("{\"type\":\"string\",\"format\":\"uri\"}")); + + assertThat(schema.validate(Json.parse("\"https://open-rpc.org\""))).extracting("valid").isEqualTo(true); + } +} diff --git a/json-java21-schema/src/test/java/io/github/simbo1905/json/schema/OpenRPCSchemaValidationIT.java b/json-java21-schema/src/test/java/io/github/simbo1905/json/schema/OpenRPCSchemaValidationIT.java index 008c484..69dd487 100644 --- a/json-java21-schema/src/test/java/io/github/simbo1905/json/schema/OpenRPCSchemaValidationIT.java +++ b/json-java21-schema/src/test/java/io/github/simbo1905/json/schema/OpenRPCSchemaValidationIT.java @@ -16,13 +16,14 @@ import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; +import static io.github.simbo1905.json.schema.SchemaLogging.LOG; /// Integration tests: validate OpenRPC documents using a minimal embedded meta-schema. /// Resources: /// - Schema: src/test/resources/openrpc/schema.json /// - Examples: src/test/resources/openrpc/examples/*.json /// Files containing "-bad-" are intentionally invalid and must fail validation. -public class OpenRPCSchemaValidationIT { +class OpenRPCSchemaValidationIT extends JsonSchemaTestBase { private static String readResource(String name) throws IOException { try { @@ -35,31 +36,18 @@ private static String readResource(String name) throws IOException { @TestFactory Stream validateOpenRPCExamples() throws Exception { + LOG.info(() -> "TEST: " + getClass().getSimpleName() + "#validateOpenRPCExamples"); // Compile the minimal OpenRPC schema (self-contained, no remote $ref) - String schemaJson = readResource("openrpc/schema.json"); - JsonSchema schema = JsonSchema.compile(Json.parse(schemaJson)); + JsonSchema schema = OpenRPCTestSupport.loadOpenRpcSchema(); // Discover example files - URL dirUrl = Objects.requireNonNull(getClass().getClassLoader().getResource("openrpc/examples"), - "missing openrpc examples directory"); - Path dir = Path.of(dirUrl.toURI()); - - try (Stream files = Files.list(dir)) { - List jsons = files - .filter(p -> p.getFileName().toString().endsWith(".json")) - .sorted() - .toList(); - - assertThat(jsons).isNotEmpty(); - - return jsons.stream().map(path -> DynamicTest.dynamicTest(path.getFileName().toString(), () -> { - String doc = Files.readString(path, StandardCharsets.UTF_8); - boolean expectedValid = !path.getFileName().toString().contains("-bad-"); - boolean actualValid = schema.validate(Json.parse(doc)).valid(); - Assertions.assertThat(actualValid) - .as("validation of %s", path.getFileName()) - .isEqualTo(expectedValid); - })); - } + List names = OpenRPCTestSupport.exampleNames(); + assertThat(names).isNotEmpty(); + return names.stream().map(name -> DynamicTest.dynamicTest(name, () -> { + LOG.info(() -> "TEST: " + getClass().getSimpleName() + "#" + name); + boolean expectedValid = !name.contains("-bad-"); + boolean actualValid = OpenRPCTestSupport.validateExample(name).valid(); + Assertions.assertThat(actualValid).as("validation of %s", name).isEqualTo(expectedValid); + })); } } diff --git a/json-java21-schema/src/test/java/io/github/simbo1905/json/schema/OpenRPCTestSupport.java b/json-java21-schema/src/test/java/io/github/simbo1905/json/schema/OpenRPCTestSupport.java new file mode 100644 index 0000000..1ec1055 --- /dev/null +++ b/json-java21-schema/src/test/java/io/github/simbo1905/json/schema/OpenRPCTestSupport.java @@ -0,0 +1,57 @@ +package io.github.simbo1905.json.schema; + +import jdk.sandbox.java.util.json.Json; +import jdk.sandbox.java.util.json.JsonValue; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; +import java.util.stream.Stream; + +final class OpenRPCTestSupport { + private OpenRPCTestSupport() {} + + static JsonSchema loadOpenRpcSchema() { + return JsonSchema.compile(readJson("openrpc/schema.json")); + } + + static JsonValue readJson(String resourcePath) { + return Json.parse(readText(resourcePath)); + } + + static String readText(String resourcePath) { + try { + URL url = Objects.requireNonNull(OpenRPCTestSupport.class.getClassLoader().getResource(resourcePath), resourcePath); + return Files.readString(Path.of(url.toURI()), StandardCharsets.UTF_8); + } catch (URISyntaxException | IOException e) { + throw new RuntimeException("Failed to read resource: " + resourcePath, e); + } + } + + static List exampleNames() { + try { + URL dirUrl = Objects.requireNonNull(OpenRPCTestSupport.class.getClassLoader().getResource("openrpc/examples"), "missing openrpc/examples directory"); + try (Stream s = Files.list(Path.of(dirUrl.toURI()))) { + return s.filter(p -> p.getFileName().toString().endsWith(".json")) + .map(p -> p.getFileName().toString()) + .sorted(Comparator.naturalOrder()) + .toList(); + } + } catch (Exception e) { + throw new RuntimeException("Failed to list openrpc examples", e); + } + } + + static JsonSchema.ValidationResult validateExample(String name) { + JsonSchema schema = loadOpenRpcSchema(); + JsonValue doc = readJson("openrpc/examples/" + name); + return schema.validate(doc); + } +} + diff --git a/json-java21-schema/src/test/java/io/github/simbo1905/json/schema/Pack1Pack2VerificationTest.java b/json-java21-schema/src/test/java/io/github/simbo1905/json/schema/Pack1Pack2VerificationTest.java index d3c0577..994d77f 100644 --- a/json-java21-schema/src/test/java/io/github/simbo1905/json/schema/Pack1Pack2VerificationTest.java +++ b/json-java21-schema/src/test/java/io/github/simbo1905/json/schema/Pack1Pack2VerificationTest.java @@ -5,7 +5,7 @@ import static org.assertj.core.api.Assertions.*; /// Verification test for Pack 1 and Pack 2 implementation completeness -class Pack1Pack2VerificationTest extends JsonSchemaLoggingConfig { +class Pack1Pack2VerificationTest extends JsonSchemaTestBase { @Test void testPatternSemantics_unanchoredFind() { @@ -243,4 +243,4 @@ void testPrefixItemsTupleValidation() { assertThat(prefixOnly.validate(Json.parse("[1,\"anything\",{},null]")).valid()).isTrue(); // prefix + any extras assertThat(prefixOnly.validate(Json.parse("[\"not integer\"]")).valid()).isFalse(); // wrong prefix type } -} \ No newline at end of file +} diff --git a/json-java21-schema/src/test/java/io/github/simbo1905/json/schema/RemoteSchemaServerRule.java b/json-java21-schema/src/test/java/io/github/simbo1905/json/schema/RemoteSchemaServerRule.java new file mode 100644 index 0000000..6af9101 --- /dev/null +++ b/json-java21-schema/src/test/java/io/github/simbo1905/json/schema/RemoteSchemaServerRule.java @@ -0,0 +1,60 @@ +package io.github.simbo1905.json.schema; + +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import com.sun.net.httpserver.HttpServer; +import org.junit.jupiter.api.extension.AfterAllCallback; +import org.junit.jupiter.api.extension.BeforeAllCallback; +import org.junit.jupiter.api.extension.ExtensionContext; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; +import java.util.Map; + +final class RemoteSchemaServerRule implements BeforeAllCallback, AfterAllCallback { + private HttpServer server; + private String host; + private int port; + + @Override + public void beforeAll(ExtensionContext context) throws Exception { + server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0); + host = "127.0.0.1"; + port = server.getAddress().getPort(); + + // Basic example document + addJson("/a.json", "{\n \"$id\": \"http://" + host + ":" + port + "/a.json\",\n \"$defs\": {\n \"X\": {\"type\": \"integer\", \"minimum\": 1},\n \"Y\": {\"type\": \"string\", \"minLength\": 1}\n }\n}\n"); + + // Simple second doc + addJson("/b.json", "{\n \"$id\": \"http://" + host + ":" + port + "/b.json\",\n \"type\": \"string\"\n}\n"); + + // 2-node cycle + addJson("/cycle1.json", "{\n \"$id\": \"http://" + host + ":" + port + "/cycle1.json\",\n \"$ref\": \"http://" + host + ":" + port + "/cycle2.json#\"\n}\n"); + addJson("/cycle2.json", "{\n \"$id\": \"http://" + host + ":" + port + "/cycle2.json\",\n \"$ref\": \"http://" + host + ":" + port + "/cycle1.json#\"\n}\n"); + + server.start(); + } + + private void addJson(String path, String body) { + server.createContext(path, new JsonResponder(body)); + } + + String url(String path) { return "http://" + host + ":" + port + path; } + + @Override + public void afterAll(ExtensionContext context) { + if (server != null) server.stop(0); + } + + private static final class JsonResponder implements HttpHandler { + private final byte[] bytes; + JsonResponder(String body) { this.bytes = body.getBytes(StandardCharsets.UTF_8); } + @Override public void handle(HttpExchange exchange) throws IOException { + exchange.getResponseHeaders().add("Content-Type", "application/schema+json"); + exchange.sendResponseHeaders(200, bytes.length); + try (OutputStream os = exchange.getResponseBody()) { os.write(bytes); } + } + } +} diff --git a/json-java21/src/test/java/jdk/sandbox/java/util/json/ReadmeDemoTests.java b/json-java21/src/test/java/jdk/sandbox/java/util/json/ReadmeDemoTests.java index 79cb926..ada8173 100644 --- a/json-java21/src/test/java/jdk/sandbox/java/util/json/ReadmeDemoTests.java +++ b/json-java21/src/test/java/jdk/sandbox/java/util/json/ReadmeDemoTests.java @@ -20,6 +20,9 @@ void quickStartExample() { JsonObject obj = (JsonObject) value; assertThat(((JsonString) obj.members().get("name")).value()).isEqualTo("Alice"); assertThat(((JsonNumber) obj.members().get("age")).toNumber()).isEqualTo(30L); + + String roundTrip = value.toString(); + assertThat(roundTrip).isEqualTo(jsonString); } // Domain model using records @@ -221,4 +224,4 @@ void displayFormattingExample() { assertThat(formatted).contains(" ]"); assertThat(formatted).contains("}"); } -} \ No newline at end of file +} diff --git a/logging.properties b/logging.properties new file mode 100644 index 0000000..de1ea3f --- /dev/null +++ b/logging.properties @@ -0,0 +1,5 @@ +.level=FINE +jdk.sandbox.compatibility.JsonTestSuiteSummary.level=FINE +handlers=java.util.logging.ConsoleHandler +java.util.logging.ConsoleHandler.level=FINE +java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter \ No newline at end of file diff --git a/mvn-test-no-boilerplate.sh b/mvn-test-no-boilerplate.sh deleted file mode 100755 index 2732d31..0000000 --- a/mvn-test-no-boilerplate.sh +++ /dev/null @@ -1,71 +0,0 @@ -#!/bin/bash - -# Strip Maven test boilerplate - show compile errors and test results only -# Usage: ./mvn-test-no-boilerplate.sh [maven test arguments] -# -# Examples: -# ./mvn-test-no-boilerplate.sh -Dtest=RefactorTests -# ./mvn-test-no-boilerplate.sh -Dtest=RefactorTests#testList -Djava.util.logging.ConsoleHandler.level=INFO -# ./mvn-test-no-boilerplate.sh -Dtest=RefactorTests#testList -Djava.util.logging.ConsoleHandler.level=FINER -# -# For running tests in a specific module: -# ./mvn-test-no-boilerplate.sh -pl json-java21-api-tracker -Dtest=CompilerApiLearningTest -# -# The script automatically detects if mvnd is available, otherwise falls back to mvn - -# Detect if mvnd is available, otherwise use mvn -if command -v mvnd &> /dev/null; then - MVN_CMD="mvnd" -else - MVN_CMD="mvn" -fi - -timeout 120 $MVN_CMD test "$@" 2>&1 | awk ' -BEGIN { - scanning_started = 0 - compilation_section = 0 - test_section = 0 -} - -# Skip all WARNING lines before project scanning starts -/INFO.*Scanning for projects/ { - scanning_started = 1 - print - next -} - -# Before scanning starts, skip WARNING lines -!scanning_started && /^WARNING:/ { next } - -# Show compilation errors -/COMPILATION ERROR/ { compilation_section = 1 } -/BUILD FAILURE/ && compilation_section { compilation_section = 0 } - -# Show test section -/INFO.*T E S T S/ { - test_section = 1 - print "-------------------------------------------------------" - print " T E S T S" - print "-------------------------------------------------------" - next -} - -# In compilation error section, show everything -compilation_section { print } - -# In test section, show everything - let user control logging with -D arguments -test_section { - print -} - -# Before test section starts, show important lines only -!test_section && scanning_started { - if (/INFO.*Scanning|INFO.*Building|INFO.*resources|INFO.*compiler|INFO.*surefire|ERROR|FAILURE/) { - print - } - # Show compilation warnings/errors - if (/WARNING.*COMPILATION|ERROR.*/) { - print - } -} -' \ No newline at end of file diff --git a/pom.xml b/pom.xml index 1ffd1a7..5134926 100644 --- a/pom.xml +++ b/pom.xml @@ -59,8 +59,6 @@ 1.7.1 - - @@ -75,6 +73,12 @@ ${junit.jupiter.version} test + + org.junit.jupiter + junit-jupiter-params + ${junit.jupiter.version} + test + org.assertj assertj-core