/*
 * Copyright (c) 2014, 2023, 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.
 */


import java.io.IOException;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import jdk.test.lib.Utils;
import org.testng.Assert;
import org.testng.TestNG;
import org.testng.annotations.Test;

/*
 * @test
 * @requires vm.flagless
 * @library /test/lib
 * @modules java.base/jdk.internal.misc
 *          jdk.management
 * @build jdk.test.lib.Utils
 *        jdk.test.lib.Asserts
 *        jdk.test.lib.JDKToolFinder
 *        jdk.test.lib.JDKToolLauncher
 *        jdk.test.lib.Platform
 *        jdk.test.lib.process.*
 * @run testng/othervm TreeTest
 * @summary Test counting and JavaChild.spawning and counting of Processes.
 * @author Roger Riggs
 */
public class TreeTest extends ProcessUtil {
    // Main can be used to run the tests from the command line with only testng.jar.
    @SuppressWarnings("raw_types")
    public static void main(String[] args) {
        Class<?>[] testclass = {TreeTest.class};
        TestNG testng = new TestNG();
        testng.setTestClasses(testclass);
        testng.run();
    }

    /**
     * Test counting and spawning and counting of Processes.
     */
    @Test
    public static void test1() {
        final int MAXCHILDREN = 2;
        List<JavaChild> spawned = new ArrayList<>();

        try {
            ProcessHandle self = ProcessHandle.current();

            printf("self pid: %d%n", self.pid());
            printDeep(self, "");

            for (int i = 0; i < MAXCHILDREN; i++) {
                // spawn and wait for instructions
                spawned.add(JavaChild.spawnJavaChild("pid", "stdin"));
            }

            // Verify spawned Process is in list of children
            final List<ProcessHandle> initialChildren = getChildren(self);
            spawned.stream()
                    .map(Process::toHandle)
                    .filter(p -> !initialChildren.contains(p))
                    .forEach(p -> Assert.fail("Spawned process missing from children: " + p));

            // Send exit command to each spawned Process
            spawned.forEach(p -> {
                    try {
                        p.sendAction("exit", "");
                    } catch (IOException ex) {
                        Assert.fail("IOException in sendAction", ex);
                    }
                });

            // Wait for each Process to exit
            spawned.forEach(p -> {
                    do {
                        try {
                            Assert.assertEquals(p.waitFor(), 0, "exit status incorrect");
                            break;
                        } catch (InterruptedException  ex) {
                            continue; // Retry
                        }
                    } while (true);
                });

            // Verify that ProcessHandle.isAlive sees each of them as not alive
            for (Process p : spawned) {
                ProcessHandle ph = p.toHandle();
                Assert.assertFalse(ph.isAlive(),
                        "ProcessHandle.isAlive for exited process: " + ph);
            }

            // Verify spawned processes are not visible as children
            final List<ProcessHandle> afterChildren = getChildren(self);
            spawned.stream()
                    .map(Process::toHandle)
                    .filter(p -> afterChildren.contains(p))
                    .forEach(p -> Assert.fail("Spawned process missing from children: " + p));

        } catch (IOException ioe) {
            Assert.fail("unable to spawn process", ioe);
        } finally {
            // Cleanup any left over processes
            spawned.stream()
                    .map(Process::toHandle)
                    .filter(ProcessHandle::isAlive)
                    .forEach(ph -> {
                        printDeep(ph, "test1 cleanup: ");
                        ph.destroyForcibly();
                    });
        }
    }

    /**
     * Test counting and spawning and counting of Processes.
     */
    @Test
    public static void test2() {
        try {
            ConcurrentHashMap<ProcessHandle, ProcessHandle> processes = new ConcurrentHashMap<>();

            ProcessHandle self = ProcessHandle.current();
            List<ProcessHandle> initialChildren = getChildren(self);
            long count = initialChildren.size();
            if (count > 0) {
                initialChildren.forEach(p -> printDeep(p, "test2 initial unexpected: "));
            }

            JavaChild p1 = JavaChild.spawnJavaChild("stdin");
            ProcessHandle p1Handle = p1.toHandle();
            printf("  p1 pid: %d%n", p1.pid());

            // Gather the PIDs from the output of the spawing process
            p1.forEachOutputLine((s) -> {
                String[] split = s.trim().split(" ");
                if (split.length == 3 && split[1].equals("spawn")) {
                    Long child = Long.valueOf(split[2]);
                    Long parent = Long.valueOf(split[0].split(":")[0]);
                    processes.put(ProcessHandle.of(child).get(), ProcessHandle.of(parent).get());
                }
            });

            int spawnNew = 3;
            p1.sendAction("spawn", spawnNew, "stdin");

            // Wait for direct children to be created and save the list
            List<ProcessHandle> subprocesses = waitForAllChildren(p1Handle, spawnNew);
            Optional<Instant> p1Start = p1Handle.info().startInstant();
            for (ProcessHandle ph : subprocesses) {
                Assert.assertTrue(ph.isAlive(), "Child should be alive: " + ph);
                // Verify each child was started after the parent
                ph.info().startInstant()
                        .ifPresent(childStart -> p1Start.ifPresent(parentStart -> {
                            Assert.assertFalse(childStart.isBefore(parentStart),
                                    String.format("Child process started before parent: child: %s, parent: %s",
                                            childStart, parentStart));
                        }));
            }

            // Each child spawns two processes and waits for commands
            int spawnNewSub = 2;
            p1.sendAction("child", "spawn", spawnNewSub, "stdin");

            // Poll until all 9 child processes exist or the timeout is reached
            int expected = 9;
            long timeout = Utils.adjustTimeout(60L);
            Instant endTimeout = Instant.now().plusSeconds(timeout);
            do {
                Thread.sleep(200L);
                printf(" subprocess count: %d, waiting for %d%n", processes.size(), expected);
            } while (processes.size() < expected &&
                    Instant.now().isBefore(endTimeout));

            if (processes.size() < expected) {
                printf("WARNING: not all children have been started. Can't complete test.%n");
                printf("         You can try to increase the timeout or%n");
                printf("         you can try to use a faster VM (i.e. not a debug version).%n");
            }

            // show the complete list of children (for debug)
            List<ProcessHandle> descendants = getDescendants(p1Handle);
            printf(" descendants:  %s%n",
                    descendants.stream().map(p -> p.pid())
                           .collect(Collectors.toList()));

            // Verify that all spawned children show up in the descendants  List
            processes.forEach((p, parent) -> {
                Assert.assertEquals(p.isAlive(), true, "Child should be alive: " + p);
                Assert.assertTrue(descendants.contains(p), "Spawned child should be listed in descendants: " + p);
            });

            // Closing JavaChild's InputStream will cause all children to exit
            p1.getOutputStream().close();

            for (ProcessHandle p : descendants) {
                try {
                    p.onExit().get();       // wait for the child to exit
                } catch (ExecutionException e) {
                    Assert.fail("waiting for process to exit", e);
                }
            }
            p1.waitFor();           // wait for spawned process to exit

            // Verify spawned processes are no longer alive
            processes.forEach((ph, parent) -> Assert.assertFalse(ph.isAlive(),
                            "process should not be alive: " + ph));
        } catch (IOException | InterruptedException t) {
            t.printStackTrace();
            throw new RuntimeException(t);
        }
    }

    /**
     * Test destroy of processes.
     * A JavaChild is started and it starts three children.
     * Each one is then checked to be alive and listed by descendants
     * and forcibly destroyed.
     * After they exit they should no longer be listed by descendants.
     */
    @Test
    public static void test3() {
        ConcurrentHashMap<ProcessHandle, ProcessHandle> processes = new ConcurrentHashMap<>();

        try {
            ProcessHandle self = ProcessHandle.current();

            JavaChild p1 = JavaChild.spawnJavaChild("stdin");
            ProcessHandle p1Handle = p1.toHandle();
            printf(" p1: %s%n", p1.pid());

            int newChildren = 3;
            CountDownLatch spawnCount = new CountDownLatch(newChildren);
            // Spawn children and have them wait
            p1.sendAction("spawn", newChildren, "stdin");

            // Gather the PIDs from the output of the spawning process
            p1.forEachOutputLine((s) -> {
                String[] split = s.trim().split(" ");
                if (split.length == 3 && split[1].equals("spawn")) {
                    Long child = Long.valueOf(split[2]);
                    Long parent = Long.valueOf(split[0].split(":")[0]);
                    processes.put(ProcessHandle.of(child).get(), ProcessHandle.of(parent).get());
                    spawnCount.countDown();
                }
            });

            // Wait for all the subprocesses to be listed as started
            Assert.assertTrue(spawnCount.await(Utils.adjustTimeout(30L), TimeUnit.SECONDS),
                    "Timeout waiting for processes to start");

            // Debugging; list descendants that are not expected in processes
            List<ProcessHandle> descendants = ProcessUtil.getDescendants(p1Handle);
            long count = descendants.stream()
                    .filter(ph -> !processes.containsKey(ph))
                    .count();
            if (count > 0) {
                descendants.stream()
                    .filter(ph -> !processes.containsKey(ph))
                    .forEach(ph1 -> ProcessUtil.printProcess(ph1, "Extra process: "));
                ProcessUtil.logTaskList();
                Assert.assertEquals(0, count, "Extra processes in descendants");
            }

            // Verify that all spawned children are alive, show up in the descendants list
            // then destroy them
            processes.forEach((p, parent) -> {
                Assert.assertEquals(p.isAlive(), true, "Child should be alive: " + p);
                Assert.assertTrue(descendants.contains(p), "Spawned child should be listed in descendants: " + p);
                p.destroyForcibly();
            });
            Assert.assertEquals(processes.size(), newChildren, "Wrong number of children");

            // Wait for each of the processes to exit
            processes.forEach((p, parent) ->  {
                for (long retries = Utils.adjustTimeout(100L); retries > 0 ; retries--) {
                    if (!p.isAlive()) {
                        return;                 // not alive, go on to the next
                    }
                    // Wait a bit and retry
                    try {
                        Thread.sleep(100L);
                    } catch (InterruptedException ie) {
                        // try again
                    }
                }
                printf("Timeout waiting for exit of pid %s, parent: %s, info: %s%n",
                        p, parent, p.info());
                Assert.fail("Process still alive: " + p);
            });
            p1.destroyForcibly();
            p1.waitFor();

            // Verify that none of the spawned children are still listed by descendants
            List<ProcessHandle> remaining = getDescendants(self);
            Assert.assertFalse(remaining.remove(p1Handle), "Child p1 should have exited");
            remaining = remaining.stream().filter(processes::containsKey).collect(Collectors.toList());
            Assert.assertEquals(remaining.size(), 0, "Subprocess(es) should have exited: " + remaining);

        } catch (IOException ioe) {
            Assert.fail("Spawn of subprocess failed", ioe);
        } catch (InterruptedException inte) {
            Assert.fail("InterruptedException", inte);
        } finally {
            processes.forEach((p, parent) -> {
                if (p.isAlive()) {
                    ProcessUtil.printProcess(p);
                    p.destroyForcibly();
                }
            });
        }
    }

    /**
     * Test (Not really a test) that dumps the list of all Processes.
     */
    @Test
    public static void test4() {
        printf("    Parent     Child  Info%n");
        Stream<ProcessHandle> s = ProcessHandle.allProcesses();
        ProcessHandle[] processes = s.toArray(ProcessHandle[]::new);
        int len = processes.length;
        ProcessHandle[] parent = new ProcessHandle[len];
        Set<ProcessHandle> processesSet =
                Arrays.stream(processes).collect(Collectors.toSet());
        Integer[] sortindex = new Integer[len];
        for (int i = 0; i < len; i++) {
            sortindex[i] = i;
         }
        for (int i = 0; i < len; i++) {
            parent[sortindex[i]] = processes[sortindex[i]].parent().orElse(null);
        }
        Arrays.sort(sortindex, (i1, i2) -> {
            int cmp = Long.compare((parent[i1] == null ? 0L : parent[i1].pid()),
                    (parent[i2] == null ? 0L : parent[i2].pid()));
            if (cmp == 0) {
                cmp = Long.compare((processes[i1] == null ? 0L : processes[i1].pid()),
                        (processes[i2] == null ? 0L : processes[i2].pid()));
            }
            return cmp;
        });
        boolean fail = false;
        for (int i = 0; i < len; i++) {
            ProcessHandle p = processes[sortindex[i]];
            ProcessHandle p_parent = parent[sortindex[i]];
            ProcessHandle.Info info = p.info();
            String indent = "    ";
            if (p_parent != null) {
                if (!processesSet.contains(p_parent)) {
                    fail = true;
                    indent = "*** ";
                }
            }
            printf("%s %7s, %7s, %s%n", indent, p_parent, p, info);
        }
        Assert.assertFalse(fail, "Parents missing from all Processes");

    }

    /**
     * A test for scale; launch a large number (14) of subprocesses.
     */
    @Test
    public static void test5() {
        ConcurrentHashMap<ProcessHandle, ProcessHandle> processes = new ConcurrentHashMap<>();

        int factor = 2;
        JavaChild p1 = null;
        Instant start = Instant.now();
        try {
            p1 = JavaChild.spawnJavaChild("stdin");
            ProcessHandle p1Handle = p1.toHandle();

            printf("Spawning %d x %d x %d processes, pid: %d%n",
                    factor, factor, factor, p1.pid());

            // Start the first tier of subprocesses
            p1.sendAction("spawn", factor, "stdin");

            // Start the second tier of subprocesses
            p1.sendAction("child", "spawn", factor, "stdin");

            // Start the third tier of subprocesses
            p1.sendAction("child", "child", "spawn", factor, "stdin");

            int newChildren = factor * (1 + factor * (1 + factor));
            CountDownLatch spawnCount = new CountDownLatch(newChildren);

            // Gather the PIDs from the output of the spawning process
            p1.forEachOutputLine((s) -> {
                String[] split = s.trim().split(" ");
                if (split.length == 3 && split[1].equals("spawn")) {
                    Long child = Long.valueOf(split[2]);
                    Long parent = Long.valueOf(split[0].split(":")[0]);
                    processes.put(ProcessHandle.of(child).get(), ProcessHandle.of(parent).get());
                    spawnCount.countDown();
                }
            });

            // Wait for all the subprocesses to be listed as started
            Assert.assertTrue(spawnCount.await(Utils.adjustTimeout(30L), TimeUnit.SECONDS),
                    "Timeout waiting for processes to start");

            // Debugging; list descendants that are not expected in processes
            List<ProcessHandle> descendants = ProcessUtil.getDescendants(p1Handle);
            long count = descendants.stream()
                    .filter(ph -> !processes.containsKey(ph))
                    .count();
            if (count > 0) {
                descendants.stream()
                    .filter(ph -> !processes.containsKey(ph))
                    .forEach(ph1 -> ProcessUtil.printProcess(ph1, "Extra process: "));
                ProcessUtil.logTaskList();
                Assert.assertEquals(0, count, "Extra processes in descendants");
            }

            List<ProcessHandle> subprocesses = getChildren(p1Handle);
            printf(" children:  %s%n",
                    subprocesses.stream().map(p -> p.pid())
                    .collect(Collectors.toList()));

            Assert.assertEquals(getChildren(p1Handle).size(),
                    factor, "expected direct children");
            count = getDescendants(p1Handle).size();
            long totalChildren = factor * factor * factor + factor * factor + factor;
            Assert.assertTrue(count >= totalChildren,
                    "expected at least " + totalChildren + ", actual: " + count);

            List<ProcessHandle> descSubprocesses = getDescendants(p1Handle);
            printf(" descendants:  %s%n",
                    descSubprocesses.stream().map(p -> p.pid())
                    .collect(Collectors.toList()));

            p1.getOutputStream().close();  // Close stdin for the controlling p1
            p1.waitFor();
        } catch (InterruptedException | IOException ex) {
            Assert.fail("Unexpected Exception", ex);
        } finally {
            printf("Duration: %s%n", Duration.between(start, Instant.now()));
            if (p1 != null) {
                p1.destroyForcibly();
            }
            processes.forEach((p, parent) -> {
                if (p.isAlive()) {
                    ProcessUtil.printProcess(p, "Process Cleanup: ");
                    p.destroyForcibly();
                }
            });
        }
    }

}
