This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new 7ab6c2946729 CAMEL-24485: windows-validator - stop killing
slow-but-passing PowerShell suite runs
7ab6c2946729 is described below
commit 7ab6c2946729c6a2ce2bf3f47c73d738e16441aa
Author: Adriano Machado <[email protected]>
AuthorDate: Fri Sep 18 12:53:32 2026 -0400
CAMEL-24485: windows-validator - stop killing slow-but-passing PowerShell
suite runs
The windows-validator job of the Package-native validation workflow
failed in 33 of 54 completed runs; 19 of those were surefire fork
timeouts. WebsiteInstallTest$WindowsPowerShell takes 626-1198s on
windows-latest, straddling camel-launcher's 1200s forkTimeout, so
slow-but-passing runs were being killed (the "Connection reset" in the
ticket is just the Develocity capture thread losing its socket).
- Pass -Dcamel.surefire.forkTimeout=2700 to the Windows test step and
cap the job with timeout-minutes: 60 so a real hang still fails fast.
- WebsiteInstallerFixture logs one timing line per spawned process and
the job archives target/surefire-reports (if: always()) so timings
survive a fork kill.
- Share one PSModuleAnalysisCachePath across the run and pass TEMP/TMP
through, so each powershell.exe launch stops rebuilding its module
cache and Add-Type scratch files in the per-test home.
- Extract the bounded runner into WebsiteInstallerFixture.execute() and
use it at all 9 helper call sites; the previous readAllBytes()-before-
waitFor pattern meant the 30s timeout never actually applied.
Closes #26568
Co-authored-by: Claude Code <[email protected]>
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
---
.github/workflows/package-native-validation.yml | 18 ++++++-
.../dsl/jbang/launcher/WebsiteInstallTest.java | 58 +++++++---------------
.../jbang/launcher/WebsiteInstallerFixture.java | 33 +++++++++++-
3 files changed, 68 insertions(+), 41 deletions(-)
diff --git a/.github/workflows/package-native-validation.yml
b/.github/workflows/package-native-validation.yml
index 8a98b82c561d..faeb7df8e919 100644
--- a/.github/workflows/package-native-validation.yml
+++ b/.github/workflows/package-native-validation.yml
@@ -150,6 +150,8 @@ jobs:
# ── Windows (install.ps1 unit tests) ──────────────────────────────
windows-validator:
runs-on: windows-latest
+ # Caps a genuine hang well below GitHub's 6h default; a passing run has
taken up to ~25 minutes.
+ timeout-minutes: 60
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #
v7.0.1
with:
@@ -184,6 +186,20 @@ jobs:
- name: Run Windows installer unit tests
shell: bash
+ # The PowerShell suite has taken 10-20 minutes on windows-latest,
straddling camel-launcher's 1200s
+ # surefire fork timeout, so slow-but-passing runs were killed. Give
this job's fork more room.
run: |
cd dsl/camel-jbang/camel-launcher
- mvn -B -ntp test -pl . -Dtest=WebsiteInstallTest
+ mvn -B -ntp test -pl . -Dtest=WebsiteInstallTest
-Dcamel.surefire.forkTimeout=2700
+
+ # Per-test timings for the PowerShell suite, which runs close to
camel-launcher's surefire fork timeout.
+ # A fork killed on that timeout writes no XML report; the per-process
timings the fixture prints to the
+ # build log cover that case.
+ - name: Archive surefire reports
+ if: always()
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
# v7.0.1
+ with:
+ name: windows-validator-surefire-reports
+ path: dsl/camel-jbang/camel-launcher/target/surefire-reports
+ retention-days: 7
+ if-no-files-found: ignore
diff --git
a/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/WebsiteInstallTest.java
b/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/WebsiteInstallTest.java
index 0fbda6496e9d..a514a7c10828 100644
---
a/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/WebsiteInstallTest.java
+++
b/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/WebsiteInstallTest.java
@@ -34,7 +34,6 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
-import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;
import org.junit.jupiter.api.AfterEach;
@@ -152,11 +151,8 @@ class WebsiteInstallTest {
Path versionDir = home.resolve(".local/share/camel-cli/versions/"
+ version);
assertTrue(Files.isDirectory(versionDir), "expected version
directory " + versionDir);
- Process check = new ProcessBuilder("/bin/sh", shim.toString(),
"version")
- .redirectErrorStream(true)
- .start();
- String output = new String(check.getInputStream().readAllBytes(),
StandardCharsets.UTF_8);
- check.waitFor();
+ String output = WebsiteInstallerFixture.execute(
+ new ProcessBuilder("/bin/sh", shim.toString(),
"version").redirectErrorStream(true)).stdout();
assertTrue(output.contains("Camel " + version), output);
}
@@ -672,9 +668,8 @@ class WebsiteInstallTest {
+ "if ($path) { $kind = $k.GetValueKind('Path'); "
+ "$kept = ($path -split ';') | Where-Object { $_
-and (-not ($dirs -icontains $_)) }; "
+ "$k.SetValue('Path', ($kept -join ';'), $kind) }
$k.Close() }");
- Process cleanup = new ProcessBuilder("powershell", "-NoProfile",
"-Command", script.toString())
- .redirectErrorStream(true).start();
- cleanup.waitFor(30, TimeUnit.SECONDS);
+ WebsiteInstallerFixture.execute(
+ new ProcessBuilder("powershell", "-NoProfile", "-Command",
script.toString()).redirectErrorStream(true));
}
private WebsiteInstallerFixture.Result install(
@@ -716,11 +711,8 @@ class WebsiteInstallTest {
assertTrue(Files.isDirectory(versionDir(home, version)),
"expected version directory " + versionDir(home, version));
- Process check = new ProcessBuilder("cmd.exe", "/c",
shim.toString(), "version")
- .redirectErrorStream(true)
- .start();
- String output = new String(check.getInputStream().readAllBytes(),
StandardCharsets.UTF_8);
- check.waitFor(30, TimeUnit.SECONDS);
+ String output = WebsiteInstallerFixture.execute(
+ new ProcessBuilder("cmd.exe", "/c", shim.toString(),
"version").redirectErrorStream(true)).stdout();
assertTrue(output.contains("Camel " + version), output);
}
@@ -740,13 +732,10 @@ class WebsiteInstallTest {
}
private static String queryEnvironmentPath(String scope) throws
Exception {
- Process p = new ProcessBuilder(
+ return WebsiteInstallerFixture.execute(new ProcessBuilder(
"powershell", "-NoProfile", "-Command",
"[Environment]::GetEnvironmentVariable('Path','" + scope +
"')")
- .redirectErrorStream(true).start();
- String out = new String(p.getInputStream().readAllBytes(),
StandardCharsets.UTF_8).trim();
- p.waitFor(30, TimeUnit.SECONDS);
- return out;
+ .redirectErrorStream(true)).stdout().trim();
}
private static long countOccurrencesCaseInsensitive(String path,
String dir) {
@@ -760,15 +749,12 @@ class WebsiteInstallTest {
// Reads HKCU\Environment\Path without expanding %VAR% references,
returning "" when unset.
private static String readUserPathRaw() throws Exception {
- Process p = new ProcessBuilder(
+ return WebsiteInstallerFixture.execute(new ProcessBuilder(
"powershell", "-NoProfile", "-Command",
"$k =
[Microsoft.Win32.Registry]::CurrentUser.OpenSubKey('Environment'); "
+ "if ($k) {
[Console]::Out.Write($k.GetValue('Path', '', "
- +
"[Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames));
$k.Close() }")
- .start();
- String out = new String(p.getInputStream().readAllBytes(),
StandardCharsets.UTF_8);
- p.waitFor(30, TimeUnit.SECONDS);
- return out;
+ +
"[Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames));
$k.Close() }"))
+ .stdout();
}
// Writes HKCU\Environment\Path as a REG_EXPAND_SZ value; the value is
passed via the environment
@@ -781,21 +767,18 @@ class WebsiteInstallTest {
+
"[Microsoft.Win32.RegistryValueKind]::ExpandString); $k.Close()")
.redirectErrorStream(true);
pb.environment().put("CAMEL_TEST_USERPATH", value);
- Process p = pb.start();
- String out = new String(p.getInputStream().readAllBytes(),
StandardCharsets.UTF_8);
- if (!p.waitFor(30, TimeUnit.SECONDS) || p.exitValue() != 0) {
- throw new IllegalStateException("failed to seed user PATH: " +
out);
+ WebsiteInstallerFixture.Result r =
WebsiteInstallerFixture.execute(pb);
+ if (r.exit() != 0) {
+ throw new IllegalStateException("failed to seed user PATH: " +
r.stdout());
}
}
private static void deleteUserPath() throws Exception {
- Process p = new ProcessBuilder(
+ WebsiteInstallerFixture.execute(new ProcessBuilder(
"powershell", "-NoProfile", "-Command",
"$k =
[Microsoft.Win32.Registry]::CurrentUser.OpenSubKey('Environment', $true); "
+ "if ($k) {
$k.DeleteValue('Path', $false); $k.Close() }")
- .redirectErrorStream(true).start();
- p.getInputStream().readAllBytes();
- p.waitFor(30, TimeUnit.SECONDS);
+ .redirectErrorStream(true));
}
@Test
@@ -1039,14 +1022,11 @@ class WebsiteInstallTest {
// ("bar baz") trips cmd.exe's quote-stripping rule (it only
preserves quotes when there are
// exactly two), which would unquote the space in "Apache
Camel" and break the call. A bare
// command name resolved through PATH+PATHEXT avoids that
quirk and mirrors real usage.
- Process argsProc = shimProcess(binDir, "echo-args", "foo",
"bar baz").start();
- String argsOut = new
String(argsProc.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
- argsProc.waitFor(30, TimeUnit.SECONDS);
+ String argsOut =
WebsiteInstallerFixture.execute(shimProcess(binDir, "echo-args", "foo", "bar
baz"))
+ .stdout();
assertTrue(argsOut.contains("foo") && argsOut.contains("bar
baz"), argsOut);
- Process exitProc = shimProcess(binDir, "exit-code",
"7").start();
- exitProc.waitFor(30, TimeUnit.SECONDS);
- assertEquals(7, exitProc.exitValue());
+ assertEquals(7,
WebsiteInstallerFixture.execute(shimProcess(binDir, "exit-code", "7")).exit());
}
}
diff --git
a/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/WebsiteInstallerFixture.java
b/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/WebsiteInstallerFixture.java
index a94857d9b708..996e2eddf74f 100644
---
a/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/WebsiteInstallerFixture.java
+++
b/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/WebsiteInstallerFixture.java
@@ -64,6 +64,8 @@ final class WebsiteInstallerFixture implements AutoCloseable {
private static final String KEY_ALIAS = "camel-installer-test";
private static final String DEFAULT_BASE_VERSION = "9.9.9";
private static final Duration PROCESS_TIMEOUT = Duration.ofSeconds(60);
+ private static final Path POWERSHELL_MODULE_ANALYSIS_CACHE
+ = Path.of("target", "powershell",
"ModuleAnalysisCache").toAbsolutePath();
record Result(int exit, String stdout, String stderr) {
}
@@ -446,6 +448,20 @@ final class WebsiteInstallerFixture implements
AutoCloseable {
if (pathExt != null) {
pb.environment().put("PATHEXT", pathExt);
}
+ // Without TEMP/TMP, .NET falls back to USERPROFILE (the fresh
per-test home) for the scratch
+ // files Add-Type's C# compile writes on every install.ps1 run.
install.ps1 itself never stages
+ // anything under the temp directory, so passing the runner's
through does not affect isolation.
+ for (String name : List.of("TEMP", "TMP")) {
+ String value = System.getenv(name);
+ if (value != null) {
+ pb.environment().put(name, value);
+ }
+ }
+ // PowerShell keeps its module analysis cache under LOCALAPPDATA,
which every test points at a
+ // fresh home, so each powershell.exe launch would rebuild it from
scratch when auto-loading
+ // Expand-Archive / Get-FileHash. Share one cache file across the
whole test run instead.
+
Files.createDirectories(POWERSHELL_MODULE_ANALYSIS_CACHE.getParent());
+ pb.environment().put("PSModuleAnalysisCachePath",
POWERSHELL_MODULE_ANALYSIS_CACHE.toString());
}
pb.environment().putAll(env);
String home = env.get("HOME");
@@ -454,7 +470,16 @@ final class WebsiteInstallerFixture implements
AutoCloseable {
// wherever the test JVM happens to be running from.
pb.directory(Path.of(home).toFile());
}
+ return execute(pb);
+ }
+ /**
+ * Runs {@code pb} with stdout and stderr drained on background threads,
so a chatty or stuck child can never block
+ * the test past {@link #PROCESS_TIMEOUT}: reading a stream to EOF before
{@code waitFor} would wait for as long as
+ * the child (or any grandchild inheriting its handles) keeps the pipe
open.
+ */
+ static Result execute(ProcessBuilder pb) throws Exception {
+ long start = System.nanoTime();
Process process = pb.start();
ExecutorService collectors = Executors.newFixedThreadPool(2);
try {
@@ -463,7 +488,7 @@ final class WebsiteInstallerFixture implements
AutoCloseable {
process.getOutputStream().close();
if (!process.waitFor(PROCESS_TIMEOUT.toSeconds(),
TimeUnit.SECONDS)) {
process.destroyForcibly();
- throw new IllegalStateException("installer did not exit in
time");
+ throw new IllegalStateException("process did not exit in time:
" + pb.command());
}
String out = new String(await(stdout), StandardCharsets.UTF_8);
String err = new String(await(stderr), StandardCharsets.UTF_8);
@@ -474,6 +499,12 @@ final class WebsiteInstallerFixture implements
AutoCloseable {
}
collectors.shutdownNow();
collectors.awaitTermination(10, TimeUnit.SECONDS);
+ // Surefire streams test stdout to the build log as it happens, so
these timings survive even when
+ // the fork is killed by forkedProcessTimeoutInSeconds and no XML
report is ever written.
+ String command = String.join(" ", pb.command());
+ System.out.printf("[WebsiteInstallerFixture] %d ms: %s%n",
+ TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start),
+ command.length() > 160 ? command.substring(0, 160) + "..."
: command);
}
}