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/10/21 23:26:02 UTC

(juneau) branch master updated: Add await/awaitFile test helpers to BasicTestUtils and use them in the TerminalProcess tests, and make the Orders-day browser test wait for the revenue card before continuing.

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 3d87d7bc06 Add await/awaitFile test helpers to BasicTestUtils and use them in the TerminalProcess tests, and make the Orders-day browser test wait for the revenue card before continuing.
3d87d7bc06 is described below

commit 3d87d7bc06c362e248cd924b7b1a870b6352d7fa
Author: James Bognar <ja...@apache.org>
AuthorDate: Wed Oct 21 19:25:56 2026 -0400

    Add await/awaitFile test helpers to BasicTestUtils and use them in the TerminalProcess tests, and make the Orders-day browser test wait for the revenue card before continuing.
---
 .../java/org/apache/juneau/BasicTestUtils.java     | 46 ++++++++++++
 .../org/apache/juneau/BasicTestUtils_Test.java     | 86 ++++++++++++++++++++++
 .../browser/PetstoreOrdersDay_BrowserTest.java     |  4 +-
 .../server/terminal/TerminalProcessInput_Test.java | 25 +++----
 .../server/terminal/TerminalProcessNudge_Test.java | 21 ++----
 .../terminal/TerminalProcessPrompts_Test.java      |  8 +-
 .../rest/server/terminal/TerminalProcess_Test.java | 15 ++--
 7 files changed, 160 insertions(+), 45 deletions(-)

diff --git a/juneau-core/juneau-test-utils/src/main/java/org/apache/juneau/BasicTestUtils.java b/juneau-core/juneau-test-utils/src/main/java/org/apache/juneau/BasicTestUtils.java
index c55f41b60c..9f9adabc3f 100644
--- a/juneau-core/juneau-test-utils/src/main/java/org/apache/juneau/BasicTestUtils.java
+++ b/juneau-core/juneau-test-utils/src/main/java/org/apache/juneau/BasicTestUtils.java
@@ -22,7 +22,10 @@ import static org.junit.jupiter.api.Assertions.*;
 
 import java.io.*;
 import java.net.*;
+import java.nio.file.*;
+import java.time.*;
 import java.util.*;
+import java.util.function.*;
 import java.util.stream.*;
 
 import org.apache.juneau.commons.utils.*;
@@ -46,6 +49,9 @@ import org.junit.jupiter.api.*;
 })
 public abstract class BasicTestUtils extends Shorts {
 
+	/** How long {@link #await(BooleanSupplier, Duration)} sleeps between checks of its condition. */
+	private static final long AWAIT_INTERVAL_MS = 25;
+
 	private static final ThreadLocal<TimeZone> SYSTEM_TIME_ZONE = new ThreadLocal<>();
 
 	/** Holds the previous default locale while a test overrides it via {@link #setLocale(Locale)}. */
@@ -182,6 +188,46 @@ public abstract class BasicTestUtils extends Shorts {
 		return safe(()->new URI(value).toURL());
 	}
 
+	/**
+	 * Polls a condition until it holds or the timeout elapses.
+	 *
+	 * <p>
+	 * For tests that wait on state with no signal to block on, such as a file another process writes.  Use this
+	 * instead of a hand-rolled sleep loop.  The condition is checked once more after the last sleep, so one that comes
+	 * true right at the deadline still counts.
+	 *
+	 * @param condition The condition to wait for.  Must not be <jk>null</jk>.
+	 * @param timeout How long to wait.  Must not be <jk>null</jk>.
+	 * @return <jk>true</jk> once the condition holds, or <jk>false</jk> if it still does not hold after the timeout.
+	 * @throws InterruptedException If the thread is interrupted while waiting.
+	 */
+	@SuppressWarnings({
+		"java:S2925" // The one shared bounded poll; the state it waits on has no signal to block on.
+	})
+	public static boolean await(BooleanSupplier condition, Duration timeout) throws InterruptedException {
+		var deadline = System.nanoTime() + timeout.toNanos();
+		while (! condition.getAsBoolean()) {
+			var left = deadline - System.nanoTime();
+			if (left <= 0)
+				return false;
+			Thread.sleep(Math.min(AWAIT_INTERVAL_MS, Duration.ofNanos(left).toMillis() + 1));
+		}
+		return true;
+	}
+
+	/**
+	 * Waits until a file exists or the timeout elapses.
+	 *
+	 * @param path The file to wait for.  Must not be <jk>null</jk>.
+	 * @param timeout How long to wait.  Must not be <jk>null</jk>.
+	 * @return <jk>true</jk> once the file exists, or <jk>false</jk> if it still does not exist after the timeout.
+	 * @throws InterruptedException If the thread is interrupted while waiting.
+	 * @see #await(BooleanSupplier, Duration)
+	 */
+	public static boolean awaitFile(Path path, Duration timeout) throws InterruptedException {
+		return await(() -> Files.exists(path), timeout);
+	}
+
 	/**
 	 * Returns an empty array of the specified type.
 	 *
diff --git a/juneau-core/juneau-test-utils/src/test/java/org/apache/juneau/BasicTestUtils_Test.java b/juneau-core/juneau-test-utils/src/test/java/org/apache/juneau/BasicTestUtils_Test.java
new file mode 100644
index 0000000000..bbc4e7ced8
--- /dev/null
+++ b/juneau-core/juneau-test-utils/src/test/java/org/apache/juneau/BasicTestUtils_Test.java
@@ -0,0 +1,86 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau;
+
+import static org.apache.juneau.BasicTestUtils.*;
+import static org.apache.juneau.commons.utils.ThrowableUtils.*;
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.nio.file.*;
+import java.time.*;
+import java.util.concurrent.*;
+import java.util.concurrent.atomic.*;
+
+import org.junit.jupiter.api.*;
+import org.junit.jupiter.api.io.*;
+
+class BasicTestUtils_Test extends TestBase {
+
+	@TempDir Path dir;
+
+	//------------------------------------------------------------------------------------------------------------------
+	// await(BooleanSupplier, Duration)
+	//------------------------------------------------------------------------------------------------------------------
+
+	@Test void a01_await_trueImmediately() throws Exception {
+		var calls = new AtomicInteger();
+		var t0 = System.nanoTime();
+		assertTrue(await(() -> calls.incrementAndGet() > 0, Duration.ofSeconds(10)));
+		assertEquals(1, calls.get());
+		assertTrue(Duration.ofNanos(System.nanoTime() - t0).toMillis() < 1000, "an already-true condition should not wait");
+	}
+
+	@Test void a02_await_trueAfterDelay() throws Exception {
+		var calls = new AtomicInteger();
+		assertTrue(await(() -> calls.incrementAndGet() >= 3, Duration.ofSeconds(10)));
+		assertEquals(3, calls.get());
+	}
+
+	@Test void a03_await_falseOnTimeout() throws Exception {
+		var t0 = System.nanoTime();
+		assertFalse(await(() -> false, Duration.ofMillis(200)));
+		var ms = Duration.ofNanos(System.nanoTime() - t0).toMillis();
+		assertTrue(ms >= 200, "returned before the timeout: " + ms + " ms");
+		assertTrue(ms < 5000, "overran the timeout: " + ms + " ms");
+	}
+
+	@Test void a04_await_zeroTimeoutChecksOnce() throws Exception {
+		var calls = new AtomicInteger();
+		assertFalse(await(() -> calls.incrementAndGet() < 0, Duration.ZERO));
+		assertEquals(1, calls.get());
+	}
+
+	//------------------------------------------------------------------------------------------------------------------
+	// awaitFile(Path, Duration)
+	//------------------------------------------------------------------------------------------------------------------
+
+	@Test void b01_awaitFile_exists() throws Exception {
+		var f = Files.writeString(dir.resolve("f"), "x");
+		assertTrue(awaitFile(f, Duration.ofSeconds(10)));
+	}
+
+	@Test void b02_awaitFile_createdLater() throws Exception {
+		var f = dir.resolve("f");
+		var later = CompletableFuture.delayedExecutor(100, TimeUnit.MILLISECONDS);
+		CompletableFuture.runAsync(() -> safe(() -> Files.writeString(f, "x")), later);
+		assertTrue(awaitFile(f, Duration.ofSeconds(10)));
+	}
+
+	@Test void b03_awaitFile_missing() throws Exception {
+		assertFalse(awaitFile(dir.resolve("missing"), Duration.ofMillis(100)));
+	}
+}
diff --git a/juneau-petstore/juneau-petstore-core/src/test/java/org/apache/juneau/petstore/console/browser/PetstoreOrdersDay_BrowserTest.java b/juneau-petstore/juneau-petstore-core/src/test/java/org/apache/juneau/petstore/console/browser/PetstoreOrdersDay_BrowserTest.java
index 7ebbf5d50e..3fa5b3a1fb 100644
--- a/juneau-petstore/juneau-petstore-core/src/test/java/org/apache/juneau/petstore/console/browser/PetstoreOrdersDay_BrowserTest.java
+++ b/juneau-petstore/juneau-petstore-core/src/test/java/org/apache/juneau/petstore/console/browser/PetstoreOrdersDay_BrowserTest.java
@@ -138,9 +138,9 @@ class PetstoreOrdersDay_BrowserTest extends TestBase {
 			trail());
 	}
 
-	/** Waits for the Store page to be back, fully loaded. */
+	/** Waits for the Store page to be back, fully loaded, with its revenue card rendered. */
 	private static List<Map<String,Object>> storeAgain() {
-		return List.of(wait("location.pathname === '/console/store' && document.readyState === 'complete'"), trail());
+		return List.of(wait("location.pathname === '/console/store' && document.readyState === 'complete'"), wait("!!document.querySelector('#revenue-body .petstore-kpi-value')"), trail());
 	}
 
 	/** Flattens actions and action lists, in order. */
diff --git a/juneau-rest/juneau-rest-server-terminal/src/test/java/org/apache/juneau/rest/server/terminal/TerminalProcessInput_Test.java b/juneau-rest/juneau-rest-server-terminal/src/test/java/org/apache/juneau/rest/server/terminal/TerminalProcessInput_Test.java
index a65d52a399..97bc5010f1 100644
--- a/juneau-rest/juneau-rest-server-terminal/src/test/java/org/apache/juneau/rest/server/terminal/TerminalProcessInput_Test.java
+++ b/juneau-rest/juneau-rest-server-terminal/src/test/java/org/apache/juneau/rest/server/terminal/TerminalProcessInput_Test.java
@@ -17,11 +17,14 @@
 package org.apache.juneau.rest.server.terminal;
 
 import static java.nio.charset.StandardCharsets.*;
+import static org.apache.juneau.BasicTestUtils.*;
+import static org.apache.juneau.commons.utils.ThrowableUtils.*;
 import static org.junit.jupiter.api.Assertions.*;
 import static org.junit.jupiter.api.Assumptions.*;
 
 import java.nio.file.*;
 import java.nio.file.attribute.*;
+import java.time.*;
 import java.util.*;
 import java.util.concurrent.*;
 import java.util.concurrent.atomic.*;
@@ -36,9 +39,6 @@ import org.junit.jupiter.api.io.*;
  * {@link TerminalProcess} input mode, over a fake {@code python3} runner, and over the real {@code juneau_run.py} for
  * the bytes reaching the job.
  */
-@SuppressWarnings({
-	"java:S2925" // Timing-dependent process/file test; it polls or waits on real time
-})
 @DisabledOnOs(OS.WINDOWS)
 class TerminalProcessInput_Test extends TestBase {
 
@@ -67,9 +67,7 @@ class TerminalProcessInput_Test extends TestBase {
 	}
 
 	private long awaitPid() throws Exception {
-		for (var i = 0; i < 200 && ! Files.exists(pidFile); i++)
-			Thread.sleep(25);
-		assertTrue(Files.exists(pidFile), "the fake runner never got ready");
+		assertTrue(awaitFile(pidFile, Duration.ofSeconds(5)), "the fake runner never got ready");
 		return Long.parseLong(Files.readString(pidFile).trim());
 	}
 
@@ -159,9 +157,7 @@ class TerminalProcessInput_Test extends TestBase {
 	 * soon as the marker shows, so the limit only matters when something is really wrong.
 	 */
 	private static void awaitLog(TerminalProcess p, String marker) throws Exception {
-		for (var i = 0; i < 2400 && ! log(p).contains(marker); i++)
-			Thread.sleep(25);
-		assertTrue(log(p).contains(marker), () -> "never printed " + marker + ": " + uncheckedLog(p));
+		assertTrue(await(() -> safe(() -> log(p).contains(marker)), Duration.ofSeconds(60)), () -> "never printed " + marker + ": " + uncheckedLog(p));
 	}
 
 	private static String uncheckedLog(TerminalProcess p) {
@@ -241,6 +237,9 @@ class TerminalProcessInput_Test extends TestBase {
 		}
 	}
 
+	@SuppressWarnings({
+		"java:S2925" // Deliberate pause after close(): it gives the writer thread time to try a write that must fail.
+	})
 	@Test void a09_writeDuringClose() throws Exception {
 		sleepingRunner("");
 		try (var p = start(true)) {
@@ -276,9 +275,7 @@ class TerminalProcessInput_Test extends TestBase {
 				assertTrue(busy.await(10, TimeUnit.SECONDS), "the buffer never filled");
 				p.close();
 				closed.set(true);
-				for (var i = 0; i < 40 && writer.isAlive(); i++)
-					Thread.sleep(25);
-				assertFalse(writer.isAlive(), "the writer thread outlived close() by 1 s");
+				assertTrue(await(() -> ! writer.isAlive(), Duration.ofSeconds(1)), "the writer thread outlived close() by 1 s");
 				Thread.sleep(100);
 			} finally {
 				stop.set(true);
@@ -295,9 +292,7 @@ class TerminalProcessInput_Test extends TestBase {
 			awaitPid();
 			assertTrue(p.acceptsInput());
 			assertTrue(p.write(new byte[]{'x'}), "the write is queued; the broken pipe is the writer thread's to find");
-			for (var i = 0; i < 200 && p.acceptsInput(); i++)
-				Thread.sleep(25);
-			assertFalse(p.acceptsInput(), "a broken pipe turns input off");
+			assertTrue(await(() -> ! p.acceptsInput(), Duration.ofSeconds(5)), "a broken pipe turns input off");
 			assertFalse(p.write(new byte[]{'y'}));
 		}
 	}
diff --git a/juneau-rest/juneau-rest-server-terminal/src/test/java/org/apache/juneau/rest/server/terminal/TerminalProcessNudge_Test.java b/juneau-rest/juneau-rest-server-terminal/src/test/java/org/apache/juneau/rest/server/terminal/TerminalProcessNudge_Test.java
index 997d16edad..ed6a51882c 100644
--- a/juneau-rest/juneau-rest-server-terminal/src/test/java/org/apache/juneau/rest/server/terminal/TerminalProcessNudge_Test.java
+++ b/juneau-rest/juneau-rest-server-terminal/src/test/java/org/apache/juneau/rest/server/terminal/TerminalProcessNudge_Test.java
@@ -16,10 +16,12 @@
  */
 package org.apache.juneau.rest.server.terminal;
 
+import static org.apache.juneau.BasicTestUtils.*;
 import static org.junit.jupiter.api.Assertions.*;
 
 import java.nio.file.*;
 import java.nio.file.attribute.*;
+import java.time.*;
 import java.util.*;
 import java.util.concurrent.*;
 
@@ -33,9 +35,6 @@ import org.junit.jupiter.api.io.*;
 /**
  * {@link TerminalProcess} nudges, over a fake {@code python3} runner.
  */
-@SuppressWarnings({
-	"java:S2925" // Timing-dependent process/file test; it polls or waits on real time
-})
 @DisabledOnOs(OS.WINDOWS)
 class TerminalProcessNudge_Test extends TestBase {
 
@@ -71,10 +70,7 @@ class TerminalProcessNudge_Test extends TestBase {
 		var seen = new CopyOnWriteArrayList<Nudged>();
 		try (var p = start()) {
 			p.nudges("t1", (id, end, done) -> seen.add(new Nudged(id, end, done)));
-			var deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5);
-			while (seen.stream().noneMatch(Nudged::done) && System.nanoTime() < deadline)
-				Thread.sleep(25);
-			assertTrue(seen.stream().anyMatch(Nudged::done), "no done nudge: " + seen);
+			assertTrue(await(() -> seen.stream().anyMatch(Nudged::done), Duration.ofSeconds(5)), () -> "no done nudge: " + seen);
 			assertEquals(1, seen.stream().filter(Nudged::done).count());
 			var ends = seen.stream().filter(n -> ! n.done()).mapToLong(Nudged::end).toArray();
 			for (var i = 1; i < ends.length; i++)
@@ -108,6 +104,9 @@ class TerminalProcessNudge_Test extends TestBase {
 		awaitDead(t, 2000);
 	}
 
+	@SuppressWarnings({
+		"java:S2925" // Deliberate delay: the fake exit hook is slow on purpose, so close() has a wait to observe.
+	})
 	@Test void a03b_closeStopsWatcherFirst() throws Exception {
 		fakePython("exec sleep 30");
 		// The exit hook is slow, so close() waits about two seconds for it; the watcher must not run through that wait.
@@ -157,12 +156,8 @@ class TerminalProcessNudge_Test extends TestBase {
 				});
 				try {
 					assertEquals(0, p.waitFor(30, TimeUnit.SECONDS));
-					var deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5);
-					Thread t;
-					while ((t = Thread.getAllStackTraces().keySet().stream().filter(x -> x.getName().startsWith("juneau-terminal-nudge-")
-						&& x.isAlive()).findFirst().orElse(null)) != null && System.nanoTime() < deadline)
-						t.join(50);
-					assertNull(t, "the watcher thread did not exit");
+					assertTrue(await(() -> Thread.getAllStackTraces().keySet().stream().noneMatch(x -> x.getName().startsWith("juneau-terminal-nudge-")
+						&& x.isAlive()), Duration.ofSeconds(5)), "the watcher thread did not exit");
 				} catch (Exception e) {
 					throw new AssertionError(e);
 				}
diff --git a/juneau-rest/juneau-rest-server-terminal/src/test/java/org/apache/juneau/rest/server/terminal/TerminalProcessPrompts_Test.java b/juneau-rest/juneau-rest-server-terminal/src/test/java/org/apache/juneau/rest/server/terminal/TerminalProcessPrompts_Test.java
index 905357d9f9..61e41422ac 100644
--- a/juneau-rest/juneau-rest-server-terminal/src/test/java/org/apache/juneau/rest/server/terminal/TerminalProcessPrompts_Test.java
+++ b/juneau-rest/juneau-rest-server-terminal/src/test/java/org/apache/juneau/rest/server/terminal/TerminalProcessPrompts_Test.java
@@ -17,10 +17,12 @@
 package org.apache.juneau.rest.server.terminal;
 
 import static java.nio.charset.StandardCharsets.*;
+import static org.apache.juneau.BasicTestUtils.*;
 import static org.junit.jupiter.api.Assertions.*;
 import static org.junit.jupiter.api.Assumptions.*;
 
 import java.nio.file.*;
+import java.time.*;
 import java.util.*;
 import java.util.concurrent.*;
 import java.util.concurrent.atomic.*;
@@ -142,14 +144,10 @@ class TerminalProcessPrompts_Test extends TestBase {
 			() -> TerminalProcess.start("generic", cmd, 80, 24, run, (TerminalPrompts)null));
 	}
 
-	@SuppressWarnings({
-		"java:S2925" // Bounded poll for the runner's log file, which has no signal to wait on.
-	})
 	@Test void a05_closeEndsRunnerCancelled() throws Exception {
 		var sink = new Sink();
 		var p = startReal(List.of("python3", "-c", "import time; time.sleep(60)"), sink);
-		for (var i = 0; i < 2400 && ! Files.exists(p.log()); i++)
-			Thread.sleep(25);
+		awaitFile(p.log(), Duration.ofSeconds(60));
 		var t0 = System.nanoTime();
 		p.close();
 		assertTrue(System.nanoTime() - t0 < TimeUnit.SECONDS.toNanos(10), "close() took too long");
diff --git a/juneau-rest/juneau-rest-server-terminal/src/test/java/org/apache/juneau/rest/server/terminal/TerminalProcess_Test.java b/juneau-rest/juneau-rest-server-terminal/src/test/java/org/apache/juneau/rest/server/terminal/TerminalProcess_Test.java
index 1bdc7591a6..8a462ce6d1 100644
--- a/juneau-rest/juneau-rest-server-terminal/src/test/java/org/apache/juneau/rest/server/terminal/TerminalProcess_Test.java
+++ b/juneau-rest/juneau-rest-server-terminal/src/test/java/org/apache/juneau/rest/server/terminal/TerminalProcess_Test.java
@@ -21,6 +21,7 @@ import static org.junit.jupiter.api.Assertions.*;
 
 import java.nio.file.*;
 import java.nio.file.attribute.*;
+import java.time.*;
 import java.util.*;
 import java.util.concurrent.*;
 
@@ -33,9 +34,6 @@ import org.junit.jupiter.api.*;
 import org.junit.jupiter.api.condition.*;
 import org.junit.jupiter.api.io.*;
 
-@SuppressWarnings({
-	"java:S2925" // Timing-dependent process/file test; it polls or waits on real time
-})
 class TerminalProcess_Test extends TestBase {
 
 	@TempDir Path dir;
@@ -221,15 +219,12 @@ class TerminalProcess_Test extends TestBase {
 		var p = TerminalProcess.start("generic", List.of("true"), 80, 24, run, host("/x.py", null));
 		long pid = -1;
 		try {
-			for (var i = 0; i < 200 && ! Files.exists(pidFile); i++)
-				Thread.sleep(50);
-			assertTrue(Files.exists(pidFile), "the grandchild never started");
+			assertTrue(awaitFile(pidFile, Duration.ofSeconds(10)), "the grandchild never started");
 			pid = Long.parseLong(Files.readString(pidFile).trim());
-			assertTrue(ProcessHandle.of(pid).map(ProcessHandle::isAlive).orElse(false), "the grandchild should be running");
+			var grandchild = ProcessHandle.of(pid);
+			assertTrue(grandchild.map(ProcessHandle::isAlive).orElse(false), "the grandchild should be running");
 			p.close();
-			for (var i = 0; i < 100 && ProcessHandle.of(pid).map(ProcessHandle::isAlive).orElse(false); i++)
-				Thread.sleep(50);
-			assertFalse(ProcessHandle.of(pid).map(ProcessHandle::isAlive).orElse(false), "the grandchild survived close()");
+			assertTrue(await(() -> ! grandchild.map(ProcessHandle::isAlive).orElse(false), Duration.ofSeconds(5)), "the grandchild survived close()");
 		} finally {
 			if (pid > 0)
 				ProcessHandle.of(pid).ifPresent(ProcessHandle::destroyForcibly);