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

Reply via email to