/*
 * Copyright (c) 2006, 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.share.jdi;

import nsk.share.Consts;
import nsk.share.TestBug;
import nsk.share.jpda.AbstractDebuggeeTest;
import java.io.*;
import java.util.*;

/*
 * This class serial executes several JDI tests based on nsk.share.jdi.TestDebuggerType2 in single VM
 * SerialExecutionDebugger is used together with SerialExecutionDebuggee, execution process is following:
 *
 *  - SerialExecutionDebugger reads tests to execute from input file, test description is debugger class name and test's parameters,
 *  if 'shuffle' option is specified in input file debugger executes tests in random order (input file should contain line "OPTIONS:shuffle").
 *  SerialExecutionDebugger can execute tests several times in loop, number of iterations should be specified in input file in following manner:
 *  OPTIONS:iterations <iterations_number>.
 *
 *  - SerialExecutionDebugger starts debuggee VM with main class SerialExecutionDebuggee,
 *  initializes IOPipe and 'debuggee' object which represents debuggee VM
 *
 *  - for each test from input file:
 *
 *      - SerialExecutionDebugger creates object of current debugger and initializes it with already created pipe and debuggee
 *      - SerialExecutionDebugger sends command to SerialExecutionDebuggee:  'COMMAND_EXECUTE_DEBUGGEE <CurrentDebuggeeName>'
 *      (CurrentDebuggeeName name should provide current debugger), and waits READY signal from debuggee
 *      - SerialExecutionDebuggee parses received command, extracts debugee name, creates object of current debuggee, which should be
 *      subclass of nsk.share.jpda.AbstractDebuggeeTestName
 *      - SerialExecutionDebuggee executes current debuggee's method 'doTest()', in this method debuggee sends signal READY
 *      and waits debugger command
 *      - SerialExecutionDebugger receives signal READY and executes current debugger's method 'doTest()', in
 *      this method debugger should perform test
 *      - when debugger method doTest() finishes SerialExecutionDebugger checks is this test passed or failed and
 *      sends command QUIT to the current debuggee, and when current debuggee finishes sends command 'COMMAND_CLEAR_DEBUGGEE' to the SerialExecutionDebuggee,
 *      after this command SerialExecutionDebugger and SerialExecutionDebuggee ready to execute next test
 *
 *  - when all tests was executed SerialExecutionDebugger sends command QUIT to the SerialExecutionDebuggee and exits
 *
 * SerialExecutionDebugger requires "-configFile <ini-file>" parameter, <ini-file> - file with list of tests for execution
 */
public class SerialExecutionDebugger extends TestDebuggerType2 {
    static public void main(String[] args) {
        System.exit(Consts.JCK_STATUS_BASE + new SerialExecutionDebugger().runIt(args, System.out));
    }

    public String debuggeeClassName() {
        return SerialExecutionDebuggee.class.getName();
    }

    // contains test's debugger class name and test parameters
    static class Test {
        public Test(String debuggerClassName, String[] arguments) {
            this.debuggerClassName = debuggerClassName;
            this.arguments = arguments;
        }

        public String argumentsToString() {
            String result = "";

            for (String argument : arguments)
                result += argument + " ";

            return result;
        }

        String debuggerClassName;

        String arguments[];
    }

    private Test tests[];

    // how many times execute tests
    private int iterations = 1;

    // requires "-configFile <ini-file>" parameter, <ini-file> - file with list
    // of tests for execution
    protected String[] doInit(String args[], PrintStream out) {
        args = super.doInit(args, out);

        String configFileName = null;

        ArrayList<String> standardArgs = new ArrayList<String>();

        for (int i = 0; i < args.length; i++) {
            if (args[i].equals("-configFile") && (i < args.length - 1)) {
                configFileName = args[i + 1];
                i++;
            } else
                standardArgs.add(args[i]);
        }

        if (configFileName == null) {
            throw new TestBug("Config file wasn't specified (use option -configFile <file name>)");
        }

        tests = parseConfigFile(configFileName);

        if (tests.length == 0)
            throw new TestBug("Tests to run were not specified");

        return standardArgs.toArray(new String[] {});
    }

    // read test names and test parameters from ini-file
    private Test[] parseConfigFile(String fileName) {
        List<Test> result = new ArrayList<Test>();

        boolean shuffle = false;

        try {
            File file = new File(fileName);

            LineNumberReader lineReader = new LineNumberReader(new FileReader(file));

            String line = null;

            while ((line = lineReader.readLine()) != null) {
                // skip empty lines and comments started with '#"
                if (line.length() == 0 || line.startsWith("#"))
                    continue;

                if (line.startsWith("OPTIONS:")) {
                    String arguments[] = line.substring(8).split(" ");

                    for (int i = 0; i < arguments.length; i++) {
                        if (arguments[i].equalsIgnoreCase("shuffle"))
                            shuffle = true;
                        else if (arguments[i].equalsIgnoreCase("iterations") && (i < (arguments.length - 1))) {
                            iterations = Integer.parseInt(arguments[i + 1]);
                            i++;
                        }
                    }

                    continue;
                }

                StreamTokenizer tokenizer = new StreamTokenizer(new StringReader(line));
                tokenizer.resetSyntax();
                tokenizer.wordChars(Integer.MIN_VALUE, Integer.MAX_VALUE);
                tokenizer.whitespaceChars(' ', ' ');

                if (tokenizer.nextToken() != StreamTokenizer.TT_WORD)
                    throw new TestBug("Invalid ini file format");

                String testClassName = tokenizer.sval;
                List<String> parameters = new ArrayList<String>();

                int token;
                while ((token = tokenizer.nextToken()) != StreamTokenizer.TT_EOF) {
                    if (token == StreamTokenizer.TT_WORD) {
                        if (tokenizer.sval.equals("$CLASSPATH"))
                            parameters.add(classpath);
                        else
                            parameters.add(tokenizer.sval);
                    }

                    if (token == StreamTokenizer.TT_NUMBER) {
                        parameters.add("" + tokenizer.nval);
                    }
                }

                result.add(new Test(testClassName, parameters.toArray(new String[] {})));
            }

        } catch (IOException e) {
            throw new TestBug("Exception during config file parsing: " + e);
        }

        if (shuffle) {
            if (testWorkDir == null)
                throw new TestBug("Debugger requires -testWorkDir parameter");

            Collections.shuffle(result);

            // save resulted tests sequence in file (to simplify reproducing)
            try {
                File file = new File(testWorkDir + File.separator + "run.tests");
                file.createNewFile();

                PrintWriter writer = new PrintWriter(new FileWriter(file));

                for (Test test : result) {
                    writer.println(test.debuggerClassName + " " + test.argumentsToString());
                }

                writer.close();
            } catch (IOException e) {
                throw new TestBug("Unexpected IOException: " + e);
            }
        }

        System.out.println("Tests execution order: ");
        for (Test test : result) {
            System.out.println(test.debuggerClassName + " " + test.argumentsToString());
        }

        return result.toArray(new Test[] {});
    }

    public void doTest() {

        stresser.start(iterations);

        try {
            if (iterations == 1) {
                /*
                 * Since many test couldn't be run in single VM twice and test config specifies only 1 iteration don't
                 * multiple iterations by iterations factor and execute tests once (not depending on iterations factor)
                 */
                executeTests();
            } else {
                while (stresser.continueExecution()) {
                    if (!executeTests()) {
                        // if error occured stop execution
                        break;
                    }
                }
            }
        } finally {
            stresser.finish();
        }
    }

    boolean executeTests() {
        // maximum execution time of single test
        long maxExecutionTime = 0;

        for (Test test : tests) {
            long testStartTime = System.currentTimeMillis();

            TestDebuggerType2 debugger = null;

            try {
                // create debugger object
                Class debuggerClass = Class.forName(test.debuggerClassName);

                if (!TestDebuggerType2.class.isAssignableFrom(debuggerClass)) {
                    setSuccess(false);
                    log.complain("Invalid debugger class: " + debuggerClass);
                    return false;
                }

                // init test debugger, pass to the debugger already created
                // objects: argHandler, log, pipe, debuggee, vm
                debugger = (TestDebuggerType2) debuggerClass.newInstance();
                debugger.initDebugger(argHandler, log, pipe, debuggee, vm);
                debugger.doInit(test.arguments, System.out);
            } catch (Exception e) {
                setSuccess(false);
                log.complain("Unexpected exception during debugger initialization: " + e);
                e.printStackTrace(log.getOutStream());

                return false;
            }

            log.display("Execute debugger: " + debugger);

            // send command to the SerialExecutionDebuggee (create debuggee
            // object)
            pipe.println(SerialExecutionDebuggee.COMMAND_EXECUTE_DEBUGGEE + ":" + debugger.debuggeeClassName());

            // wait first READY from AbstractDebuggeeTest.doTest() (debuggee
            // sends this command when it was initialized and ready for
            // test)
            if (!isDebuggeeReady())
                return false;

            try {
                // here debuggee should be ready for test and current
                // debugger may perform test
                debugger.doTest();

                if (debugger.getSuccess()) {
                    log.display("Debugger " + debugger + " finished successfully");
                } else {
                    setSuccess(false);
                    log.complain("Debugger " + debugger + " finished with errors");
                }
            } catch (TestBug testBug) {
                setSuccess(false);
                log.complain("Test bug in " + debugger + ": " + testBug);
                testBug.printStackTrace(log.getOutStream());
            } catch (Throwable t) {
                setSuccess(false);
                log.complain("Unexpected exception during test execution(debugger: " + debugger + "): " + t);
                t.printStackTrace(log.getOutStream());
            }

            // send QUIT command to the current debuggee
            pipe.println(AbstractDebuggeeTest.COMMAND_QUIT);

            if (!isDebuggeeReady())
                return false;

            // send command to the SerialExecutionDebuggee
            pipe.println(SerialExecutionDebuggee.COMMAND_CLEAR_DEBUGGEE);

            if (!isDebuggeeReady())
                return false;

            long testExecutionTime = System.currentTimeMillis() - testStartTime;

            if (testExecutionTime > maxExecutionTime)
                maxExecutionTime = testExecutionTime;

            if (maxExecutionTime > stresser.getTimeLeft()) {
                log.display("WARNING: stop test execution because of timeout " +
                                "(max execution time for single test: " + maxExecutionTime + ", time left: " + stresser.getTimeLeft() + ")");
                return false;
            }
        }

        return true;
    }
}
