/*
 * Copyright (c) 2003, 2018, 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 nsk.monitoring.stress.thread;

import java.lang.management.*;
import java.io.*;
import nsk.share.*;
import nsk.monitoring.share.*;

public class strace001 {
    public final static String LIB_NAME = "StackTraceController";
    private final static String THREAD_NAME
        = "nsk.monitoring.stress.thread.RunningThread";
    private final static int ITERATIONS = 50;

    public static volatile boolean finish;
    public static volatile boolean testFailed = false;
    public static Object common = new Object();
    public static Integer activeThreads;
    private static Log log;
    private static int depth;
    private static int threadCount;
    private static String[] expectedTrace;
    private static ThreadMonitor monitor;
    private static ThreadController controller;

    public static void main(String[] argv) {
        System.exit(run(argv, System.out) + Consts.JCK_STATUS_BASE);
    }

    public static int run(String[] argv, PrintStream out) {
        ArgumentHandler argHandler = new ArgumentHandler(argv);
        log = new Log(out, argHandler);
        monitor = Monitor.getThreadMonitor(log, argHandler);
        threadCount = argHandler.getThreadCount();
        depth = argHandler.getThreadDepth();
        controller = new ThreadController(log, threadCount, depth,
                                          argHandler.getInvocationType());
        RunningThread threads[] = new RunningThread[threadCount];

        // Fill expectedTrace array according to invocation type that is set in
        // test options
        if ( !fillTrace() ) {
            log.complain("Unknown invocation type: "
                       + controller.getInvocationType());
            return Consts.TEST_FAILED;
        }

        for (int i = 0; i < ITERATIONS; i++) {
            log.display("\nIteration: " + i);
            activeThreads = new Integer(0);
            finish = false;

            // Start all threads. Half of them are user threads,
            // others - deamon
            for (int j = 0; j < threadCount; j++) {
                threads[j] = new RunningThread(j, controller, log, depth);
                threads[j].setDaemon(i % 2 == 0);
                threads[j].start();
            }

            // Wait for all threads to start
            while (activeThreads.intValue() < threadCount)
                Thread.currentThread().yield();
            log.display("All threads started: " + activeThreads);

            // Make a snapshot of stack trace for all threads and check it
            for (int j = 0; j < threadCount; j++) {
                boolean isAlive = threads[j].isAlive();
                ThreadInfo info = monitor.getThreadInfo(threads[j].getId(), Integer.MAX_VALUE);

                // A thread may be dead because of OutOfMemoryError or
                // StackOverflowError
                if (isAlive) {
                    if (info == null) {
                        log.complain("ThreadInfo for thread " + j + " is null, "
                                   + "but Thread.isAlive() returned true.");
                        testFailed = true;
                        continue;
                    }

                    StackTraceElement[] snapshot = info.getStackTrace();
                    if ( !checkTrace(snapshot) ) {
                        log.display("\nSnapshot of thread: " + j);
                        printStackTrace(snapshot);
                        testFailed = true;
                    }
                } else {
                    log.display("Thread " + j + " is dead, skipping it.");
                }
            }

            // Let all threads to complete their job
            finish = true;

            // Wait for all threads to be dead
            for (int j = 0; j < threadCount; j++)
                try {
                    threads[j].join();
                } catch (InterruptedException e) {
                    log.complain("Unexpected exception while joining thread "
                               + j);
                    e.printStackTrace(log.getOutStream());
                    testFailed = true;
                }
            log.display("All threads have died.");
        } // for i

        if (testFailed)
            log.complain("TEST FAILED.");

        return (testFailed) ? Consts.TEST_FAILED : Consts.TEST_PASSED;
    }

    // Fill expectedTrace array according to the invocation type that is set in
    // test options
    private static boolean fillTrace() {
        switch (controller.getInvocationType()) {
            case ThreadController.JAVA_TYPE:
                expectedTrace = new String[] {
                    "java.lang.Thread.sleep"
                    , "java.lang.Thread.yield"
                    , THREAD_NAME + ".waitForSign"
                    , THREAD_NAME + ".recursionJava"
                    , THREAD_NAME + ".run"
                };
                break;

            case ThreadController.NATIVE_TYPE:
                expectedTrace = new String[] {
                    "java.lang.Thread.sleep"
                    , "java.lang.Thread.yield"
                    , THREAD_NAME + ".waitForSign"
                    , THREAD_NAME + ".recursionNative"
                    , THREAD_NAME + ".run"
                };
                break;

            case ThreadController.MIXED_TYPE:
                expectedTrace = new String[] {
                    "java.lang.Thread.sleep"
                    , "java.lang.Thread.yield"
                    , THREAD_NAME + ".waitForSign"
                    , THREAD_NAME + ".recursionNative"
                    , THREAD_NAME + ".recursionJava"
                    , THREAD_NAME + ".run"
                };
                break;

            default:
                return false;
        }

        return true;
    }

    // The method prints stack trace in style JVM does
    private static void printStackTrace(StackTraceElement[] elements) {
        for (int i = 0; i < elements.length; i++) {
            String s = "\t " + i + ": " + elements[i].getClassName() + "."
                     + elements[i].getMethodName();

            if (elements[i].isNativeMethod())
                s = s + "(Native Method)";
            else
                s = s + "(" + elements[i].getFileName() + ":"
                  + elements[i].getLineNumber() + ")";
            log.display(s);
        }
    }

    // The method performs checks of the stack trace
    private static boolean checkTrace(StackTraceElement[] elements) {
        int length = elements.length;
        int expectedLength = depth +3;
        boolean result = true;

        // Check the length of the trace. It must not be greater than
        // expectedLength. Number of recursionJava() or recursionNative()
        // methods must not ne greater than depth, also one Object.wait() or
        // Thread.yield() method, one run( ) and one waitForSign().
        if (length > expectedLength) {
            log.complain("Length of the stack trace is " + length + ", but "
                       + "expected to be not greater than " + expectedLength);
            result = false;
        }

        // Check each element of the snapshot
        for (int i = 0; i < elements.length; i++) {
            if (i == elements.length - 1) {

                // The latest method of the snapshot must be RunningThread.run()
                if ( !checkLastElement(elements[i]) )
                    result = false;
            } else {

                // getClassName() and getMethodName() must return correct values
                // for each element
                if ( !checkElement(i, elements[i]) )
                    result = false;
            }
        }
        return result;
    }

    // The method checks that StackTraceElement.getClassName() and
    // StackTraceElement.getMethodName() return expected values
    private static boolean checkElement(int n, StackTraceElement element) {
        String name = element.getClassName() + "." + element.getMethodName();

        // The latest element is not checked, since it must be "run()"
        for (int i = 0; i < expectedTrace.length - 1; i++) {
            if (expectedTrace[i].equals(name))
                return true;
        }

        log.complain("Unexpected " + n + " element of the stack trace:\n\t"
                   + name);
        return false;
    }

    // The method checks that StackTraceElement.getClassName() returns
    // "RunningThread" and StackTraceElement.getMethodName() returns "run"
    // for the latest element of the snapshot
    private static boolean checkLastElement(StackTraceElement element) {
        String name = element.getClassName() + "." + element.getMethodName();
        String last = expectedTrace[expectedTrace.length - 1];

        if (!last.equals(name)) {
            log.complain("Unexpected last element of the stack trace:\n\t"
                   + name + "\nexpected:\n\t" + last);
            return false;
        }
        return true;
    }
}

// This thread starts a recursion until it reaches specified depth. Then the
// thread waits until it gets a notification from main thread. Pure java
// and native methods are used in the thread. So, the thread is definitly in
// "running" state when main thread performs its checks.
class RunningThread extends Thread {
    private int num;
    private static ThreadController controller;
    private Log log;
    private int depth;
    private boolean mixed = false;
    native int recursionNative(int maxDepth, int currentDepth, boolean returnToJava);

    static {
        try {
            System.loadLibrary(strace001.LIB_NAME);
        } catch (UnsatisfiedLinkError e) {
            System.err.println("Cannot load library " + strace001.LIB_NAME);
            System.err.println("java.library.path: "
                             + System.getProperty("java.library.path"));
            throw e;
        }
    }

    RunningThread(int num, ThreadController controller, Log log, int depth) {
        this.num = num;
        this.controller = controller;
        this.log = log;
        this.depth = depth;
    }

    public void run() {
        int result = 0;
        int invocationType = controller.getInvocationType();

        // This instance of the thread is alive
        synchronized (strace001.common) {
            synchronized (strace001.activeThreads) {
                strace001.activeThreads
                    = new Integer(strace001.activeThreads.intValue() + 1);
            }
        }

        // Choose a method (native or java) to continue recursion
        try {
            switch (invocationType) {
                case ThreadController.JAVA_TYPE:
                    recursionJava(depth, 0);
                    break;
                case ThreadController.NATIVE_TYPE:
                    result = recursionNative(depth, 0, false);

                    if (result == 1) {
                        log.display("Fatal error (OutOfMemoryError or "
                                + "StackOverflow) is thrown in native method of "
                                + " thread " + num);
                        return;
                    } else if (result == 2) {
                        log.complain("Unexpected exception is thrown in native "
                                + "method of thread " + num);
                        strace001.testFailed = true;
                        return;
                    }
                    break;
                case ThreadController.MIXED_TYPE:
                    mixed = true;
                    result = recursionNative(depth, 0, true);

                    if (result == 1) {
                        log.display("Fatal error (OutOfMemoryError or "
                                + "StackOverflow) is thrown in native method of "
                                + " thread " + num);
                        return;
                    } else if (result == 2) {
                        log.complain("Unexpected exception is thrown in native "
                                + "method of thread " + num);
                        strace001.testFailed = true;
                        return;
                    }
                    break;
                default:
                    log.complain("Unknown invocation type: "
                            + controller.getInvocationType());
                    strace001.testFailed = true;
            }
        } catch (OutOfMemoryError e) {
            // Recursion is too deep, so exit peacefully
            log.display("OutOfMemoryError is thrown in thread " + num);
        } catch (StackOverflowError e) {
            // Recursion is too deep, so exit peacefully
            log.display("StackOverflowError is thrown in thread " + num);
        }
    } // run()

    private void recursionJava(int maxDepth, int currentDepth) {
        // A short delay. Otherwise the method will reach the specified depth
        // almost instantly
        try {
            sleep(1);
        } catch (InterruptedException e) {
            log.complain("Unexpected exception");
            e.printStackTrace(log.getOutStream());
            strace001.testFailed = true;
        }

        currentDepth++;
        if (maxDepth > currentDepth) {
            yield();
            if (mixed) {
                int result = recursionNative(maxDepth, currentDepth, true);

                 if (result == 1) {
                     log.display("Fatal error (OutOfMemoryError or "
                               + "StackOverflow) is thrown in native method of "
                               + " thread " + num);
                     return;
                 } else if (result == 2) {
                     log.complain("Unexpected exception is thrown in native "
                                + "method of thread " + num);
                     strace001.testFailed = true;
                     return;
                 }
            } else
                recursionJava(maxDepth, currentDepth);
        }

        waitForSign();
    } // recursionJava()

    private void waitForSign() {
        // When the depth is reached, wait for a notification from main thread
        while (!strace001.finish) {
            try {
                sleep(1);
            } catch (InterruptedException e) {
                log.complain("Unexpected exception");
                e.printStackTrace(log.getOutStream());
                strace001.testFailed = true;
                break;
            }
        }
    } // waitForSign()
} // class RunningThread
