|
|
@@ -0,0 +1,141 @@
|
|
|
+package space.anyi.process;
|
|
|
+
|
|
|
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
|
|
+import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
|
+import static org.junit.jupiter.api.Assertions.assertFalse;
|
|
|
+import static org.junit.jupiter.api.Assertions.assertThrows;
|
|
|
+import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
|
+
|
|
|
+import java.io.BufferedReader;
|
|
|
+import java.io.File;
|
|
|
+import java.io.InputStream;
|
|
|
+import java.io.InputStreamReader;
|
|
|
+import java.io.OutputStream;
|
|
|
+import java.nio.charset.StandardCharsets;
|
|
|
+import java.nio.file.Files;
|
|
|
+import java.nio.file.Path;
|
|
|
+import java.nio.file.Paths;
|
|
|
+import java.util.List;
|
|
|
+import java.util.concurrent.CompletableFuture;
|
|
|
+import java.util.concurrent.TimeUnit;
|
|
|
+import java.util.stream.Collectors;
|
|
|
+
|
|
|
+import org.junit.jupiter.api.BeforeAll;
|
|
|
+import org.junit.jupiter.api.Test;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 基于 JUnit 5 的 Process 行为测试。
|
|
|
+ * 与示例一样驱动 src/main/resources/code/ 下的夹具子进程做端到端验证,
|
|
|
+ * 同样依赖 user.dir 定位工作目录,因此需要在工程根目录执行 mvn test。
|
|
|
+ */
|
|
|
+class ProcessMechanicsTest {
|
|
|
+
|
|
|
+ private static final File IO_DIR = fixtureDir("io");
|
|
|
+ private static final File STATUS_DIR = fixtureDir("executeStatus");
|
|
|
+
|
|
|
+ @BeforeAll
|
|
|
+ static void compileFixtures() throws Exception {
|
|
|
+ Jdk.compileFixture(IO_DIR);
|
|
|
+ Jdk.compileFixture(STATUS_DIR);
|
|
|
+ }
|
|
|
+
|
|
|
+ private static File fixtureDir(String name) {
|
|
|
+ return Paths.get(System.getProperty("user.dir"), "src", "main", "resources", "code", name).toFile();
|
|
|
+ }
|
|
|
+
|
|
|
+ private static Process start(File workDir) throws Exception {
|
|
|
+ return new ProcessBuilder(Jdk.java(), "-Dfile.encoding=UTF-8", "Main")
|
|
|
+ .directory(workDir)
|
|
|
+ .start();
|
|
|
+ }
|
|
|
+
|
|
|
+ private static List<String> readLines(InputStream in) {
|
|
|
+ return new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8))
|
|
|
+ .lines()
|
|
|
+ .collect(Collectors.toList());
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 案例点:标准输入 → 标准输出 的端到端回显 */
|
|
|
+ @Test
|
|
|
+ void stdinEchoRoundTrip() throws Exception {
|
|
|
+ Process process = start(IO_DIR);
|
|
|
+ CompletableFuture<List<String>> stdout = CompletableFuture.supplyAsync(() -> readLines(process.getInputStream()));
|
|
|
+
|
|
|
+ try (OutputStream stdin = process.getOutputStream()) {
|
|
|
+ stdin.write("ping\r\n".getBytes(StandardCharsets.UTF_8));
|
|
|
+ }
|
|
|
+
|
|
|
+ assertTrue(process.waitFor(5, TimeUnit.SECONDS), "子进程应在超时内退出");
|
|
|
+ assertEquals(0, process.exitValue());
|
|
|
+ assertTrue(stdout.get().contains("child: echo ping"), "stdout 应包含回显行");
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 案例点:redirectErrorStream(true) 互相重定向,stdout 与 stderr 合并到一路 */
|
|
|
+ @Test
|
|
|
+ void redirectErrorStreamMergesStreams() throws Exception {
|
|
|
+ Process process = new ProcessBuilder(Jdk.java(), "-Dfile.encoding=UTF-8", "Main")
|
|
|
+ .directory(IO_DIR)
|
|
|
+ .redirectErrorStream(true)
|
|
|
+ .start();
|
|
|
+ CompletableFuture<List<String>> merged = CompletableFuture.supplyAsync(() -> readLines(process.getInputStream()));
|
|
|
+
|
|
|
+ process.getOutputStream().close();
|
|
|
+ assertTrue(process.waitFor(5, TimeUnit.SECONDS));
|
|
|
+ List<String> lines = merged.get();
|
|
|
+ assertTrue(lines.stream().anyMatch(l -> l.startsWith("child: started")), "合并流应含 stdout 启动横幅");
|
|
|
+ assertTrue(lines.stream().anyMatch(l -> l.startsWith("child: stdin EOF")), "合并流应含 stderr 统计行");
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 案例点:标准流重定向到文件,结束后校验文件内容 */
|
|
|
+ @Test
|
|
|
+ void outputRedirectedToFile() throws Exception {
|
|
|
+ Path input = Files.createTempFile("proc-test", "-in.txt");
|
|
|
+ Path out = Files.createTempFile("proc-test", "-out.txt");
|
|
|
+ Path err = Files.createTempFile("proc-test", "-err.txt");
|
|
|
+ Files.writeString(input, "from-file\r\n");
|
|
|
+
|
|
|
+ Process process = new ProcessBuilder(Jdk.java(), "-Dfile.encoding=UTF-8", "Main")
|
|
|
+ .directory(IO_DIR)
|
|
|
+ .redirectInput(input.toFile())
|
|
|
+ .redirectOutput(out.toFile())
|
|
|
+ .redirectError(err.toFile())
|
|
|
+ .start();
|
|
|
+
|
|
|
+ assertTrue(process.waitFor(5, TimeUnit.SECONDS));
|
|
|
+ assertEquals(0, process.exitValue());
|
|
|
+ assertTrue(Files.readAllLines(out).stream().anyMatch(l -> l.contains("echo from-file")), "stdout 文件应含回显");
|
|
|
+ assertTrue(Files.readAllLines(err).stream().anyMatch(l -> l.startsWith("child: stdin EOF")), "stderr 文件应含统计");
|
|
|
+
|
|
|
+ Files.deleteIfExists(input);
|
|
|
+ Files.deleteIfExists(out);
|
|
|
+ Files.deleteIfExists(err);
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 案例点:waitFor(timeout) 返回 boolean;超时后进程仍存活,exitValue() 抛异常,destroy 后恢复正常 */
|
|
|
+ @Test
|
|
|
+ void timedWaitForTimesOutAndDestroyEnablesExitValue() throws Exception {
|
|
|
+ Process process = start(STATUS_DIR);
|
|
|
+
|
|
|
+ boolean exited = process.waitFor(500, TimeUnit.MILLISECONDS);
|
|
|
+ assertFalse(exited, "睡眠 10 秒的子进程在 500ms 内不应退出");
|
|
|
+ assertThrows(IllegalThreadStateException.class, process::exitValue, "进程存活时读退出码应抛异常");
|
|
|
+
|
|
|
+ process.destroy();
|
|
|
+ assertTrue(process.waitFor(5, TimeUnit.SECONDS), "destroy 后应能等到进程结束");
|
|
|
+ assertDoesNotThrow(process::exitValue, "结束后应能读取退出码");
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 案例点:Redirect.DISCARD 丢弃子进程输出,进程仍正常退出 */
|
|
|
+ @Test
|
|
|
+ void discardRedirectSwallowsOutput() throws Exception {
|
|
|
+ Process process = new ProcessBuilder(Jdk.java(), "-Dfile.encoding=UTF-8", "Main")
|
|
|
+ .directory(IO_DIR)
|
|
|
+ .redirectOutput(ProcessBuilder.Redirect.DISCARD)
|
|
|
+ .redirectError(ProcessBuilder.Redirect.DISCARD)
|
|
|
+ .start();
|
|
|
+ process.getOutputStream().close();
|
|
|
+
|
|
|
+ assertTrue(process.waitFor(5, TimeUnit.SECONDS));
|
|
|
+ assertEquals(0, process.exitValue());
|
|
|
+ }
|
|
|
+}
|