/*
 * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */
package jdk.java.lang.Process;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

import java.io.FilterInputStream;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.io.Reader;
import java.io.UncheckedIOException;
import java.io.Writer;
import java.lang.reflect.Field;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.stream.Stream;

/*
 * @test
 * @bug 8364361
 * @summary Tests for Process.close
 * @modules java.base/java.lang:+open java.base/java.io:+open
 * @run junit jdk.java.lang.Process.ProcessCloseTest
 */
public class ProcessCloseTest {

    private final static boolean OS_WINDOWS = System.getProperty("os.name").startsWith("Windows");
    private final static String CAT_PROGRAM = "cat";
    public static final String FORCED_CLOSE_MSG = "Forced close";
    private static List<String> JAVA_ARGS;

    private static List<String> setupJavaEXE() {
        String JAVA_HOME = System.getProperty("test.jdk");
        if (JAVA_HOME == null)
            JAVA_HOME = System.getProperty("JAVA_HOME");
        String classPath = System.getProperty("test.class.path");
        return  List.of(JAVA_HOME + "/bin/java", "-cp", classPath, ProcessCloseTest.class.getName());
    }

    private static List<String> javaArgs(ChildCommand... moreArgs) {

        List<String> javaArgs = JAVA_ARGS;
        if (javaArgs == null) {
            JAVA_ARGS = javaArgs = setupJavaEXE();
        }
        List<String> args = new ArrayList<>(javaArgs);
        for (ChildCommand arg : moreArgs) {
            args.add(arg.toString());
        }
        return args;
    }

    /**
     * {@return A Stream of Arguments}
     * Each Argument consists of three lists.
     * - A List of command line arguments to start a process.
     *   `javaArgs can be used to launch a Java child with ChildCommands
     * - A List of ProcessCommand actions to be invoked on that process
     * - A List of commands to be invoked on the process after the close or T-W-R exit.
     */
    static Stream<Arguments> singleThreadTestCases() {
        return Stream.of(
                Arguments.of(List.of("echo", "xyz0"),
                        List.of(ProcessCommand.STDOUT_PRINT_ALL_LINES,
                                ProcessCommand.STDERR_EXPECT_EMPTY,
                                ExitStatus.NORMAL),
                        List.of(ExitStatus.NORMAL)),
                Arguments.of(List.of("echo", "xyz1"),
                        List.of(ProcessCommand.STDOUT_PRINT_ALL_LINES),
                        List.of(ExitStatus.NORMAL)),
                Arguments.of(javaArgs(ChildCommand.STDOUT_ECHO),
                        List.of(ProcessCommand.WRITER_WRITE,
                                ProcessCommand.WRITER_CLOSE,
                                ProcessCommand.STDOUT_PRINT_ALL_LINES),
                        List.of(ExitStatus.NORMAL)),
                Arguments.of(javaArgs(ChildCommand.STDOUT_ECHO),
                        List.of(ProcessCommand.STDOUT_WRITE,
                                ProcessCommand.STDOUT_CLOSE,
                                ProcessCommand.STDOUT_PRINT_ALL_LINES),
                        List.of(ExitStatus.NORMAL)),
                Arguments.of(javaArgs(ChildCommand.STDOUT_ECHO),
                        List.of(ProcessCommand.STDOUT_WRITE,
                                ProcessCommand.STDOUT_CLOSE,
                                ExitStatus.NORMAL),
                        List.of(ExitStatus.NORMAL)),
                Arguments.of(javaArgs(ChildCommand.STDERR_MARCO, ChildCommand.PROCESS_EXIT1),
                        List.of(ProcessCommand.STDERR_EXPECT_POLO,
                                ProcessCommand.STDOUT_EXPECT_EMPTY),
                        List.of(ExitStatus.FAIL)),
                Arguments.of(javaArgs(ChildCommand.STDOUT_MARCO),
                        List.of(ProcessCommand.STDOUT_EXPECT_POLO,
                                ProcessCommand.STDERR_EXPECT_EMPTY),
                        List.of(ExitStatus.NORMAL)),
                Arguments.of(javaArgs(ChildCommand.STDERR_MARCO),
                        List.of(ProcessCommand.STDERR_EXPECT_POLO,
                                ProcessCommand.STDOUT_EXPECT_EMPTY),
                        List.of(ExitStatus.NORMAL)),
                Arguments.of(javaArgs(ChildCommand.PROCESS_EXIT1),
                        List.of(ExitStatus.FAIL),
                        List.of(ExitStatus.FAIL)),       // Got expected status == 1
                Arguments.of(javaArgs(ChildCommand.SLEEP),
                        List.of(ProcessCommand.PROCESS_INTERRUPT, // schedule an interrupt (in .2 sec)
                                ProcessCommand.PROCESS_CLOSE,
                                ProcessCommand.PROCESS_CHECK_INTERRUPTED), // Verify interrupted status
                        List.of(ExitStatus.KILLED)), // And process was destroyed
                Arguments.of(javaArgs(ChildCommand.SLEEP),
                        List.of(ProcessCommand.PROCESS_INTERRUPT), // interrupts the TWR/close
                        List.of(ProcessCommand.PROCESS_CHECK_INTERRUPTED, ExitStatus.KILLED)),
                Arguments.of(javaArgs(ChildCommand.SLEEP),
                        List.of(ExitStatus.NORMAL), // waitFor before T-W-R exit
                        List.of(ExitStatus.NORMAL)),
                Arguments.of(List.of("echo", "abc"),
                        List.of(ProcessCommand.PROCESS_CLOSE),
                        List.of(ExitStatus.RACY)),
                Arguments.of(javaArgs(ChildCommand.STDOUT_ECHO),
                        List.of(ProcessCommand.STDOUT_WRITE,
                                ProcessCommand.PROCESS_CLOSE),
                        List.of(ExitStatus.PIPE)),
                Arguments.of(List.of("echo", "def"),
                        List.of(ProcessCommand.PROCESS_DESTROY),
                        List.of(ExitStatus.RACY)), // Racy, not deterministic
                Arguments.of(javaArgs(ChildCommand.STDOUT_ECHO),
                        List.of(ProcessCommand.STDOUT_WRITE,
                                ProcessCommand.PROCESS_DESTROY),
                        List.of(ExitStatus.RACY)), // Racy, not deterministic
                Arguments.of(List.of("echo"),
                        List.of(ExitStatus.NORMAL),
                        List.of(ExitStatus.NORMAL))
        );
    }

    // Utility to process each command on the process
    private static void doCommands(Process proc, List<Consumer<Process>> commands) {
        commands.forEach(c -> {
            Log.printf("    %s\n", c);
            c.accept(proc);
        });
    }

    @ParameterizedTest
    @MethodSource("singleThreadTestCases")
    void simple(List<String> args, List<Consumer<Process>> commands,
                List<Consumer<Process>> postCommands) throws IOException {
        var log = Log.get();
        try {
            ProcessBuilder pb = new ProcessBuilder(args);
            Process p = pb.start(); // Buffer any debug output
            Log.printf("Program: %s; pid: %d\n", args, p.pid());
            doCommands(p, commands);
            p.close();
            doCommands(p, postCommands);
        } catch (Throwable ex) {
            System.err.print(log);
            throw ex;
        }
    }

    @ParameterizedTest
    @MethodSource("singleThreadTestCases")
    void autoCloseable(List<String> args, List<Consumer<Process>> commands,
                       List<Consumer<Process>> postCommands) {
        var log = Log.get();
        try {
            ProcessBuilder pb = new ProcessBuilder(args);
            Process proc = null;
            try (Process p = pb.start()) {
                proc = p;
                Log.printf("Program: %s; pid: %d\n", args, p.pid());
                doCommands(p, commands);
            } catch (IOException ioe) {
                Assertions.fail(ioe);
            }
            doCommands(proc, postCommands);
        } catch (Throwable ex) {
            System.err.println(log);
            throw ex;
        }
    }

    /**
     * Test AutoCloseable for the process and out, in, and err streams.
     * @param args The command line arguments
     * @param commands the commands to the process
     * @param postCommands The expected final exit status
     */
    @ParameterizedTest
    @MethodSource("singleThreadTestCases")
    void autoCloseableAll(List<String> args, List<Consumer<Process>> commands,
                          List<Consumer<Process>> postCommands) {
        var log = Log.get();
        try {
            ProcessBuilder pb = new ProcessBuilder(args);
            Process proc = null;
            try (Process p = pb.start(); var out = p.getOutputStream();
                 var in = p.getInputStream();
                 var err = p.getErrorStream()) {
                proc = p;
                Log.printf("Program: %s; pid: %d\n", args, p.pid());
                doCommands(p, commands);
            } catch (IOException ioe) {
                Assertions.fail(ioe);
            }
            doCommands(proc, postCommands);
        }  catch (Throwable ex) {
            System.err.println(log);
            throw ex;
        }
    }

    /**
     * ExitStatus named values and assertions
     */
    enum ExitStatus implements Consumer<Process> {
        NORMAL(0),
            FAIL(1),
            PIPE(0, 1, 141),   // SIGPIPE
            KILLED(1, 137), // SIGKILL
            TERMINATED(0, 143), // SIGTERM
            RACY(0, 1, 137, 143),
        ;
        private final int[] allowedStatus;

        ExitStatus(int... status) {
            this.allowedStatus = status;
        }

        // If used as a process command, checks the exit status
        public void accept(Process p) {
            try {
                Instant begin = Instant.now();
                final int exitStatus = p.waitFor();
                Duration latency = begin.until(Instant.now());
                Log.printf("    ExitStatus: %d, sig#: %d, waitFor latency: %s%n",
                        exitStatus, exitStatus & 0x7f, latency);
                assertEquals(exitStatus);
            } catch (InterruptedException ie) {
                Assertions.fail("Unexpected InterruptedException checking status: " + this);
            }
        }

        // Check a status matches one of the allowed exit status values
        void assertEquals(int actual) {
            for (int status : allowedStatus) {
                if (status == actual) {
                    return;     // status is expected
                }
            }
            if (this == RACY) {
                // Not an error but report the actual status
                Log.printf("Racy exit status: %d\n", actual);
            } else {
                Assertions.fail("Status: " + actual + ", sig#: " + (actual & 0x7f) +
                        ", expected one of: " + Arrays.toString(allowedStatus));
            }
        }
    }

    /**
     * Commands on a Process that can be sequenced in the parent.
     * See ChildCommands for commands that can be sent to the child process.
     */
    enum ProcessCommand implements Consumer<Process> {
        PROCESS_CLOSE(ProcessCommand::processClose),
        PROCESS_DESTROY(ProcessCommand::processDestroy),
        PROCESS_FORCE_OUT_CLOSE_EXCEPTION(ProcessCommand::processForceOutCloseException),
        PROCESS_FORCE_IN_CLOSE_EXCEPTION(ProcessCommand::processForceInCloseException),
        PROCESS_FORCE_ERROR_CLOSE_EXCEPTION(ProcessCommand::processForceErrorCloseException),
        WRITER_WRITE(ProcessCommand::writerWrite),
        WRITER_CLOSE(ProcessCommand::writerClose),
        STDOUT_PRINT_ALL_LINES(ProcessCommand::stdoutPrintAllLines),
        STDERR_PRINT_ALL_LINES(ProcessCommand::stderrPrintAllLines),
        STDOUT_WRITE(ProcessCommand::stdoutWrite),
        STDOUT_CLOSE(ProcessCommand::stdoutClose),
        STDOUT_EXPECT_POLO(ProcessCommand::stdoutExpectPolo),
        STDERR_EXPECT_POLO(ProcessCommand::stderrExpectPolo),
        STDOUT_EXPECT_EMPTY(ProcessCommand::stdoutExpectEmpty),
        STDERR_EXPECT_EMPTY(ProcessCommand::stderrExpectEmpty),
        PROCESS_INTERRUPT(ProcessCommand::processInterruptThread),
        PROCESS_CHECK_INTERRUPTED(ProcessCommand::processAssertInterrupted),
        ;
        private final Consumer<Process> command;

        ProcessCommand(Consumer<Process> command) {
            this.command = command;
        }

        public void accept(Process p) {
            command.accept(p);
        }

        private static void stdoutPrintAllLines(Process p) {
            try {
                var lines = p.inputReader().readAllLines();
                Assertions.assertNotEquals(0, lines.size(), "stdout should not be empty");
                Log.printf("        %d lines\n", lines.size());
                Log.printf("%s%n", lines.toString().indent(8));
            } catch (IOException ioe) {
                throw new UncheckedIOException(ioe);
            }
        }

        private static void stderrPrintAllLines(Process p) {
            try {
                var lines = p.errorReader().readAllLines();
                Assertions.assertNotEquals(0, lines.size(), "stderr should not be empty");
                Log.printf("        %d lines\n", lines.size());
                Log.printf("%s%n", lines.toString().indent(8));
            } catch (IOException ioe) {
                throw new UncheckedIOException(ioe);
            }
        }

        private static void writerWrite(Process p) {
            try {
                p.outputWriter().write("Now is the time.");
            } catch (IOException ioe) {
                throw new UncheckedIOException(ioe);
            }
        }

        private static void writerClose(Process p) {
            try {
                p.outputWriter().close();
            } catch (IOException ioe) {
                throw new UncheckedIOException(ioe);
            }
        }

        private static void stdoutExpectPolo(Process p) {
            String line = readLine(p.getInputStream());
            Assertions.assertEquals("Polo", line, "Stdout Expected Polo");
        }

        private static void stderrExpectPolo(Process p) {
            String line = readLine(p.getErrorStream());
            Assertions.assertEquals("Polo", line, "Stderr Expected Polo");
        }

        private static void stdoutExpectEmpty(Process p) {
            String line = readLine(p.getInputStream());
            Assertions.assertEquals("", line, "Stdout Expected Empty");
        }

        private static void stderrExpectEmpty(Process p) {
            String line = readLine(p.getErrorStream());
            Assertions.assertEquals("", line, "Stderr Expected Empty");
        }

        private static String readLine(InputStream in) {
            StringBuilder sb = new StringBuilder();
            try {
                int ch;
                while ((ch = in.read()) != -1) {
                    if (ch == '\n') {
                        // end of line
                        return sb.toString();
                    }
                    if (ch != '\r') {       // ignore cr - Windows defense
                        sb.append((char) ch);
                    }
                }
                // EOF - return string if no LF found
                return sb.toString();
            } catch (IOException ioe) {
                return ioe.getMessage();
            }
        }

        private static void stdoutWrite(Process p) {
            try {
                var out = p.getOutputStream();
                out.write("stdout-write".getBytes(StandardCharsets.UTF_8));
                out.flush();
            } catch (IOException ioe) {
                throw new UncheckedIOException(ioe);
            }
        }

        private static void stdoutClose(Process p) {
            try {
                p.getOutputStream().close();
            } catch (IOException ioe) {
                throw new UncheckedIOException(ioe);
            }
        }

        private static void processClose(Process p) {
            try {
                p.close();
            } catch (IOException ioe) {
                Assertions.fail(ioe);
            }
        }

        private static void processDestroy(Process p) {
            p.destroy();
        }

        // Interpose an input stream that throws on close()
        private static void processForceInCloseException(Process p) {
            try {
                synchronized (p) {
                    Field stdinField = p.getClass().getDeclaredField(OS_WINDOWS ? "stdin_stream" : "stdin");
                    stdinField.setAccessible(true);
                    stdinField.set(p, new ThrowingOutputStream((OutputStream) stdinField.get(p)));
                }
            } catch (Exception ex) {
                Assertions.fail("Failed to setup InputStream for throwing close", ex);
            }
        }

        // Interpose an output stream that throws on close()
        private static void processForceOutCloseException(Process p) {
            try {
                synchronized (p) {
                    Field stdoutField = p.getClass().getDeclaredField(OS_WINDOWS ? "stdout_stream" : "stdout");
                    stdoutField.setAccessible(true);
                    stdoutField.set(p, new ThrowingInputStream((InputStream) stdoutField.get(p)));
                }
            } catch (Exception ex) {
                Assertions.fail("Failed to setup OutputStream throwing close", ex);
            }
        }

        // Interpose an error stream that throws on close()
        private static void processForceErrorCloseException(Process p) {
            try {
                synchronized (p) {
                    Field stderrField = p.getClass().getDeclaredField(OS_WINDOWS ? "stderr_stream" : "stderr");
                    stderrField.setAccessible(true);
                    stderrField.set(p, new ThrowingInputStream((InputStream) stderrField.get(p)));
                }
            } catch (Exception ex) {
                Assertions.fail("Failed to setup OutputStream for throwing close", ex);
            }
        }

        // Hard coded to interrupt the invoking thread at a fixed rate of .2 second, if process is alive
        private static void processInterruptThread(Process p) {
            if (p.isAlive()) {
                int delay = 200;
                final Thread targetThread = Thread.currentThread();
                ForkJoinPool common = ForkJoinPool.commonPool();
                final ThreadInterruptor interrupter = new ThreadInterruptor(p, targetThread);
                common.scheduleAtFixedRate(interrupter, delay, delay, TimeUnit.MILLISECONDS);
            }
        }

        // Verify that an interrupt is pending and reset it
        private static void processAssertInterrupted(Process p) {
            Assertions.assertTrue(Thread.interrupted(), "Expected an interrupt");
        }
    }

    // Runnable scheduled at a fixed rate to interrupt a thread if a process is alive.
    private static class ThreadInterruptor implements Runnable {
        private final Process process;
        private final Thread targetThread;
        private int count;

        ThreadInterruptor(Process process, Thread targetThread) {
            this.process = process;
            this.targetThread = targetThread;
            this.count = 0;
        }

        public void run() {
            if (process.isAlive()) {
                count++;
                Log.printf("Interrupting thread, count: %d%n", count);
                targetThread.interrupt();
            } else {
                throw new RuntimeException("process not alive");
            }
        }
    }

    // Commands to Java child sent as command line arguments
    enum ChildCommand {
        STDOUT_ECHO(ChildCommand::stdoutEchoBytes),
        STDERR_ECHO(ChildCommand::stderrEchoBytes),
        SLEEP(ChildCommand::SLEEP),
        STDOUT_MARCO(ChildCommand::stdoutMarco),
        STDERR_MARCO(ChildCommand::stderrMarco),
        PROCESS_EXIT1(ChildCommand::processExit1),
        ;
        private final Runnable command;
        ChildCommand(Runnable cmd) {
            this.command = cmd;
        }

        // The child sleeps before continuing with next ChildCommand
        private static void SLEEP() {
            final int sleepMS = 2_000;
            try {
                Thread.sleep(sleepMS);
            } catch (InterruptedException ie) {
                // Interrupted sleep, re-assert interrupted status
                System.err.println("Sleep interrupted");  // Note the interruption in the log
                Thread.currentThread().interrupt();
            }
        }

        private static void stdoutEchoBytes() {
            echoBytes(System.in, System.out);
        }

        private static void stderrEchoBytes() {
            echoBytes(System.in, System.err);
        }

        private static void echoBytes(InputStream in, PrintStream out) {
            try {
                byte[] bytes = in.readAllBytes();
                out.write(bytes);
            } catch (IOException ioe) {
                out.println(ioe);
            }
        }

        private static void stdoutMarco() {
            System.out.println("Polo");
        }

        private static void stderrMarco() {
            System.err.println("Polo");
        }

        private static void processExit1() {
            System.exit(1);
        }
    }

    static Stream<Arguments> closeExceptions() {
        return Stream.of(
                Arguments.of(javaArgs(ChildCommand.SLEEP),
                        List.of(ProcessCommand.PROCESS_FORCE_OUT_CLOSE_EXCEPTION),
                        List.of(ExitStatus.RACY), List.of(FORCED_CLOSE_MSG)),
                Arguments.of(javaArgs(ChildCommand.SLEEP),
                        List.of(ProcessCommand.PROCESS_FORCE_IN_CLOSE_EXCEPTION),
                        List.of(ExitStatus.RACY), List.of(FORCED_CLOSE_MSG)),
                Arguments.of(javaArgs(ChildCommand.SLEEP),
                        List.of(ProcessCommand.PROCESS_FORCE_ERROR_CLOSE_EXCEPTION),
                        List.of(ExitStatus.RACY), List.of(FORCED_CLOSE_MSG)),
                Arguments.of(javaArgs(ChildCommand.SLEEP),
                        List.of(ProcessCommand.PROCESS_FORCE_OUT_CLOSE_EXCEPTION,
                                ProcessCommand.PROCESS_FORCE_IN_CLOSE_EXCEPTION,
                                ProcessCommand.PROCESS_FORCE_ERROR_CLOSE_EXCEPTION),
                        List.of(ExitStatus.RACY), List.of(FORCED_CLOSE_MSG, FORCED_CLOSE_MSG, FORCED_CLOSE_MSG)),
                Arguments.of(javaArgs(ChildCommand.SLEEP),
                        List.of(ProcessCommand.PROCESS_FORCE_OUT_CLOSE_EXCEPTION,
                                ProcessCommand.PROCESS_FORCE_IN_CLOSE_EXCEPTION),
                        List.of(ExitStatus.RACY), List.of(FORCED_CLOSE_MSG, FORCED_CLOSE_MSG)),
                Arguments.of(javaArgs(ChildCommand.SLEEP),
                        List.of(ProcessCommand.PROCESS_FORCE_OUT_CLOSE_EXCEPTION,
                                ProcessCommand.PROCESS_FORCE_ERROR_CLOSE_EXCEPTION),
                        List.of(ExitStatus.RACY), List.of(FORCED_CLOSE_MSG, FORCED_CLOSE_MSG)),
                Arguments.of(javaArgs(ChildCommand.SLEEP),
                        List.of(ProcessCommand.PROCESS_FORCE_IN_CLOSE_EXCEPTION,
                                ProcessCommand.PROCESS_FORCE_ERROR_CLOSE_EXCEPTION),
                        List.of(ExitStatus.RACY), List.of(FORCED_CLOSE_MSG, FORCED_CLOSE_MSG)),
                Arguments.of(List.of("echo", "xyz1"),
                        List.of(ProcessCommand.PROCESS_FORCE_OUT_CLOSE_EXCEPTION),
                        List.of(ExitStatus.RACY), List.of(FORCED_CLOSE_MSG))
        );
    }
    /**
     * Test AutoCloseable for cases that are expected to throw exceptions.
     * The list of ProcessCommands controls what is sent to the process and closing of streams.
     * The command line arguments control the sequence of actions taken by the child.
     * @param args The command line arguments for the child process
     * @param commands the commands to this process controlling the child
     * @param postCommands The expected final exit status
     * @param expectedMessages the list of exception messages expected by close()
     */
    @ParameterizedTest
    @MethodSource("closeExceptions")
    void testStreamsCloseThrowing(List<String> args, List<Consumer<Process>> commands,
                                  List<Consumer<Process>> postCommands, List<String> expectedMessages) {
        var log = Log.get();
        try {
            ProcessBuilder pb = new ProcessBuilder(args);
            Process proc = null;
            IOException expectedIOE = null;
            try (Process p = pb.start()) {
                proc = p;
                Log.printf("Program: %s; pid: %d\n",args, p.pid());
                doCommands(p, commands);
            } catch (IOException ioe) {
                expectedIOE = ioe;
            }
            // Check the exceptions thrown, if any
            if (expectedIOE != null) {
                // Check each exception that it is expected
                Assertions.assertEquals(expectedMessages.getFirst(), expectedIOE.getMessage(),
                        "Unexpected exception message");
                var suppressedEx = expectedIOE.getSuppressed();
                Assertions.assertEquals(expectedMessages.size() - 1, suppressedEx.length,
                        "Number of suppressed exceptions");
                for (int i = 1; i < expectedMessages.size(); i++) {
                    Assertions.assertEquals(expectedMessages.get(i),
                            suppressedEx[i - 1].getMessage(),
                            "Unexpected suppressed exception message");
                }
            }
            Assertions.assertNotNull(proc, "Process is null");
            doCommands(proc, postCommands);
        }  catch (Exception ex) {
            System.err.println(log);
            throw ex;
        }
    }

    /**
     * An OutputStream that delegates to another stream and always throws IOException on close().
     */
    private static class ThrowingOutputStream extends FilterOutputStream {
        public ThrowingOutputStream(OutputStream out) {
            super(out);
        }

        @Override
        public void close() throws IOException {
            try {
                out.close();
            } catch (IOException ioe) {
                // ignore except to log the exception; may be useful to debug
                ioe.printStackTrace(System.err);
            }
            throw new IOException(FORCED_CLOSE_MSG);
        }
    }

    /**
     * An InputStream that delegates to another stream and always throws IOException on close().
     */
    private static class ThrowingInputStream extends FilterInputStream {
        public ThrowingInputStream(InputStream in) {
            super(in);
        }

        @Override
        public void close() throws IOException {
            try {
                in.close();
            } catch (IOException ioe) {
                // ignore except to log the exception; may be useful to debug
                ioe.printStackTrace(System.err);
            }
            throw new IOException(FORCED_CLOSE_MSG);
        }
    }

    // Copy of ProcessExamples in java/lang/snippet-files/ProcessExamples.java
    @Test
    void example() {
        try (Process p = new ProcessBuilder(CAT_PROGRAM).start();
             Writer writer = p.outputWriter();
             Reader reader = p.inputReader()) {
            writer.write(haiku);
            writer.close();
            // Read all lines and print each
            reader.readAllLines()
                    .forEach(System.out::println);
            var status = p.waitFor();
            if (status != 0)
                throw new RuntimeException("unexpected process status: " + status);
        } catch (Exception e) {
            System.err.println("Process failed: " + e);
        }
    }

    String haiku = """
                Oh, the sunrise glow;
                Paddling with the river flow;
                Chilling still, go slow.
                """;

    /**
     * Child program that executes child actions as named by command line args.
     * @param childCommands a sequence of ChildCommand names.
     */
    public static void main(String... childCommands)  {
        List<String> args = List.of(childCommands);
        List<ChildCommand> commands = args.stream().map(ChildCommand::valueOf).toList();
        commands.forEach(c -> c.command.run());
    }

    /**
     * Log of output produced on a thread during a test.
     * Normally, the output is buffered and only output to stderr if the test fails.
     * Set -DDEBUG=true to send all output to stderr as it occurs.
     */
    private static class Log {

        private static final boolean DEBUG = Boolean.getBoolean("DEBUG");
        private final static ScopedValue<Appendable> OUT = ScopedValue.newInstance();
        private final static ScopedValue.Carrier LOG = setupLog();

        private static ScopedValue.Carrier setupLog() {
            if (DEBUG) {
                return ScopedValue.where(OUT, System.err);
            } else {
                return ScopedValue.where(OUT, new StringBuffer());
            }
        }

        // Return the log for this thread and clear the buffer.
        private static Appendable get() {
            var log = LOG.get(OUT);
            if (log instanceof StringBuffer sb)
                sb.setLength(0);
            return log;
        }

        // Printf to the log for this thread.
        private static void printf(String format, Object... args) {
            try {
                var log = LOG.get(OUT);
                log.append(format.formatted(args));
            } catch (IOException ioe) {
                throw new UncheckedIOException(ioe);
            }
        }
    }
}
