Browse Source

test: 使用 JUnit 5 编写 Process 行为测试

yangyi 1 tuần trước cách đây
mục cha
commit
28a1c2b014
1 tập tin đã thay đổi với 141 bổ sung0 xóa
  1. 141 0
      src/test/java/space/anyi/process/ProcessMechanicsTest.java

+ 141 - 0
src/test/java/space/anyi/process/ProcessMechanicsTest.java

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