This is an automated email from the ASF dual-hosted git repository.
olamy pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/maven-surefire.git
The following commit(s) were added to refs/heads/master by this push:
new 9e4317ea1 Fix jstack dumping the shell instead of the forked JVM
(#3549) (#3550)
9e4317ea1 is described below
commit 9e4317ea12e23263325f6fed5c3d0020a433fd2c
Author: Olivier Lamy <[email protected]>
AuthorDate: Wed Oct 28 17:26:59 2026 +1000
Fix jstack dumping the shell instead of the forked JVM (#3549) (#3550)
Signed-off-by: Olivier Lamy <[email protected]>
---
.../plugin/surefire/booterclient/ForkStarter.java | 40 +++---
.../apache/maven/surefire/booter/SystemUtils.java | 61 +++++++++
.../maven/surefire/booter/SystemUtilsTest.java | 104 ++++++++++++++++
.../jiras/Surefire3549JstackForkedJvmPidIT.java | 137 +++++++++++++++++++++
.../surefire-3549-jstack-forked-jvm-pid/pom.xml | 61 +++++++++
.../test/java/surefire3549/OtherSleepingTest.java | 39 ++++++
.../src/test/java/surefire3549/SleepingTest.java | 39 ++++++
7 files changed, 465 insertions(+), 16 deletions(-)
diff --git
a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/ForkStarter.java
b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/ForkStarter.java
index cd97c7673..b3dac8d2a 100644
---
a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/ForkStarter.java
+++
b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/ForkStarter.java
@@ -45,8 +45,10 @@
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
import org.apache.maven.plugin.surefire.CommonReflector;
+import org.apache.maven.plugin.surefire.JdkAttributes;
import org.apache.maven.plugin.surefire.StartupReportConfiguration;
import org.apache.maven.plugin.surefire.SurefireProperties;
import
org.apache.maven.plugin.surefire.booterclient.lazytestprovider.AbstractCommandReader;
@@ -113,7 +115,7 @@
import static
org.apache.maven.surefire.api.util.internal.DaemonThreadFactory.newDaemonThreadFactory;
import static org.apache.maven.surefire.api.util.internal.StringUtils.NL;
import static
org.apache.maven.surefire.booter.SystemPropertyManager.writePropertiesFile;
-import static org.apache.maven.surefire.booter.SystemUtils.pidOf;
+import static org.apache.maven.surefire.booter.SystemUtils.forkedJvmPidOf;
import static org.apache.maven.surefire.booter.SystemUtils.toJdkHomeFromJre;
import static
org.apache.maven.surefire.shared.utils.cli.ShutdownHookUtils.addShutDownHook;
import static
org.apache.maven.surefire.shared.utils.cli.ShutdownHookUtils.removeShutdownHook;
@@ -613,7 +615,7 @@ private RunResult fork(
currentForkClients.add(forkClient);
CountdownCloseable countdownCloseable =
new CountdownCloseable(eventConsumer,
forkChannel.getCountdownCloseablePermits());
- final ForkedProcessTimeoutContext[] timeoutContextHolder = new
ForkedProcessTimeoutContext[1];
+ final AtomicReference<ForkedProcessTimeoutContext> timeoutContext =
new AtomicReference<>();
try (CommandlineExecutor exec = new CommandlineExecutor(cli,
countdownCloseable)) {
forkChannel.tryConnectToClient();
CommandlineStreams streams = exec.execute();
@@ -628,18 +630,23 @@ private RunResult fork(
log.debug("Fork Channel [" + forkNumber + "] connected to the
client.");
if (timeoutExtensionDispatcher.hasExtensions() &&
forkedProcessTimeoutInSeconds > 0) {
- Long pid = pidOf(exec.getProcess());
- File javaExecutable = cli.getExecutable() == null ? null : new
File(cli.getExecutable());
- final ForkedProcessTimeoutContext context = new
DefaultForkedProcessTimeoutContext(
- pid == null ? -1L : pid,
- forkNumber,
- javaExecutable,
- startupReportConfiguration.getReportsDirectory(),
- forkedProcessTimeoutInSeconds,
- log,
- forkedProcessTimeoutExtensionContext);
- timeoutContextHolder[0] = context;
- forkClient.setTimeoutDetectedListener(() ->
timeoutExtensionDispatcher.fireTimeoutDetected(context));
+ Process process = exec.getProcess();
+ JdkAttributes jdk = forkConfiguration.getJdkForTests();
+ File javaExecutable = jdk == null ? null :
jdk.getJvmExecutable();
+ forkClient.setTimeoutDetectedListener(() -> {
+ // Find the PID now, the JVM may be a child of the shell
Surefire started
+ Long pid = forkedJvmPidOf(process, javaExecutable == null
? null : javaExecutable.getName());
+ ForkedProcessTimeoutContext context = new
DefaultForkedProcessTimeoutContext(
+ pid == null ? -1L : pid,
+ forkNumber,
+ javaExecutable,
+ startupReportConfiguration.getReportsDirectory(),
+ forkedProcessTimeoutInSeconds,
+ log,
+ forkedProcessTimeoutExtensionContext);
+ timeoutContext.set(context);
+ timeoutExtensionDispatcher.fireTimeoutDetected(context);
+ });
}
result = exec.awaitExit();
@@ -675,8 +682,9 @@ private RunResult fork(
}
forkClient.close(runResult.isTimeout());
- if (runResult.isTimeout() && timeoutContextHolder[0] != null) {
-
timeoutExtensionDispatcher.fireForkExited(timeoutContextHolder[0], runResult);
+ ForkedProcessTimeoutContext context = timeoutContext.get();
+ if (runResult.isTimeout() && context != null) {
+ timeoutExtensionDispatcher.fireForkExited(context, runResult);
}
if (!runResult.isTimeout()) {
diff --git
a/surefire-booter/src/main/java/org/apache/maven/surefire/booter/SystemUtils.java
b/surefire-booter/src/main/java/org/apache/maven/surefire/booter/SystemUtils.java
index 0cd8278ed..3600bffb6 100644
---
a/surefire-booter/src/main/java/org/apache/maven/surefire/booter/SystemUtils.java
+++
b/surefire-booter/src/main/java/org/apache/maven/surefire/booter/SystemUtils.java
@@ -27,13 +27,19 @@
import java.lang.management.ManagementFactory;
import java.lang.reflect.Method;
import java.math.BigDecimal;
+import java.util.List;
+import java.util.Locale;
+import java.util.Optional;
import java.util.Properties;
import java.util.StringTokenizer;
+import java.util.function.Function;
+import java.util.stream.Stream;
import org.apache.maven.surefire.api.util.ReflectionUtils;
import static java.lang.Thread.currentThread;
import static java.util.Objects.requireNonNull;
+import static java.util.stream.Collectors.toList;
import static
org.apache.maven.surefire.api.util.ReflectionUtils.invokeMethodChain;
import static
org.apache.maven.surefire.api.util.ReflectionUtils.invokeMethodWithArray;
import static org.apache.maven.surefire.api.util.ReflectionUtils.tryLoadClass;
@@ -224,6 +230,61 @@ public static Long pidOf(Process process) {
}
}
+ /**
+ * Returns the PID of the forked JVM.
+ * <p>
+ * Surefire starts the fork through a shell ({@code /bin/sh} or {@code
cmd.exe}). Some shells turn into
+ * {@code java}, others keep running with {@code java} as their child. In
that case this returns the child's PID
+ * instead of the shell's.
+ *
+ * @param process the process Surefire started, may be {@code null}
+ * @param jvmName name of the java executable, like {@code java} or {@code
java.exe}, may be {@code null}
+ * @return the JVM's PID, the process's own PID if no java child is found,
or {@code null} if unknown (Java 8)
+ * @since 3.6.2
+ */
+ public static Long forkedJvmPidOf(Process process, String jvmName) {
+ Long pid = pidOf(process);
+ Method toHandle = ReflectionUtils.tryGetMethod(Process.class,
"toHandle");
+ if (pid == null || jvmName == null || toHandle == null) {
+ return pid;
+ }
+ try {
+ Class<?> processHandle = toHandle.getReturnType();
+ Method children = ReflectionUtils.getMethod(processHandle,
"children");
+ Method childPid = ReflectionUtils.getMethod(processHandle, "pid");
+ Method info = ReflectionUtils.getMethod(processHandle, "info");
+ Method command = ReflectionUtils.getMethod(info.getReturnType(),
"command");
+ Function<Object, String> commandOf = handle -> {
+ Optional<String> cmd =
invokeMethodWithArray(invokeMethodWithArray(handle, info), command);
+ return cmd.orElse(null);
+ };
+
+ Object handle = invokeMethodWithArray(process, toHandle);
+ String processCommand = commandOf.apply(handle);
+ if (processCommand == null || isSameExecutable(processCommand,
jvmName)) {
+ // already the JVM, or we can't tell
+ return pid;
+ }
+ Stream<?> childHandles = invokeMethodWithArray(handle, children);
+ List<Long> jvmPids = childHandles
+ .filter(child -> isSameExecutable(commandOf.apply(child),
jvmName))
+ .map(child ->
ReflectionUtils.<Long>invokeMethodWithArray(child, childPid))
+ .collect(toList());
+ return jvmPids.size() == 1 ? jvmPids.get(0) : pid;
+ } catch (RuntimeException e) {
+ return pid;
+ }
+ }
+
+ private static boolean isSameExecutable(String command, String name) {
+ return command != null && executableName(new
File(command).getName()).equals(executableName(name));
+ }
+
+ private static String executableName(String fileName) {
+ String name = fileName.toLowerCase(Locale.ROOT);
+ return name.endsWith(".exe") ? name.substring(0, name.length() - 4) :
name;
+ }
+
static Long pidOnJMX() {
String processName = ManagementFactory.getRuntimeMXBean().getName();
if (processName.contains("@")) {
diff --git
a/surefire-booter/src/test/java/org/apache/maven/surefire/booter/SystemUtilsTest.java
b/surefire-booter/src/test/java/org/apache/maven/surefire/booter/SystemUtilsTest.java
index 0d80531b7..c3e04dd36 100644
---
a/surefire-booter/src/test/java/org/apache/maven/surefire/booter/SystemUtilsTest.java
+++
b/surefire-booter/src/test/java/org/apache/maven/surefire/booter/SystemUtilsTest.java
@@ -18,22 +18,28 @@
*/
package org.apache.maven.surefire.booter;
+import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
+import java.io.InputStreamReader;
import java.lang.management.ManagementFactory;
import java.math.BigDecimal;
+import java.net.URISyntaxException;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.mockito.MockedStatic;
import static java.io.File.separator;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.concurrent.TimeUnit.SECONDS;
import static org.apache.maven.surefire.shared.lang3.JavaVersion.JAVA_9;
import static org.apache.maven.surefire.shared.lang3.JavaVersion.JAVA_RECENT;
import static
org.apache.maven.surefire.shared.lang3.SystemUtils.IS_OS_FREE_BSD;
import static org.apache.maven.surefire.shared.lang3.SystemUtils.IS_OS_LINUX;
import static org.apache.maven.surefire.shared.lang3.SystemUtils.IS_OS_NET_BSD;
import static
org.apache.maven.surefire.shared.lang3.SystemUtils.IS_OS_OPEN_BSD;
+import static org.apache.maven.surefire.shared.lang3.SystemUtils.IS_OS_WINDOWS;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
import static org.mockito.ArgumentMatchers.any;
@@ -232,6 +238,104 @@ public void shouldBePid() {
assertThat(actualPid + "").isEqualTo(expectedPid);
}
+
+ @Test
+ public void shouldFindJvmStartedByShell() throws Exception {
+ assumeTrue(JAVA_RECENT.atLeast(JAVA_9) && !IS_OS_WINDOWS);
+
+ // "; true" stops the shell from turning into java, so java runs
as its child
+ Process shell = new ProcessBuilder(
+ "/bin/sh",
+ "-c",
+ "'" + java() + "' -cp '" + classPath() + "' '" +
Sleeper.class.getName() + "'; true")
+ .start();
+ try {
+ awaitReady(shell);
+ Long shellPid = SystemUtils.pidOf(shell);
+
+ assertThat(SystemUtils.forkedJvmPidOf(shell, "java"))
+ .isNotNull()
+ .isNotEqualTo(shellPid);
+ assertThat(SystemUtils.forkedJvmPidOf(shell,
"not-a-jvm")).isEqualTo(shellPid);
+ } finally {
+ stop(shell);
+ }
+ }
+
+ @Test
+ public void shouldKeepPidOfJvmEvenWithJvmChild() throws Exception {
+ assumeTrue(JAVA_RECENT.atLeast(JAVA_9));
+
+ Process jvm = new ProcessBuilder(java(), "-cp", classPath(),
Sleeper.class.getName(), "with-child").start();
+ try {
+ awaitReady(jvm);
+
+ assertThat(SystemUtils.forkedJvmPidOf(jvm,
"java")).isEqualTo(SystemUtils.pidOf(jvm));
+ } finally {
+ stop(jvm);
+ }
+ }
+
+ @Test
+ public void shouldFallBackWhenJvmNameUnknown() throws Exception {
+ assertThat(SystemUtils.forkedJvmPidOf(null, "java")).isNull();
+ assumeTrue(JAVA_RECENT.atLeast(JAVA_9));
+
+ Process jvm = new ProcessBuilder(java(), "-cp", classPath(),
Sleeper.class.getName()).start();
+ try {
+ awaitReady(jvm);
+
+ assertThat(SystemUtils.forkedJvmPidOf(jvm,
null)).isEqualTo(SystemUtils.pidOf(jvm));
+ } finally {
+ stop(jvm);
+ }
+ }
+ }
+
+ /**
+ * Tiny JVM for the {@code forkedJvmPidOf} tests: prints "ready" and waits
until its stdin closes.
+ * With an argument, it first starts another one as its child. Uses only
JDK classes, it runs in its own JVM.
+ */
+ public static final class Sleeper {
+ public static void main(String[] args) throws Exception {
+ if (args.length > 0) {
+ String java = System.getProperty("java.home") + separator +
"bin" + separator + "java";
+ Process child = new ProcessBuilder(
+ java, "-cp",
System.getProperty("java.class.path"), Sleeper.class.getName())
+ .start();
+ new BufferedReader(new
InputStreamReader(child.getInputStream(), UTF_8)).readLine();
+ }
+ System.out.println("ready");
+ System.out.flush();
+ while (System.in.read() != -1) {
+ // run until stdin closes
+ }
+ }
+ }
+
+ private static String java() {
+ return System.getProperty("java.home") + separator + "bin" + separator
+ "java";
+ }
+
+ private static String classPath() throws URISyntaxException {
+ return new File(Sleeper.class
+ .getProtectionDomain()
+ .getCodeSource()
+ .getLocation()
+ .toURI())
+ .getPath();
+ }
+
+ private static void awaitReady(Process process) throws IOException {
+ BufferedReader out = new BufferedReader(new
InputStreamReader(process.getInputStream(), UTF_8));
+ assertThat(out.readLine()).isEqualTo("ready");
+ }
+
+ private static void stop(Process process) throws Exception {
+ process.getOutputStream().close();
+ if (!process.waitFor(10, SECONDS)) {
+ process.destroyForcibly();
+ }
}
/**
diff --git
a/surefire-its/src/test/java/org/apache/maven/surefire/its/jiras/Surefire3549JstackForkedJvmPidIT.java
b/surefire-its/src/test/java/org/apache/maven/surefire/its/jiras/Surefire3549JstackForkedJvmPidIT.java
new file mode 100644
index 000000000..23ed18aff
--- /dev/null
+++
b/surefire-its/src/test/java/org/apache/maven/surefire/its/jiras/Surefire3549JstackForkedJvmPidIT.java
@@ -0,0 +1,137 @@
+/*
+ * 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.maven.surefire.its.jiras;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.maven.surefire.its.fixture.OutputValidator;
+import org.apache.maven.surefire.its.fixture.SurefireJUnit4IntegrationTestCase;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assumptions.assumeTrue;
+
+/**
+ * GH-3549: jstack must dump the forked JVM, not the shell that starts it.
+ */
+public class Surefire3549JstackForkedJvmPidIT extends
SurefireJUnit4IntegrationTestCase {
+
+ private static final Pattern DUMP_NAME =
Pattern.compile("surefire-timeout-jstack-\\d+-(\\d+)\\.txt");
+
+ @BeforeEach
+ public void needsJstack() {
+ assumeTrue(IS_JAVA9_PLUS, "the fork PID is only known on Java 9+");
+ assumeTrue(jstackAvailable(), "jstack not found in this JDK");
+ }
+
+ @Test
+ public void jstackDumpsTheForkedJvm() throws Exception {
+ assertEveryForkWasDumped(runWithForks(1), 1);
+ }
+
+ @Test
+ public void jstackDumpsEachForkedJvm() throws Exception {
+ assertEveryForkWasDumped(runWithForks(2), 2);
+ }
+
+ private OutputValidator runWithForks(int forkCount) {
+ return unpack("surefire-3549-jstack-forked-jvm-pid")
+ .forkCount(forkCount)
+ .maven()
+ .withFailure()
+ .executeTest()
+ .verifyTextInLog("There was a timeout in the fork");
+ }
+
+ private static void assertEveryForkWasDumped(OutputValidator validator,
int forks) throws Exception {
+ // PIDs written by the tests themselves
+ File target = new File(validator.getBaseDir(), "target");
+ Set<String> jvmPids = new TreeSet<>();
+ File[] pidFiles = target.listFiles((dir, name) ->
name.startsWith("pid-"));
+ if (pidFiles != null) {
+ for (File pidFile : pidFiles) {
+ jvmPids.add(read(pidFile));
+ }
+ }
+ assertThat(jvmPids).as("PIDs written by the sleeping
tests").hasSize(forks);
+
+ // PIDs jstack ran against, taken from the dump file names
+ File jstackDir = new File(target, "jstacks");
+ String[] dumps = jstackDir.list();
+ Set<String> dumpedPids = new TreeSet<>();
+ if (dumps != null) {
+ for (String dump : dumps) {
+ Matcher matcher = DUMP_NAME.matcher(dump);
+ if (matcher.matches()) {
+ dumpedPids.add(matcher.group(1));
+ }
+ }
+ }
+ assertThat(dumpedPids)
+ .as("jstack should target the forked JVMs %s but got:%n%s",
jvmPids, describe(jstackDir, dumps))
+ .isEqualTo(jvmPids);
+
+ for (String dump : dumps) {
+ assertThat(read(new File(jstackDir, dump)))
+ .as(dump)
+ .contains("Full thread dump")
+ .contains("surefire3549.");
+ }
+ validator.verifyTextNotInLog("jstack exited with code");
+ }
+
+ private static String describe(File dir, String[] files) throws
IOException {
+ if (files == null) {
+ return dir + " does not exist";
+ }
+ if (files.length == 0) {
+ return dir + " is empty";
+ }
+ StringBuilder sb = new StringBuilder();
+ for (String file : files) {
+ sb.append(file).append(": ").append(read(new File(dir,
file))).append(System.lineSeparator());
+ }
+ return sb.toString();
+ }
+
+ private static String read(File file) throws IOException {
+ return new String(Files.readAllBytes(file.toPath()), UTF_8).trim();
+ }
+
+ private static boolean jstackAvailable() {
+ String javaHome = System.getProperty("java.home");
+ if (javaHome == null) {
+ return false;
+ }
+ String exe = System.getProperty("os.name",
"").toLowerCase().contains("win") ? "jstack.exe" : "jstack";
+ if (new File(javaHome, "bin/" + exe).canExecute()) {
+ return true;
+ }
+ File parent = new File(javaHome).getParentFile();
+ return parent != null && new File(parent, "bin/" + exe).canExecute();
+ }
+}
diff --git
a/surefire-its/src/test/resources/surefire-3549-jstack-forked-jvm-pid/pom.xml
b/surefire-its/src/test/resources/surefire-3549-jstack-forked-jvm-pid/pom.xml
new file mode 100644
index 000000000..72d09081e
--- /dev/null
+++
b/surefire-its/src/test/resources/surefire-3549-jstack-forked-jvm-pid/pom.xml
@@ -0,0 +1,61 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ -->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <groupId>org.apache.maven.plugins.surefire</groupId>
+ <artifactId>surefire-3549-jstack-forked-jvm-pid</artifactId>
+ <version>1.0-SNAPSHOT</version>
+ <name>surefire-3549-jstack-forked-jvm-pid</name>
+
+ <properties>
+ <maven.compiler.source>1.8</maven.compiler.source>
+ <maven.compiler.target>1.8</maven.compiler.target>
+ </properties>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.junit.jupiter</groupId>
+ <artifactId>junit-jupiter</artifactId>
+ <version>5.9.1</version>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-surefire-plugin</artifactId>
+ <version>${surefire.version}</version>
+ <configuration>
+ <reuseForks>true</reuseForks>
+ <forkedProcessTimeoutInSeconds>5</forkedProcessTimeoutInSeconds>
+ <forkedProcessTimeoutExtensionContext>
+ <jstack.enabled>true</jstack.enabled>
+
<jstack.output.location>${project.build.directory}/jstacks</jstack.output.location>
+ </forkedProcessTimeoutExtensionContext>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+</project>
diff --git
a/surefire-its/src/test/resources/surefire-3549-jstack-forked-jvm-pid/src/test/java/surefire3549/OtherSleepingTest.java
b/surefire-its/src/test/resources/surefire-3549-jstack-forked-jvm-pid/src/test/java/surefire3549/OtherSleepingTest.java
new file mode 100644
index 000000000..6a582982b
--- /dev/null
+++
b/surefire-its/src/test/resources/surefire-3549-jstack-forked-jvm-pid/src/test/java/surefire3549/OtherSleepingTest.java
@@ -0,0 +1,39 @@
+/*
+ * 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 surefire3549;
+
+import java.lang.management.ManagementFactory;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+
+import org.junit.jupiter.api.Test;
+
+public class OtherSleepingTest {
+
+ @Test
+ public void sleepsLongerThanTimeout() throws Exception {
+ // Save our PID so the IT can check jstack dumped this JVM
+ String name = ManagementFactory.getRuntimeMXBean().getName();
+ String pid = name.substring(0, name.indexOf('@'));
+ String file = "pid-" + getClass().getSimpleName() + ".txt";
+ Files.write(Paths.get("target", file),
pid.getBytes(StandardCharsets.UTF_8));
+ Thread.sleep(30_000L);
+ }
+}
diff --git
a/surefire-its/src/test/resources/surefire-3549-jstack-forked-jvm-pid/src/test/java/surefire3549/SleepingTest.java
b/surefire-its/src/test/resources/surefire-3549-jstack-forked-jvm-pid/src/test/java/surefire3549/SleepingTest.java
new file mode 100644
index 000000000..862ff8a18
--- /dev/null
+++
b/surefire-its/src/test/resources/surefire-3549-jstack-forked-jvm-pid/src/test/java/surefire3549/SleepingTest.java
@@ -0,0 +1,39 @@
+/*
+ * 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 surefire3549;
+
+import java.lang.management.ManagementFactory;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+
+import org.junit.jupiter.api.Test;
+
+public class SleepingTest {
+
+ @Test
+ public void sleepsLongerThanTimeout() throws Exception {
+ // Save our PID so the IT can check jstack dumped this JVM
+ String name = ManagementFactory.getRuntimeMXBean().getName();
+ String pid = name.substring(0, name.indexOf('@'));
+ String file = "pid-" + getClass().getSimpleName() + ".txt";
+ Files.write(Paths.get("target", file),
pid.getBytes(StandardCharsets.UTF_8));
+ Thread.sleep(30_000L);
+ }
+}