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);