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