Skip to content

Commit 52bd016

Browse files
committed
Issue #153 treat upstream fetch failure as drift in the API tracker
hasDifferences now returns true when missingUpstream > 0, the fingerprint includes UPSTREAM_ERROR classes so issue dedup works for fetch-failure drift, and the summary renders a Missing Upstream section listing each affected class and error. Previously a dead upstream fetch path reported all-clear, which is what blinded the daily tracker after the upstream incubator move (see #145). Verify: mvnd -pl json-java21-api-tracker -am clean test (14 tracker tests green, 3 new); full reactor clean verify = 1679 tests 0 skipped; ci.yml exp_tests 1676 -> 1679.
1 parent 3cc73b0 commit 52bd016

3 files changed

Lines changed: 149 additions & 4 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
}

0 commit comments

Comments
 (0)