Skip to content

Commit 4c05919

Browse files
authored
Issue #153 treat upstream fetch failure as drift; Issue #154 refresh sync tooling
Squash of PR #155. Closes #153 and #154. Tracker blindspot fix (UPSTREAM_ERROR now counts as drift, fingerprint + Missing Upstream section) and sync tooling retargeted to the jdk.incubator.json module. 1679 tests green; ci.yml exp_tests=1679. No release, no version bump.
1 parent 3cc73b0 commit 4c05919

5 files changed

Lines changed: 165 additions & 16 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ jobs:
3939
for k in totals: totals[k]+=int(r.get(k,'0'))
4040
except Exception:
4141
pass
42-
exp_tests=1676
42+
exp_tests=1679
4343
exp_skipped=0
4444
if totals['tests']!=exp_tests or totals['skipped']!=exp_skipped:
4545
print(f"Unexpected test totals: {totals} != expected tests={exp_tests}, skipped={exp_skipped}")

‎json-java21-api-tracker/src/main/java/io/github/simbo1905/tracker/ApiTracker.java‎

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -947,7 +947,7 @@ static String fetchUpstreamSource(String className) {
947947
/// @param report the full comparison report
948948
/// @return 7-character fingerprint or "0000000" if no differences
949949
static String generateFingerprint(JsonObject report) {
950-
if (getDifferentApiCount(report) == 0) {
950+
if (!hasDifferences(report)) {
951951
return "0000000";
952952
}
953953

@@ -959,6 +959,13 @@ static String generateFingerprint(JsonObject report) {
959959
final var diffObj = (JsonObject) diff;
960960
final var status = ((JsonString) diffObj.asMap().get("status")).asString();
961961

962+
if ("UPSTREAM_ERROR".equals(status)) {
963+
// Fetch failures are drift too: include them so the issue
964+
// dedup fingerprint is distinct per affected class set.
965+
stableLines.add(((JsonString) diffObj.asMap().get("className")).asString() + ":UPSTREAM_ERROR");
966+
continue;
967+
}
968+
962969
if (!"DIFFERENT".equals(status)) continue;
963970

964971
final var className = ((JsonString) diffObj.asMap().get("className")).asString();
@@ -1009,6 +1016,21 @@ private static long getDifferentApiCount(JsonObject report) {
10091016
return 0;
10101017
}
10111018

1019+
/// Extracts the missingUpstream count from a report summary
1020+
/// @param report the comparison report
1021+
/// @return the count of classes whose upstream fetch or parse failed
1022+
private static long getMissingUpstreamCount(JsonObject report) {
1023+
final var summary = (JsonObject) report.asMap().get("summary");
1024+
if (summary == null) {
1025+
return 0;
1026+
}
1027+
final var missingUpstreamValue = summary.asMap().get("missingUpstream");
1028+
if (missingUpstreamValue instanceof JsonNumber num) {
1029+
return num.asLong();
1030+
}
1031+
return 0;
1032+
}
1033+
10121034
/// Generates a terse human-readable summary of the API differences
10131035
/// Suitable for GitHub issue body
10141036
/// @param report the full comparison report
@@ -1068,6 +1090,23 @@ static String generateSummary(JsonObject report) {
10681090
}
10691091
}
10701092

1093+
if (missingUpstream > 0) {
1094+
sb.append("## Missing Upstream\n\n");
1095+
sb.append("These classes could not be fetched or parsed upstream. A fetch failure is drift: the tracker cannot confirm we are current.\n\n");
1096+
for (final var diff : differences.asList()) {
1097+
final var diffObj = (JsonObject) diff;
1098+
final var status = ((JsonString) diffObj.asMap().get("status")).asString();
1099+
1100+
if (!"UPSTREAM_ERROR".equals(status)) continue;
1101+
1102+
final var className = ((JsonString) diffObj.asMap().get("className")).asString();
1103+
final var errorValue = diffObj.asMap().get("error");
1104+
final var error = errorValue instanceof JsonString js ? js.asString() : "unknown error";
1105+
sb.append("- ⚠️ **").append(className).append("**: `").append(error).append("`\n");
1106+
}
1107+
sb.append("\n");
1108+
}
1109+
10711110
sb.append("---\n");
10721111
final var timestamp = ((JsonString) report.asMap().get("timestamp")).asString();
10731112
sb.append("*Generated by API Tracker on ").append(timestamp.split("T")[0]).append("*\n");
@@ -1077,8 +1116,8 @@ static String generateSummary(JsonObject report) {
10771116

10781117
/// Checks if there are any API differences in the report
10791118
/// @param report the comparison report
1080-
/// @return true if differentApi > 0
1119+
/// @return true if differentApi > 0 or missingUpstream > 0
10811120
static boolean hasDifferences(JsonObject report) {
1082-
return getDifferentApiCount(report) > 0;
1121+
return getDifferentApiCount(report) > 0 || getMissingUpstreamCount(report) > 0;
10831122
}
10841123
}

‎json-java21-api-tracker/src/test/java/io/github/simbo1905/tracker/ApiTrackerTest.java‎

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@
99

1010
import jdk.incubator.java.util.json.JsonBoolean;
1111
import jdk.incubator.java.util.json.JsonArray;
12+
import jdk.incubator.java.util.json.JsonNumber;
1213
import jdk.incubator.java.util.json.JsonObject;
1314
import jdk.incubator.java.util.json.JsonString;
15+
import jdk.incubator.java.util.json.Json;
1416

1517
import java.util.Set;
1618
import java.util.Map;
@@ -234,4 +236,108 @@ void testNormalizeTypeName() {
234236
.isEqualTo("String");
235237
}
236238
}
239+
240+
@Nested
241+
@DisplayName("Drift Detection Gates")
242+
class DriftDetectionGateTests {
243+
244+
private static JsonObject upstreamErrorDiff(String className, String error) {
245+
return JsonObject.of(Map.of(
246+
"className", JsonString.of(className),
247+
"status", JsonString.of("UPSTREAM_ERROR"),
248+
"error", JsonString.of(error)
249+
));
250+
}
251+
252+
private static JsonObject report(long differentApi, long missingUpstream, JsonArray differences) {
253+
return JsonObject.of(Map.of(
254+
"timestamp", JsonString.of("2026-08-30T00:00:00Z"),
255+
"summary", JsonObject.of(Map.of(
256+
"totalClasses", JsonNumber.of(differentApi + missingUpstream),
257+
"matchingClasses", JsonNumber.of(0),
258+
"differentApi", JsonNumber.of(differentApi),
259+
"missingUpstream", JsonNumber.of(missingUpstream)
260+
)),
261+
"differences", differences
262+
));
263+
}
264+
265+
@Test
266+
@DisplayName("Upstream fetch failures must count as drift, not all-clear")
267+
void testHasDifferencesTreatsUpstreamErrorAsDrift() {
268+
LoggingControl.setupCleanLogging();
269+
java.util.logging.Logger.getLogger(getClass().getName())
270+
.info(() -> "TEST: testHasDifferencesTreatsUpstreamErrorAsDrift");
271+
final var allErrors = report(0, 2, JsonArray.of(java.util.List.of(
272+
upstreamErrorDiff("JsonNumber", "NOT_FOUND: Upstream file not found"),
273+
upstreamErrorDiff("JsonObject", "HTTP_ERROR: Status 500")
274+
)));
275+
276+
assertThat(ApiTracker.hasDifferences(allErrors))
277+
.as("a fully-blind detector run (every class UPSTREAM_ERROR) must report drift")
278+
.isTrue();
279+
280+
final var mixed = report(1, 1, JsonArray.of(java.util.List.of(
281+
upstreamErrorDiff("JsonNumber", "NOT_FOUND: Upstream file not found"),
282+
JsonObject.of(Map.of(
283+
"className", JsonString.of("JsonValue"),
284+
"status", JsonString.of("DIFFERENT"),
285+
"differences", JsonArray.of(java.util.List.of())
286+
))
287+
)));
288+
assertThat(ApiTracker.hasDifferences(mixed)).isTrue();
289+
}
290+
291+
@Test
292+
@DisplayName("Fingerprint covers UPSTREAM_ERROR classes and is stable and distinct")
293+
void testFingerprintCoversUpstreamErrors() {
294+
LoggingControl.setupCleanLogging();
295+
java.util.logging.Logger.getLogger(getClass().getName())
296+
.info(() -> "TEST: testFingerprintCoversUpstreamErrors");
297+
final var none = report(0, 0, JsonArray.of(java.util.List.of()));
298+
assertThat(ApiTracker.generateFingerprint(none)).isEqualTo("0000000");
299+
300+
final var errorsA = report(0, 2, JsonArray.of(java.util.List.of(
301+
upstreamErrorDiff("JsonNumber", "NOT_FOUND: Upstream file not found"),
302+
upstreamErrorDiff("JsonObject", "NOT_FOUND: Upstream file not found")
303+
)));
304+
final var errorsAgain = report(0, 2, JsonArray.of(java.util.List.of(
305+
upstreamErrorDiff("JsonObject", "NOT_FOUND: Upstream file not found"),
306+
upstreamErrorDiff("JsonNumber", "NOT_FOUND: Upstream file not found")
307+
)));
308+
final var errorsB = report(0, 1, JsonArray.of(java.util.List.of(
309+
upstreamErrorDiff("JsonNumber", "NOT_FOUND: Upstream file not found")
310+
)));
311+
312+
final var fpA = ApiTracker.generateFingerprint(errorsA);
313+
assertThat(fpA)
314+
.as("fetch-failure drift must not hash to the no-differences sentinel")
315+
.isNotEqualTo("0000000");
316+
assertThat(ApiTracker.generateFingerprint(errorsAgain))
317+
.as("same error set in different order must fingerprint identically")
318+
.isEqualTo(fpA);
319+
assertThat(ApiTracker.generateFingerprint(errorsB))
320+
.as("different error sets must fingerprint differently")
321+
.isNotEqualTo(fpA);
322+
}
323+
324+
@Test
325+
@DisplayName("Summary renders a Missing Upstream section for fetch failures")
326+
void testSummaryRendersMissingUpstreamSection() {
327+
LoggingControl.setupCleanLogging();
328+
java.util.logging.Logger.getLogger(getClass().getName())
329+
.info(() -> "TEST: testSummaryRendersMissingUpstreamSection");
330+
final var allErrors = report(0, 2, JsonArray.of(java.util.List.of(
331+
upstreamErrorDiff("JsonNumber", "NOT_FOUND: Upstream file not found (possibly deleted or renamed)"),
332+
upstreamErrorDiff("JsonObject", "HTTP_ERROR: Status 500")
333+
)));
334+
335+
final var summary = ApiTracker.generateSummary(allErrors);
336+
337+
assertThat(summary).contains("Missing Upstream");
338+
assertThat(summary).contains("JsonNumber");
339+
assertThat(summary).contains("JsonObject");
340+
assertThat(summary).contains("NOT_FOUND: Upstream file not found (possibly deleted or renamed)");
341+
}
342+
}
237343
}

‎updates/2025-09-04/RefreshFromUpstream.java‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// Compact single-file Java 25 script to refresh and compare impl sources
22
// Run: java RefreshFromUpstream.java
3+
// Refreshed 2026-08-30 for the upstream jdk.incubator.json module layout (issue #154).
34

45
import java.io.*;
56
import java.net.URI;
@@ -37,19 +38,20 @@ void main() throws Exception {
3738

3839
// Work dirs (not checked in)
3940
Path updatesDir = repoRoot.resolve("updates/2025-09-04");
40-
Path upstreamDir = updatesDir.resolve("upstream/jdk.internal.util.json");
41+
Path upstreamDir = updatesDir.resolve("upstream/jdk.incubator.json.impl");
4142
Path reportDir = updatesDir.resolve("reports");
4243
Files.createDirectories(upstreamDir);
4344
Files.createDirectories(reportDir);
4445

45-
// Upstream raw base for impl package
46-
String upstreamBase = "https://raw.githubusercontent.com/openjdk/jdk-sandbox/refs/heads/json/src/java.base/share/classes/jdk/internal/util/json/";
46+
// Upstream raw base for the incubator module impl package
47+
String upstreamBase = "https://raw.githubusercontent.com/openjdk/jdk-sandbox/refs/heads/json/src/jdk.incubator.json/share/classes/jdk/incubator/json/impl/";
4748

48-
// Discover local impl files
49+
// Discover local impl files (LazyConstant.java is a local-only polyfill with no upstream counterpart)
4950
List<Path> localFiles;
5051
try (var stream = Files.list(localImplDir)) {
5152
localFiles = stream
5253
.filter(p -> p.getFileName().toString().endsWith(".java"))
54+
.filter(p -> !p.getFileName().toString().equals("LazyConstant.java"))
5355
.sorted()
5456
.toList();
5557
}

‎updates/2025-09-04/transform_upstream.py‎

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import os, sys, re, shutil
22

3-
SRC = 'updates/2025-09-04/upstream/jdk.internal.util.json'
3+
# Refreshed 2026-08-30 for the upstream jdk.incubator.json module layout (issue #154).
4+
# Scope: impl files fetched into the snapshot dir (see RefreshFromUpstream.java).
5+
# Public API files follow the manual process in json-java21/AGENTS.md.
6+
SRC = 'updates/2025-09-04/upstream/jdk.incubator.json.impl'
47
DST = 'json-java21/src/main/java/jdk/incubator/internal/util/json'
58

69
def read(path):
@@ -27,10 +30,11 @@ def write_safe(path, text):
2730
return True
2831

2932
def transform(text, name):
30-
# package
31-
text = re.sub(r'^package\s+jdk\.internal\.util\.json;', 'package jdk.incubator.internal.util.json;', text, flags=re.M)
32-
# imports for public API
33-
text = re.sub(r'^(\s*import\s+)java\.util\.json\.', r'\1jdk.incubator.java.util.json.', text, flags=re.M)
33+
# package: upstream impl package -> our internal package
34+
text = re.sub(r'^package\s+jdk\.incubator\.json\.impl;', 'package jdk.incubator.internal.util.json;', text, flags=re.M)
35+
# imports: impl-internal first (defensive; upstream impl rarely imports itself), then public API
36+
text = re.sub(r'^(\s*import\s+)jdk\.incubator\.json\.impl\.', r'\1jdk.incubator.internal.util.json.', text, flags=re.M)
37+
text = re.sub(r'^(\s*import\s+)jdk\.incubator\.json\.', r'\1jdk.incubator.java.util.json.', text, flags=re.M)
3438
# annotations (single-line)
3539
text = re.sub(r'^\s*@(?:jdk\.internal\..*|ValueBased|StableValue).*\n', '', text, flags=re.M)
3640
# remove import of ValueBased if present
@@ -57,9 +61,6 @@ def main():
5761
for name in os.listdir(SRC):
5862
if not name.endswith('.java'):
5963
continue
60-
if name in ('StableValue.java', 'Utils.java'):
61-
# Keep local backport helper and existing Utils for now
62-
continue
6364
src_path = os.path.join(SRC, name)
6465
dst_path = os.path.join(DST, name)
6566
data = read(src_path)
@@ -69,6 +70,7 @@ def main():
6970
if not ok:
7071
sys.exit(2)
7172
print('Transform complete')
73+
print('Reminder: after transforming, re-append the Utils.powExact polyfill (Java 21 lacks Math.powExact) and keep LazyConstant.java untouched (local polyfill).')
7274

7375
if __name__ == '__main__':
7476
main()

0 commit comments

Comments
 (0)