You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@juneau.apache.org by ja...@apache.org on 2026/08/30 19:52:01 UTC
(juneau) branch master updated: Dedupe shared Host.properties across six nested-Host view test classes, harden the chrome sprite-render and font-size scanner checks, and fix Jetty transport-failure test flakiness.
This is an automated email from the ASF dual-hosted git repository.
jamesbognar pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/juneau.git
The following commit(s) were added to refs/heads/master by this push:
new 65e759d498 Dedupe shared Host.properties across six nested-Host view test classes, harden the chrome sprite-render and font-size scanner checks, and fix Jetty transport-failure test flakiness.
65e759d498 is described below
commit 65e759d4987be02994b77850ca3bd9fd37d4d4f6
Author: James Bognar <ja...@apache.org>
AuthorDate: Sun Aug 30 15:51:55 2026 -0400
Dedupe shared Host.properties across six nested-Host view test classes, harden the chrome sprite-render and font-size scanner checks, and fix Jetty transport-failure test flakiness.
J0491: dedupe shared Host.properties across 6 nested-Host view test classes (per-class bundle resolution)
J0451: assert the four locked chrome glyphs stay pairwise pixel-diff distinguishable (sprite-render probe)
J0467: font-size-specific carve-out in ChromeScaleScanner's D9 duplicate-literal rule
J0493: make the Jetty transport-failure test deterministic (CountDownLatch) to fix load-contention flakiness
---
.../JettyHttpTransport_TransportFailure_Test.java | 27 ++++++-
.../views/CalendarTableRequestAware_Test.java | 4 +-
.../rest/server/views/ChromeScaleScanner.java | 66 +++++++++++++++-
.../views/ChromeScale_ContractScan_Test.java | 34 +++++++++
.../views/LocalizationChromeResolution_Test.java | 9 ++-
.../server/views/PageTableRequestAware_Test.java | 4 +-
.../views/ServerValuesHostsConcurrency_Test.java | 4 +-
.../rest/server/views/ServerValuesHosts_Test.java | 4 +-
.../server/views/ServerValuesResolution_Test.java | 4 +-
.../views/SymbolSprite_Render_BrowserTest.java | 89 ++++++++++++++++++++--
.../src/test/js/symbol-sprite-render.cjs | 70 ++++++++++++++++-
...=> LocalizationChromeResolutionHost.properties} | 0
12 files changed, 288 insertions(+), 27 deletions(-)
diff --git a/juneau-rest/juneau-rest-client-jetty/src/test/java/org/apache/juneau/rest/client/JettyHttpTransport_TransportFailure_Test.java b/juneau-rest/juneau-rest-client-jetty/src/test/java/org/apache/juneau/rest/client/JettyHttpTransport_TransportFailure_Test.java
index 15bae363ea..2ca80ca3c9 100644
--- a/juneau-rest/juneau-rest-client-jetty/src/test/java/org/apache/juneau/rest/client/JettyHttpTransport_TransportFailure_Test.java
+++ b/juneau-rest/juneau-rest-client-jetty/src/test/java/org/apache/juneau/rest/client/JettyHttpTransport_TransportFailure_Test.java
@@ -20,6 +20,7 @@ import static org.junit.jupiter.api.Assertions.*;
import java.io.*;
import java.net.*;
+import java.util.concurrent.*;
import java.util.concurrent.atomic.*;
import org.apache.juneau.http.*;
@@ -142,8 +143,12 @@ class JettyHttpTransport_TransportFailure_Test {
@Test
void d01_bodyWriteFails_requestAborted() throws Exception {
var serverSocket = new ServerSocket(0);
+ // Signaled once the server has actually accepted the TCP connection, so the writer thread below
+ // cannot fail before HttpClient.send() has genuinely started the exchange (see failingBody, below).
+ var connectionAccepted = new CountDownLatch(1);
var acceptThread = new Thread(() -> {
try (var socket = serverSocket.accept()) {
+ connectionAccepted.countDown();
// Read whatever arrives (partial request) and never respond; the client-side write failure
// is what we're exercising, not any particular server behavior.
socket.getInputStream().readAllBytes();
@@ -159,7 +164,27 @@ class JettyHttpTransport_TransportFailure_Test {
var transport = JettyHttpTransport.builder().responseTimeoutMs(500).build();
HttpBody failingBody = new HttpBody() {
@Override public String getContentType() { return "text/plain"; }
- @Override public void writeTo(OutputStream out) throws IOException { throw new IOException("simulated write failure"); }
+ @Override public void writeTo(OutputStream out) throws IOException {
+ // Bound the race against HttpClient.send() deterministically instead of failing immediately:
+ // closing an OutputStreamRequestContent's stream before Jetty has started consuming it is
+ // indistinguishable from a legitimately empty body, so an immediate throw/close here can --
+ // under scheduling contention -- lose the race and never surface as an abort. Waiting for the
+ // server to have accepted the connection guarantees the exchange is already underway.
+ try {
+ connectionAccepted.await(2, TimeUnit.SECONDS);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ // Mirrors sibling a01's Thread.sleep(200): gives Jetty's async state machine a moment to be
+ // actively mid-exchange (rather than just barely past accept()) before this thread fails, so
+ // the abort is reliably observed as a failure rather than racing the 500ms responseTimeoutMs.
+ try {
+ Thread.sleep(200);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ throw new IOException("simulated write failure");
+ }
};
var request = TransportRequest.builder()
.method("POST")
diff --git a/juneau-rest/juneau-rest-server-views/src/test/java/org/apache/juneau/rest/server/views/CalendarTableRequestAware_Test.java b/juneau-rest/juneau-rest-server-views/src/test/java/org/apache/juneau/rest/server/views/CalendarTableRequestAware_Test.java
index f9ed0682e2..056b28d79e 100644
--- a/juneau-rest/juneau-rest-server-views/src/test/java/org/apache/juneau/rest/server/views/CalendarTableRequestAware_Test.java
+++ b/juneau-rest/juneau-rest-server-views/src/test/java/org/apache/juneau/rest/server/views/CalendarTableRequestAware_Test.java
@@ -62,7 +62,7 @@ class CalendarTableRequestAware_Test extends TestBase {
}
@Rest
- public static class Host extends BasicRestServlet {
+ public static class CalendarTableRequestAwareHost extends BasicRestServlet {
private static final long serialVersionUID = 1L;
@RestGet(path="/cal") public HttpResource cal(RestRequest req) {
@@ -76,7 +76,7 @@ class CalendarTableRequestAware_Test extends TestBase {
}
}
- private static final MockRestClient c = MockRestClient.buildLax(Host.class);
+ private static final MockRestClient c = MockRestClient.buildLax(CalendarTableRequestAwareHost.class);
@Test void a01_frenchAcceptLanguage_localizesTheMonthTitle() throws Exception {
var html = c.get("/cal").header("Accept-Language", "fr").accept("text/html").run()
diff --git a/juneau-rest/juneau-rest-server-views/src/test/java/org/apache/juneau/rest/server/views/ChromeScaleScanner.java b/juneau-rest/juneau-rest-server-views/src/test/java/org/apache/juneau/rest/server/views/ChromeScaleScanner.java
index 09afd9731a..7774234a93 100644
--- a/juneau-rest/juneau-rest-server-views/src/test/java/org/apache/juneau/rest/server/views/ChromeScaleScanner.java
+++ b/juneau-rest/juneau-rest-server-views/src/test/java/org/apache/juneau/rest/server/views/ChromeScaleScanner.java
@@ -54,6 +54,21 @@ import java.util.regex.*;
* {@code html}/{@code body}, so the root context is the browser default in both a themeless and a themed
* render.
* </ul>
+ * <p>
+ * <b>{@code font-size}'s {@code rem} reachability ({@code TODO-J0467}).</b> {@code font-size}'s three
+ * chrome-scale steps ({@code --jc-chrome-font-size-1/2/3}) are declared in {@code rem}, not {@code px} - the
+ * only family in {@link #scale()} that is. That is a different question from the one above: this class's own
+ * literal-duplicate matcher has admitted both {@code px} and {@code rem} on every property it checks since its
+ * original implementation (a {@code gap} literal needs the same {@code rem} reach {@link #RECORDED_LITERALS}
+ * already relies on it for), so a {@code rem}-valued {@code font-size} duplicate was never structurally invisible
+ * to <i>this</i> class the way it was to the separate, one-time manual membership audit that named the
+ * {@code em}-only {@code D9} rule when the shared chrome-scale contract now archived at {@code FINISHED-J0458}
+ * was first designed. {@link #findDuplicatedStep} still gives {@code font-size} its own named, dedicated
+ * {@link #FONT_SIZE_ABSOLUTE} matcher rather than leaning on the general {@link #ABSOLUTE} one, mirroring the
+ * {@code line-height}-scoped {@link #BARE_NUMBER} carve-out below and making the reachability an explicit,
+ * auditable, per-property fact rather than an incidental side effect - see
+ * {@code ChromeScale_ContractScan_Test.b12}/{@code b13} for the proof, in both directions, that this fix does
+ * not disturb the locked exclusion list.
*
* <h5 class='section'>Provisional steps are excluded from the second half</h5>
* <p>
@@ -185,9 +200,29 @@ final class ChromeScaleScanner {
/** Matches a {@code var(...)} reference, including any fallback, so a fallback is not read as a bare literal. */
private static final Pattern VAR_REF = Pattern.compile("var\\([^()]*(?:\\([^()]*\\)[^()]*)*\\)");
- /** Matches an absolute length/size token. */
+ /**
+ * Matches an absolute {@code px} or {@code rem} length/size token. General on purpose: unlike
+ * {@code line-height}'s unitless step (see {@link #BARE_NUMBER}), several properties this contract checks
+ * carry a legitimate {@code rem} literal that must stay reachable regardless of which family it duplicates -
+ * for example {@code gap}'s card-grid density declaration ({@link #RECORDED_LITERALS}), not just
+ * {@code font-size}. See {@link #FONT_SIZE_ABSOLUTE} for the one property with its own dedicated,
+ * additionally-documented reference to this same {@code rem} domain.
+ */
private static final Pattern ABSOLUTE = Pattern.compile("(?<![\\w.-])(\\d*\\.?\\d+)(px|rem)(?![\\w-])");
+ /**
+ * The {@code font-size}-scoped counterpart to {@link #ABSOLUTE}, used only for the {@code font-size}
+ * property (see {@link #findDuplicatedStep}). Declared as its own named pattern - identical in what it
+ * matches to {@link #ABSOLUTE} today - so that {@code font-size}'s {@code rem} reachability is an explicit,
+ * auditable carve-out in its own right (mirroring the {@code line-height}-scoped {@link #BARE_NUMBER}
+ * carve-out) rather than an incidental side effect of the general matcher, closing the unit-domain mismatch
+ * named by {@code TODO-J0467}: {@code font-size}'s three chrome-scale steps
+ * ({@code --jc-chrome-font-size-1/2/3}) are declared in {@code rem}, and a {@code rem}-valued
+ * {@code font-size} literal that duplicates one must never go unmatched the way a tenth un-tokenized site
+ * once did.
+ */
+ private static final Pattern FONT_SIZE_ABSOLUTE = Pattern.compile("(?<![\\w.-])(\\d*\\.?\\d+)(px|rem)(?![\\w-])");
+
/**
* Matches a bare, unitless decimal. {@code line-height}'s scale step ({@code --jc-chrome-line-height},
* {@code 1.2}) is the only step value in the ladder with no {@code px}/{@code rem} unit, so this pattern is
@@ -298,14 +333,21 @@ final class ChromeScaleScanner {
// line-height's step value is unitless, so it needs bare-number matching instead of the general
// px/rem literal matcher - scoped to this one property so no other property's matching widens (LD-2).
var lineHeight = "line-height".equals(d.property());
- var literals = lineHeight ? bareNumberValues(d.value()) : absoluteValues(d.value());
+ // font-size's three steps are declared in rem, not px. The general ABSOLUTE matcher already admits
+ // rem (other properties, e.g. gap, rely on that too), but font-size gets its own named matcher here so
+ // its rem reachability is an explicit, font-size-scoped carve-out (TODO-J0467) rather than an
+ // incidental side effect of the shared one - mirroring the line-height carve-out above in shape.
+ var fontSize = "font-size".equals(d.property());
+ var literals = lineHeight ? bareNumberValues(d.value())
+ : fontSize ? fontSizeAbsoluteValues(d.value())
+ : absoluteValues(d.value());
if (literals.isEmpty())
return null;
for (var roleNamed : List.of(true, false)) {
for (var s : scale) {
if (!s.confirmed() || !families.contains(s.family()) || (s.family() == Family.SPACE) == roleNamed)
continue;
- var stepValue = lineHeight ? bareValue(s.value()) : toPx(s.value());
+ var stepValue = lineHeight ? bareValue(s.value()) : fontSize ? fontSizeToPx(s.value()) : toPx(s.value());
if (stepValue != null && literals.contains(stepValue))
return s;
}
@@ -346,6 +388,24 @@ final class ChromeScaleScanner {
return m.find() ? px(Double.parseDouble(m.group(1)), m.group(2)) : null;
}
+ /**
+ * The {@code font-size}-scoped counterpart to {@link #absoluteValues}: also admits {@code rem}. Only ever
+ * called for the {@code font-size} property (see {@link #findDuplicatedStep}).
+ */
+ private static Set<Double> fontSizeAbsoluteValues(String value) {
+ var out = new LinkedHashSet<Double>();
+ var m = FONT_SIZE_ABSOLUTE.matcher(VAR_REF.matcher(value).replaceAll(" "));
+ while (m.find())
+ out.add(px(Double.parseDouble(m.group(1)), m.group(2)));
+ return out;
+ }
+
+ /** The {@code font-size}-scoped counterpart to {@link #toPx}: also admits {@code rem}. */
+ private static Double fontSizeToPx(String value) {
+ var m = FONT_SIZE_ABSOLUTE.matcher(value);
+ return m.find() ? px(Double.parseDouble(m.group(1)), m.group(2)) : null;
+ }
+
/**
* Every bare unitless number in a declaration's value, {@code var(...)} fallbacks excluded. Only ever called
* for the {@code line-height} property (see {@link #findDuplicatedStep}).
diff --git a/juneau-rest/juneau-rest-server-views/src/test/java/org/apache/juneau/rest/server/views/ChromeScale_ContractScan_Test.java b/juneau-rest/juneau-rest-server-views/src/test/java/org/apache/juneau/rest/server/views/ChromeScale_ContractScan_Test.java
index 0553a3e91b..668d90b435 100644
--- a/juneau-rest/juneau-rest-server-views/src/test/java/org/apache/juneau/rest/server/views/ChromeScale_ContractScan_Test.java
+++ b/juneau-rest/juneau-rest-server-views/src/test/java/org/apache/juneau/rest/server/views/ChromeScale_ContractScan_Test.java
@@ -177,4 +177,38 @@ class ChromeScale_ContractScan_Test extends TestBase {
+ r.violations());
assertTrue(r.violations().get(0).contains("--jc-chrome-line-height (1.2)"), r.violations().get(0));
}
+
+ /**
+ * {@code font-size}'s three chrome-scale steps are declared in {@code rem}, not {@code px} - the general
+ * px-only literal matcher could never bind to a {@code rem}-valued {@code font-size} duplicate before this
+ * fix (TODO-J0467), so a tenth un-tokenized {@code 0.75rem}/{@code 0.8125rem}/{@code 0.875rem} site
+ * introduced today would have gone unnoticed exactly the same way the original twelve did. Proves the
+ * widened, {@code font-size}-scoped {@code rem} carve-out now catches it.
+ */
+ @Test void b12_fontSizeRemCarveOutCatchesADuplicatedValue() {
+ var r = ChromeScaleScanner.scan(".synthetic-label { font-size: 0.875rem; }");
+ assertEquals(1, r.violations().size(), () -> "expected the rem-valued font-size duplicate to be caught: "
+ + r.violations());
+ assertTrue(r.violations().get(0).contains("--jc-chrome-font-size-3 (0.875rem)"), r.violations().get(0));
+ }
+
+ /**
+ * The {@code font-size} {@code rem} carve-out must not re-expose the three DataTables density literals that
+ * {@code TODO-J0467}'s own locked exclusion list pins permanently - a token there would re-resolve globally
+ * the moment a theme touches the scale, because the cell-level rule is deliberately pinned in {@code rem}
+ * rather than left to inherit (see {@link ChromeScaleScanner#recordedLiterals()}). Reproduces the three
+ * pinned sites synthetically, by selector shape rather than by reading the shipped stylesheet's own line
+ * numbers (which have already drifted twice), so this proves the carve-out leaves them alone independent of
+ * where they currently sit in the file.
+ */
+ @Test void b13_fontSizeRemCarveOutDoesNotReExposeTheLockedExclusionList() {
+ var r = ChromeScaleScanner.scan(
+ "table[data-juneau-view], table.dataTable { font-size: 0.75rem; }"
+ + " table[data-juneau-view] > thead > tr > th { font-size: 0.75rem; }"
+ + " table[data-juneau-view] > tbody > tr > th { font-size: 0.75rem; }");
+ assertTrue(r.violations().isEmpty(), () -> "the locked exclusion list was re-exposed as a violation: "
+ + r.violations());
+ assertEquals(3, r.matchedRecords().size(), () -> "expected all three pinned density literals to still "
+ + "match their recorded exception: " + r.matchedRecords());
+ }
}
diff --git a/juneau-rest/juneau-rest-server-views/src/test/java/org/apache/juneau/rest/server/views/LocalizationChromeResolution_Test.java b/juneau-rest/juneau-rest-server-views/src/test/java/org/apache/juneau/rest/server/views/LocalizationChromeResolution_Test.java
index ba28b078cb..fa9c892851 100644
--- a/juneau-rest/juneau-rest-server-views/src/test/java/org/apache/juneau/rest/server/views/LocalizationChromeResolution_Test.java
+++ b/juneau-rest/juneau-rest-server-views/src/test/java/org/apache/juneau/rest/server/views/LocalizationChromeResolution_Test.java
@@ -38,7 +38,8 @@ import org.junit.jupiter.api.*;
* scoping, {@link Column#titleKey} sugar (LD-3), and the request-free {@link Messages}-bean seam (LD-4).
*
* <p>
- * {@code Host.properties} (same package, {@code src/test/resources}) backs {@code RestRequest.getMessages()} and
+ * {@code LocalizationChromeResolutionHost.properties} (same package, {@code src/test/resources}) backs
+ * {@code RestRequest.getMessages()} and
* the request-free {@link Messages#of(Class)} lookups below: {@code col.name=Name}, {@code col.status=Status},
* {@code col.a=$L{col.b}}, {@code col.b=Leaf}, {@code page.title=Releases Page}, {@code tab.main=Main}.
*/
@@ -79,7 +80,7 @@ class LocalizationChromeResolution_Test extends TestBase {
//------------------------------------------------------------------------------------------------------------------
@Rest(mixins=ViewsMixin.class)
- public static class Host extends BasicRestServlet {
+ public static class LocalizationChromeResolutionHost extends BasicRestServlet {
private static final long serialVersionUID = 1L;
@RestGet(path="/view") public HttpResource view(RestRequest req) {
@@ -101,7 +102,7 @@ class LocalizationChromeResolution_Test extends TestBase {
}
}
- private static final MockRestClient c = MockRestClient.buildLax(Host.class);
+ private static final MockRestClient c = MockRestClient.buildLax(LocalizationChromeResolutionHost.class);
private static String body(String path) throws Exception {
return c.get(path).run().assertStatus(200).getContent().asString();
@@ -207,7 +208,7 @@ class LocalizationChromeResolution_Test extends TestBase {
//------------------------------------------------------------------------------------------------------------------
private static Messages messages() {
- return Messages.of(Host.class);
+ return Messages.of(LocalizationChromeResolutionHost.class);
}
@Test void d01_messagesBean_resolvesLWithNoRequest() {
diff --git a/juneau-rest/juneau-rest-server-views/src/test/java/org/apache/juneau/rest/server/views/PageTableRequestAware_Test.java b/juneau-rest/juneau-rest-server-views/src/test/java/org/apache/juneau/rest/server/views/PageTableRequestAware_Test.java
index b2f0ebf6c5..1a4ec2c7b7 100644
--- a/juneau-rest/juneau-rest-server-views/src/test/java/org/apache/juneau/rest/server/views/PageTableRequestAware_Test.java
+++ b/juneau-rest/juneau-rest-server-views/src/test/java/org/apache/juneau/rest/server/views/PageTableRequestAware_Test.java
@@ -78,7 +78,7 @@ class PageTableRequestAware_Test extends TestBase {
}
@Rest(mixins=ViewsMixin.class)
- public static class Host extends BasicRestServlet {
+ public static class PageTableRequestAwareHost extends BasicRestServlet {
private static final long serialVersionUID = 1L;
@Bean public VarResolver varResolver(VarResolver.Builder b) {
@@ -107,7 +107,7 @@ class PageTableRequestAware_Test extends TestBase {
}
}
- private static final MockRestClient c = MockRestClient.buildLax(Host.class);
+ private static final MockRestClient c = MockRestClient.buildLax(PageTableRequestAwareHost.class);
private static String body(String path) throws Exception {
return c.get(path).accept("text/html").run().assertStatus(200).getContent().asString();
diff --git a/juneau-rest/juneau-rest-server-views/src/test/java/org/apache/juneau/rest/server/views/ServerValuesHostsConcurrency_Test.java b/juneau-rest/juneau-rest-server-views/src/test/java/org/apache/juneau/rest/server/views/ServerValuesHostsConcurrency_Test.java
index 77db772b3b..3d9e829a60 100644
--- a/juneau-rest/juneau-rest-server-views/src/test/java/org/apache/juneau/rest/server/views/ServerValuesHostsConcurrency_Test.java
+++ b/juneau-rest/juneau-rest-server-views/src/test/java/org/apache/juneau/rest/server/views/ServerValuesHostsConcurrency_Test.java
@@ -102,7 +102,7 @@ class ServerValuesHostsConcurrency_Test extends TestBase {
.build();
@Rest(mixins=ViewsMixin.class)
- public static class Host extends BasicRestServlet {
+ public static class ServerValuesHostsConcurrencyHost extends BasicRestServlet {
private static final long serialVersionUID = 1L;
@Bean public VarResolver varResolver(VarResolver.Builder b) {
@@ -144,7 +144,7 @@ class ServerValuesHostsConcurrency_Test extends TestBase {
for (var i = 0; i < THREADS; i++) {
var mine = "T" + i;
// One client per thread so the only shared mutable state under test is the PageDef itself.
- var client = MockRestClient.buildLax(Host.class);
+ var client = MockRestClient.buildLax(ServerValuesHostsConcurrencyHost.class);
futures.add(pool.submit(() -> {
await(start);
for (var r = 0; r < ROUNDS; r++) {
diff --git a/juneau-rest/juneau-rest-server-views/src/test/java/org/apache/juneau/rest/server/views/ServerValuesHosts_Test.java b/juneau-rest/juneau-rest-server-views/src/test/java/org/apache/juneau/rest/server/views/ServerValuesHosts_Test.java
index 39bd62c2d8..24d4d6e9fb 100644
--- a/juneau-rest/juneau-rest-server-views/src/test/java/org/apache/juneau/rest/server/views/ServerValuesHosts_Test.java
+++ b/juneau-rest/juneau-rest-server-views/src/test/java/org/apache/juneau/rest/server/views/ServerValuesHosts_Test.java
@@ -154,7 +154,7 @@ class ServerValuesHosts_Test extends TestBase {
//------------------------------------------------------------------------------------------------------------------
@Rest(mixins=ViewsMixin.class)
- public static class Host extends BasicRestServlet {
+ public static class ServerValuesHostsHost extends BasicRestServlet {
private static final long serialVersionUID = 1L;
@Bean public VarResolver varResolver(VarResolver.Builder b) {
@@ -193,7 +193,7 @@ class ServerValuesHosts_Test extends TestBase {
}
}
- private static final MockRestClient c = MockRestClient.buildLax(Host.class);
+ private static final MockRestClient c = MockRestClient.buildLax(ServerValuesHostsHost.class);
private static String body(String path) throws Exception {
return c.get(path).run().assertStatus(200).getContent().asString();
diff --git a/juneau-rest/juneau-rest-server-views/src/test/java/org/apache/juneau/rest/server/views/ServerValuesResolution_Test.java b/juneau-rest/juneau-rest-server-views/src/test/java/org/apache/juneau/rest/server/views/ServerValuesResolution_Test.java
index 6b23851de6..f2344a1c0d 100644
--- a/juneau-rest/juneau-rest-server-views/src/test/java/org/apache/juneau/rest/server/views/ServerValuesResolution_Test.java
+++ b/juneau-rest/juneau-rest-server-views/src/test/java/org/apache/juneau/rest/server/views/ServerValuesResolution_Test.java
@@ -105,7 +105,7 @@ class ServerValuesResolution_Test extends TestBase {
.build();
@Rest(mixins=ViewsMixin.class)
- public static class Host extends BasicRestServlet {
+ public static class ServerValuesResolutionHost extends BasicRestServlet {
private static final long serialVersionUID = 1L;
@Bean public VarResolver varResolver(VarResolver.Builder b) {
@@ -136,7 +136,7 @@ class ServerValuesResolution_Test extends TestBase {
}
}
- private static final MockRestClient c = MockRestClient.buildLax(Host.class);
+ private static final MockRestClient c = MockRestClient.buildLax(ServerValuesResolutionHost.class);
private static String body(String path) throws Exception {
return c.get(path).run().assertStatus(200).getContent().asString();
diff --git a/juneau-rest/juneau-rest-server-views/src/test/java/org/apache/juneau/rest/server/views/SymbolSprite_Render_BrowserTest.java b/juneau-rest/juneau-rest-server-views/src/test/java/org/apache/juneau/rest/server/views/SymbolSprite_Render_BrowserTest.java
index 9cc466dc13..1f38470b17 100644
--- a/juneau-rest/juneau-rest-server-views/src/test/java/org/apache/juneau/rest/server/views/SymbolSprite_Render_BrowserTest.java
+++ b/juneau-rest/juneau-rest-server-views/src/test/java/org/apache/juneau/rest/server/views/SymbolSprite_Render_BrowserTest.java
@@ -50,13 +50,20 @@ import org.junit.jupiter.api.condition.*;
* exist so a human can compare a redrawn set against its predecessor <i>as pixels</i>, and - the part per-glyph
* review structurally cannot do - see the four document glyphs beside each other, where family drift lives.
* <p>
- * <b>Do not add a threshold assertion on any metric in this class.</b> Antialiasing coverage is a property of the
- * Chromium build doing the rasterising. The {@code js-tests} profile pins that build, which makes these numbers
- * <i>reproducible</i> today but not <i>stable</i> across a future pin bump - so a numeric gate would convert a
- * routine Playwright version bump into a false artwork regression, and the reflex fix for a false regression is
- * to loosen the number until it passes, which leaves a gate that asserts nothing. The numbers are for a human to
- * read. The only durable assertions here are ink-is-non-zero and the anti-vacuous check that proves that
- * measurement can fail.
+ * <b>Do not add a threshold assertion on any of the four per-glyph metrics above (ink/solid/gradient/mush).</b>
+ * Antialiasing coverage is a property of the Chromium build doing the rasterising. The {@code js-tests} profile
+ * pins that build, which makes these numbers <i>reproducible</i> today but not <i>stable</i> across a future pin
+ * bump - so a numeric gate on one of them would convert a routine Playwright version bump into a false artwork
+ * regression, and the reflex fix for a false regression is to loosen the number until it passes, which leaves a
+ * gate that asserts nothing. The numbers are for a human to read.
+ * <p>
+ * <b>The one exception is {@link #c01_fourLockedGlyphsStayMutuallyDistinguishable()}</b> ({@code
+ * [TODO-J0451]}), which thresholds the prober's {@code adjacencyDiffs} - the mean per-pixel luminance
+ * difference <i>between two glyphs rasterised in the same run, same size, same Chromium build</i>, not either
+ * glyph's absolute coverage. A pin bump moves both sides of that comparison together, so the pairwise diff does
+ * not carry the non-reproducibility problem the paragraph above describes; it is safe to threshold precisely
+ * because it is relative, not absolute. Before this class, `cancel`, `columns`, `edit` and `settings` had zero
+ * glyph-specific automated coverage - only the generic non-vacuous ink check every stem gets.
*
* <h5 class='section'>What is real and what is restated:</h5>
* <p>
@@ -182,7 +189,17 @@ class SymbolSprite_Render_BrowserTest extends TestBase {
// The four-glyph document family plus the one glyph the redraw gave a new meaning, whose 16px
// distinguishability against both settings and spreadsheet is a named review check.
"family", List.of("csv", "pdf", "spreadsheet", "copy"),
- "adjacencies", List.of(List.of("columns", "spreadsheet"), List.of("columns", "settings"))));
+ // [TODO-J0451] LD-1: every pairwise combination of the four locked-scope glyphs (cancel, columns,
+ // edit, settings), plus the pre-existing columns-spreadsheet review-only pair (columns-settings is
+ // reused rather than duplicated). See c01_fourLockedGlyphsStayMutuallyDistinguishable() below.
+ "adjacencies", List.of(
+ List.of("columns", "spreadsheet"),
+ List.of("cancel", "columns"),
+ List.of("cancel", "edit"),
+ List.of("cancel", "settings"),
+ List.of("columns", "edit"),
+ List.of("columns", "settings"),
+ List.of("edit", "settings"))));
report = Json.to(run(dir, harness, fixtureFile, request), Map.class);
}
@@ -258,6 +275,62 @@ class SymbolSprite_Render_BrowserTest extends TestBase {
assertEquals(0.0, metric(BOGUS_STEM, "ink12"));
}
+ //------------------------------------------------------------------------------------------------------------------
+ // c: the four [TODO-J0451] LD-1 glyphs stay mutually distinguishable (real coverage, not a human PNG review)
+ //------------------------------------------------------------------------------------------------------------------
+
+ /** {@code [TODO-J0451]} LD-1's exact locked scope - these four stems, and no others. */
+ private static final List<String> FOUR_GLYPHS = List.of("cancel", "columns", "edit", "settings");
+
+ /**
+ * Minimum acceptable mean per-pixel luminance diff (0..255 scale; see the prober's {@code PIXEL_DIFF}, in
+ * {@code symbol-sprite-render.cjs}) between any two of {@link #FOUR_GLYPHS}, at either render size.
+ *
+ * <p>
+ * Chosen against the six pairwise diffs measured on {@code HEAD} at filing time, which ranged
+ * <b>30.66 - 50.45</b> (the tightest pair was {@code columns} vs {@code settings} at the 12px paging-pill
+ * size). This floor sits well under half the smallest of those - loose enough that ordinary antialiasing
+ * jitter from a future Playwright/Chromium pin bump should not trip it (both sides of the comparison move
+ * together across a pin bump; see the class javadoc), but tight enough that two glyphs actually drifting
+ * into each other visually - the real risk this check exists for, per the closed {@code Q2} scare where
+ * {@code columns} briefly appeared to have been left as a copy of the {@code settings} cog - would have to
+ * land far closer to identical than any plausible accidental redraw before this goes green.
+ */
+ private static final double MIN_PAIR_PIXEL_DIFF = 15.0;
+
+ /** Looks up the prober's {@code adjacencyDiffs} entry for an unordered glyph pair. */
+ private static Map<?,?> adjacencyDiff(String a, String b) {
+ var diffs = (List<?>) report.get("adjacencyDiffs");
+ assertNotNull(diffs, () -> "prober report has no adjacencyDiffs section: " + report);
+ for (var d : diffs) {
+ var m = (Map<?,?>) d;
+ var names = (List<?>) m.get("names");
+ if (names != null && names.size() == 2 && names.contains(a) && names.contains(b))
+ return m;
+ }
+ return null;
+ }
+
+ @Test void c01_fourLockedGlyphsStayMutuallyDistinguishable() {
+ for (var i = 0; i < FOUR_GLYPHS.size(); i++) {
+ for (var j = i + 1; j < FOUR_GLYPHS.size(); j++) {
+ var a = FOUR_GLYPHS.get(i);
+ var b = FOUR_GLYPHS.get(j);
+ var pair = adjacencyDiff(a, b);
+ assertNotNull(pair, () -> "no adjacencyDiffs entry for " + a + " vs " + b + "; the prober's"
+ + " adjacencies request list must carry every pairwise combination of " + FOUR_GLYPHS);
+ var ribbon = ((Number) pair.get("pixelDiffRibbon")).doubleValue();
+ var small = ((Number) pair.get("pixelDiffSmall")).doubleValue();
+ assertTrue(ribbon >= MIN_PAIR_PIXEL_DIFF, () -> a + " and " + b + " are only " + ribbon
+ + " luminance levels apart at the ribbon (16px) render size - below the "
+ + MIN_PAIR_PIXEL_DIFF + " distinguishability floor, i.e. they risk reading as the same glyph");
+ assertTrue(small >= MIN_PAIR_PIXEL_DIFF, () -> a + " and " + b + " are only " + small
+ + " luminance levels apart at the paging-pill (12px) render size - below the "
+ + MIN_PAIR_PIXEL_DIFF + " distinguishability floor");
+ }
+ }
+ }
+
//------------------------------------------------------------------------------------------------------------------
// b: the review package exists (it is a deliverable, so its absence is a failure and not a warning)
//------------------------------------------------------------------------------------------------------------------
diff --git a/juneau-rest/juneau-rest-server-views/src/test/js/symbol-sprite-render.cjs b/juneau-rest/juneau-rest-server-views/src/test/js/symbol-sprite-render.cjs
index 3a544498ba..70ba74679f 100644
--- a/juneau-rest/juneau-rest-server-views/src/test/js/symbol-sprite-render.cjs
+++ b/juneau-rest/juneau-rest-server-views/src/test/js/symbol-sprite-render.cjs
@@ -42,6 +42,14 @@
* solid pixels whose luminance is <= 60 (i.e. essentially full-strength ink)
* gradient mean |luminance(x+1,y) - luminance(x,y)| over the box; higher means crisper edges
* mush share of the box in the ambiguous 60..215 luminance band; higher means smeared strokes
+ *
+ * ADJACENCY PIXEL DIFF IS NOT ONE OF THE ABOVE, AND IS ASSERTABLE. The four metrics above describe one glyph in
+ * isolation, so a Chromium pin bump that shifts antialiasing coverage globally shifts them too - that is why the
+ * Java side must not threshold them (see that class's javadoc). `adjacencyDiffs` is different in kind: it is the
+ * mean per-pixel luminance difference (0..255 scale) between two glyphs rasterised in the *same* run, at the
+ * *same* size, through the *same* Chromium build. A pin bump that changes antialiasing changes both sides of the
+ * comparison together, so the pairwise diff is far more stable across a pin bump than either side's absolute
+ * metrics are - that is what makes it safe for the Java side to assert a floor on it.
*/
'use strict';
@@ -241,6 +249,45 @@ const MEASURE_AND_MAGNIFY = async function (args) {
};
};
+/**
+ * Mean per-pixel luminance difference (0..255 scale) between two equal-size rasters, decoded fresh from their
+ * data URLs. This is the pairwise-distinguishability half of the `adjacencies` plumbing: {@link
+ * MEASURE_AND_MAGNIFY} answers "does this glyph have ink"; this answers "do these two glyphs look different from
+ * each other" - the question family drift (e.g. `columns` redrawn to look like `settings`) actually turns on.
+ */
+const PIXEL_DIFF = async function (args) {
+ const decode = function (dataUrl) {
+ return new Promise(function (resolve, reject) {
+ const img = new Image();
+ img.onload = function () { resolve(img); };
+ img.onerror = function () { reject(new Error('could not decode a raster for pixel diff')); };
+ img.src = dataUrl;
+ });
+ };
+ const toLum = function (img) {
+ const c = document.createElement('canvas');
+ c.width = img.naturalWidth;
+ c.height = img.naturalHeight;
+ const ctx = c.getContext('2d', { willReadFrequently: true });
+ ctx.drawImage(img, 0, 0);
+ const px = ctx.getImageData(0, 0, c.width, c.height).data;
+ const lum = new Float64Array(c.width * c.height);
+ for (let i = 0; i < lum.length; i++)
+ lum[i] = 0.2126 * px[i * 4] + 0.7152 * px[i * 4 + 1] + 0.0722 * px[i * 4 + 2];
+ return { lum: lum, width: c.width, height: c.height };
+ };
+ const [imgA, imgB] = await Promise.all([decode(args.dataUrlA), decode(args.dataUrlB)]);
+ const a = toLum(imgA);
+ const b = toLum(imgB);
+ if (a.width !== b.width || a.height !== b.height)
+ throw new Error('pixel diff requires equal-size rasters; got ' + a.width + 'x' + a.height + ' vs '
+ + b.width + 'x' + b.height);
+ let sum = 0;
+ for (let i = 0; i < a.lum.length; i++)
+ sum += Math.abs(a.lum[i] - b.lum[i]);
+ return { meanAbsDiff: Math.round((sum / a.lum.length) * 1000) / 1000 };
+};
+
/** Composites a sheet from already-rendered tiles. Pure canvas drawing; no measurement happens here. */
const COMPOSE = async function (spec) {
const load = function (dataUrl) {
@@ -395,7 +442,14 @@ async function shoot(page) {
await page.evaluate(RENDER_PAIR, { markups: pair.map(p => markup[p]), size: ribbon, ink: INK });
const dataUrl = await shoot(page);
const m = await page.evaluate(MEASURE_AND_MAGNIFY, { dataUrl: dataUrl, mag: MAG });
- pairs.push({ names: pair, tile: m, dataUrl: dataUrl });
+ // Reuses the per-stem tiles already rasterised for sheet.png - no extra rendering needed, since
+ // PIXEL_DIFF just re-decodes the two dataUrls already sitting in `tiles`.
+ const diffRibbon = await page.evaluate(PIXEL_DIFF,
+ { dataUrlA: tiles[pair[0]][ribbon].dataUrl, dataUrlB: tiles[pair[1]][ribbon].dataUrl });
+ const diffSmall = await page.evaluate(PIXEL_DIFF,
+ { dataUrlA: tiles[pair[0]][small].dataUrl, dataUrlB: tiles[pair[1]][small].dataUrl });
+ pairs.push({ names: pair, tile: m, dataUrl: dataUrl,
+ pixelDiff: { ribbon: diffRibbon.meanAbsDiff, small: diffSmall.meanAbsDiff } });
}
const famColW = ribbon * MAG + 40;
@@ -521,6 +575,17 @@ async function shoot(page) {
}
}
lines.push('');
+ lines.push('NAMED ADJACENCY PIXEL DIFFS - mean per-pixel luminance diff (0..255) between two glyphs at');
+ lines.push('the same size, same run, same Chromium build; unlike the metrics above, THIS one is asserted');
+ lines.push('on (see SymbolSprite_Render_BrowserTest) because both sides move together across a pin bump.');
+ lines.push('');
+ lines.push(pad('pair', 26) + num('ribbonPx', 10) + num('smallPx', 10));
+ lines.push('-'.repeat(46));
+ pairs.forEach(function (p) {
+ lines.push(pad(p.names.join(' vs '), 26) + num(p.pixelDiff.ribbon.toFixed(3), 10)
+ + num(p.pixelDiff.small.toFixed(3), 10));
+ });
+ lines.push('');
fs.writeFileSync(path.join(req.outputDir, 'metrics.txt'), lines.join('\n'));
//--------------------------------------------------------------------------------------------------------
@@ -544,6 +609,9 @@ async function shoot(page) {
sizes: sizes,
glyphs: glyphs,
familySpread: spreads,
+ adjacencyDiffs: pairs.map(function (p) {
+ return { names: p.names, pixelDiffRibbon: p.pixelDiff.ribbon, pixelDiffSmall: p.pixelDiff.small };
+ }),
outputDir: req.outputDir,
jsFailures: failures.slice()
}, null, 2) + '\n');
diff --git a/juneau-rest/juneau-rest-server-views/src/test/resources/org/apache/juneau/rest/server/views/Host.properties b/juneau-rest/juneau-rest-server-views/src/test/resources/org/apache/juneau/rest/server/views/LocalizationChromeResolutionHost.properties
similarity index 100%
rename from juneau-rest/juneau-rest-server-views/src/test/resources/org/apache/juneau/rest/server/views/Host.properties
rename to juneau-rest/juneau-rest-server-views/src/test/resources/org/apache/juneau/rest/server/views/LocalizationChromeResolutionHost.properties