Skip to content

Commit 0f631b6

Browse files
committed
tests fixed
1 parent 9df1e4f commit 0f631b6

File tree

3 files changed

+70
-17
lines changed

3 files changed

+70
-17
lines changed

AGENTS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ $(command -v mvnd || command -v mvn || command -v ./mvnw) -Dtest=BlahTest#testSo
5959
$(command -v mvnd || command -v mvn || command -v ./mvnw) -pl json-java21-api-tracker -Dtest=ApiTrackerTest -Djava.util.logging.ConsoleHandler.level=FINE
6060
```
6161

62+
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.
63+
6264
### Output Visibility Requirements
6365

6466
- 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

json-java21-schema/src/main/java/io/github/simbo1905/json/schema/JsonSchema.java

Lines changed: 56 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,7 @@ static CompiledRegistry compileWorkStack(JsonValue initialJson,
422422
Deque<java.net.URI> workStack = new ArrayDeque<>();
423423
Map<java.net.URI, CompiledRoot> built = new NormalizedUriMap(new LinkedHashMap<>());
424424
Set<java.net.URI> active = new HashSet<>();
425+
Map<java.net.URI, java.net.URI> parentMap = new HashMap<>();
425426

426427
LOG.finest(() -> "compileWorkStack: initialized workStack=" + workStack + ", built=" + built + ", active=" + active);
427428

@@ -481,7 +482,7 @@ static CompiledRegistry compileWorkStack(JsonValue initialJson,
481482
if (refToken instanceof RefToken.RemoteRef remoteRef) {
482483
LOG.finest(() -> "compileWorkStack: processing RemoteRef object=" + remoteRef + ", base=" + remoteRef.baseUri() + ", target=" + remoteRef.targetUri());
483484
java.net.URI targetDocUri = normalizeUri(finalCurrentUri, remoteRef.targetUri().toString());
484-
boolean scheduled = scheduleRemoteIfUnseen(finalWorkStack, finalBuilt, targetDocUri);
485+
boolean scheduled = scheduleRemoteIfUnseen(finalWorkStack, finalBuilt, parentMap, finalCurrentUri, targetDocUri);
485486
LOG.finer(() -> "compileWorkStack: remote ref scheduled=" + scheduled + ", target=" + targetDocUri);
486487
}
487488
}, built, options, compileOptions);
@@ -650,12 +651,23 @@ public String pointer() {
650651
}
651652

652653
/// Schedule remote document for compilation if not seen before
653-
static boolean scheduleRemoteIfUnseen(Deque<java.net.URI> workStack, Map<java.net.URI, CompiledRoot> built, java.net.URI targetDocUri) {
654+
static boolean scheduleRemoteIfUnseen(Deque<java.net.URI> workStack,
655+
Map<java.net.URI, CompiledRoot> built,
656+
Map<java.net.URI, java.net.URI> parentMap,
657+
java.net.URI currentDocUri,
658+
java.net.URI targetDocUri) {
654659
LOG.finer(() -> "scheduleRemoteIfUnseen: target=" + targetDocUri + ", workStack.size=" + workStack.size() + ", built.size=" + built.size());
655660
LOG.finest(() -> "scheduleRemoteIfUnseen: targetDocUri object=" + targetDocUri + ", scheme=" + targetDocUri.getScheme() + ", host=" + targetDocUri.getHost() + ", path=" + targetDocUri.getPath());
656661
LOG.finest(() -> "scheduleRemoteIfUnseen: workStack object=" + workStack + ", contents=" + workStack.stream().map(Object::toString).collect(java.util.stream.Collectors.joining(", ", "[", "]")));
657662
LOG.finest(() -> "scheduleRemoteIfUnseen: built map object=" + built + ", keys=" + built.keySet() + ", size=" + built.size());
658663

664+
// Detect remote cycles by walking parent chain
665+
if (formsRemoteCycle(parentMap, currentDocUri, targetDocUri)) {
666+
String cycleMessage = "ERROR: CYCLE: remote $ref cycle current=" + currentDocUri + ", target=" + targetDocUri;
667+
LOG.severe(() -> cycleMessage);
668+
throw new IllegalArgumentException(cycleMessage);
669+
}
670+
659671
// Check if already built or already in work stack
660672
boolean alreadyBuilt = built.containsKey(targetDocUri);
661673
boolean inWorkStack = workStack.contains(targetDocUri);
@@ -667,13 +679,37 @@ static boolean scheduleRemoteIfUnseen(Deque<java.net.URI> workStack, Map<java.ne
667679
return false;
668680
}
669681

682+
// Track parent chain for cycle detection before scheduling child
683+
parentMap.putIfAbsent(targetDocUri, currentDocUri);
684+
670685
// Add to work stack
671686
workStack.push(targetDocUri);
672687
LOG.finer(() -> "scheduleRemoteIfUnseen: scheduled remote document: " + targetDocUri);
673688
LOG.finest(() -> "scheduleRemoteIfUnseen: workStack after push=" + workStack + ", contents=" + workStack.stream().map(Object::toString).collect(java.util.stream.Collectors.joining(", ", "[", "]")));
674689
return true;
675690
}
676691

692+
private static boolean formsRemoteCycle(Map<java.net.URI, java.net.URI> parentMap,
693+
java.net.URI currentDocUri,
694+
java.net.URI targetDocUri) {
695+
if (currentDocUri.equals(targetDocUri)) {
696+
return true;
697+
}
698+
699+
java.net.URI cursor = currentDocUri;
700+
while (cursor != null) {
701+
java.net.URI parent = parentMap.get(cursor);
702+
if (parent == null) {
703+
break;
704+
}
705+
if (parent.equals(targetDocUri)) {
706+
return true;
707+
}
708+
cursor = parent;
709+
}
710+
return false;
711+
}
712+
677713
/// Detect and throw on compile-time cycles
678714
static void detectAndThrowCycle(Set<java.net.URI> active, java.net.URI docUri, String pathTrail) {
679715
LOG.finest(() -> "detectAndThrowCycle: active set=" + active + ", docUri=" + docUri + ", pathTrail='" + pathTrail + "'");
@@ -1464,6 +1500,7 @@ private static final class Session {
14641500
final Map<String, JsonSchema> definitions = new LinkedHashMap<>();
14651501
final Map<String, JsonSchema> compiledByPointer = new LinkedHashMap<>();
14661502
final Map<String, JsonValue> rawByPointer = new LinkedHashMap<>();
1503+
final Map<java.net.URI, java.net.URI> parentMap = new LinkedHashMap<>();
14671504
JsonSchema currentRootSchema;
14681505
Options currentOptions;
14691506
long totalFetchedBytes;
@@ -1473,7 +1510,8 @@ private static final class Session {
14731510
private static java.net.URI stripFragment(java.net.URI uri) {
14741511
String s = uri.toString();
14751512
int i = s.indexOf('#');
1476-
return i >= 0 ? java.net.URI.create(s.substring(0, i)) : uri;
1513+
java.net.URI base = i >= 0 ? java.net.URI.create(s.substring(0, i)) : uri;
1514+
return base.normalize();
14771515
}
14781516
// removed static mutable state; state now lives in Session
14791517

@@ -1953,13 +1991,26 @@ private static JsonSchema compileInternalWithContext(Session session, JsonValue
19531991
// Handle remote refs by adding to work stack
19541992
if (refToken instanceof RefToken.RemoteRef remoteRef) {
19551993
LOG.finer(() -> "Remote ref detected: " + remoteRef.targetUri());
1956-
// Get document URI without fragment
19571994
java.net.URI targetDocUri = stripFragment(remoteRef.targetUri());
1958-
if (!seenUris.contains(targetDocUri)) {
1995+
LOG.fine(() -> "Remote ref scheduling from docUri=" + docUri + " to target=" + targetDocUri);
1996+
LOG.finest(() -> "Remote ref parentMap before cycle check: " + session.parentMap);
1997+
if (formsRemoteCycle(session.parentMap, docUri, targetDocUri)) {
1998+
String cycleMessage = "ERROR: CYCLE: remote $ref cycle current=" + docUri + ", target=" + targetDocUri;
1999+
LOG.severe(() -> cycleMessage);
2000+
throw new IllegalArgumentException(cycleMessage);
2001+
}
2002+
boolean alreadySeen = seenUris.contains(targetDocUri);
2003+
LOG.finest(() -> "Remote ref alreadySeen=" + alreadySeen + " for target=" + targetDocUri);
2004+
if (!alreadySeen) {
19592005
workStack.push(new WorkItem(targetDocUri));
19602006
seenUris.add(targetDocUri);
2007+
session.parentMap.putIfAbsent(targetDocUri, docUri);
19612008
LOG.finer(() -> "Added to work stack: " + targetDocUri);
2009+
} else {
2010+
session.parentMap.putIfAbsent(targetDocUri, docUri);
2011+
LOG.finer(() -> "Remote ref already scheduled or compiled: " + targetDocUri);
19622012
}
2013+
LOG.finest(() -> "Remote ref parentMap after scheduling: " + session.parentMap);
19632014
LOG.finest(() -> "compileInternalWithContext: Creating RefSchema for remote ref " + remoteRef.targetUri());
19642015

19652016
LOG.fine(() -> "Creating RefSchema for remote ref " + remoteRef.targetUri() +

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

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -295,18 +295,18 @@ void detects_cross_document_cycle() {
295295
));
296296
final var options = JsonSchema.CompileOptions.remoteDefaults(fetcher);
297297

298-
LOG.finer(() -> "Compiling schema expecting cycle resolution");
299-
final var schema = JsonSchema.compile(
300-
toJson("""
301-
{"$ref":"file:///JsonSchemaRemoteRefTest/a.json"}
302-
"""),
303-
JsonSchema.Options.DEFAULT,
304-
options
305-
);
306-
307-
final var result = schema.validate(toJson("true"));
308-
logResult("validate-true", result);
309-
assertThat(result.valid()).isTrue();
298+
LOG.finer(() -> "Compiling schema expecting cycle detection");
299+
try (CapturedLogs logs = captureLogs(java.util.logging.Level.SEVERE)) {
300+
assertThatThrownBy(() -> JsonSchema.compile(
301+
toJson("""
302+
{"$ref":"file:///JsonSchemaRemoteRefTest/a.json"}
303+
"""),
304+
JsonSchema.Options.DEFAULT,
305+
options
306+
)).isInstanceOf(IllegalArgumentException.class)
307+
.hasMessageContaining("ERROR: CYCLE: remote $ref cycle");
308+
assertThat(logs.lines().stream().anyMatch(line -> line.startsWith("ERROR: CYCLE:"))).isTrue();
309+
}
310310
}
311311

312312
@Test

0 commit comments

Comments
 (0)