/*
 * Copyright (c) 2010, 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 vm.mlvm.anonloader.share;

import java.io.File;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean;
import java.nio.file.Files;
import java.nio.file.Paths;
import nsk.share.test.Stresser;
import vm.share.options.Option;
import vm.share.options.OptionSupport;
import vm.share.options.IgnoreUnknownArgumentsHandler;
import vm.mlvm.share.Env;
import vm.mlvm.share.MlvmTest;
import vm.mlvm.share.CustomClassLoaders;
import vm.share.FileUtils;
import vm.share.UnsafeAccess;

/**
 * Does stress-testing of class loading subsystem.
 * This class should be subclassed by the tests
 * to provide test class data.
 *
 * <p>StressClassLoadingTest performs a number of iterations
 * (the default value is 100 000).
 * Each iteration gets class bytes from the subclass
 * and loads it into JVM using either:
 * <ul>
 *    <li>a custom {@link java.lang.ClassLoader} implementation or
 *    <li>{@link sun.misc.Unsafe#defineAnonymousClass} call.
 * </ul>
 *
 * <p>Loading is done in a separate thread. If this thread is stuck,
 * it is killed after some timeout (default is 10 seconds, please see
 * -parseTimeout option). The class file is saved as hangXX.class, where XX
 * starts at 00 and is increased on every hangup.
 * A prefix can be added to the file name using {@link #setFileNamePrefix}
 *
 * <p>The test fails, if there were hangups.
 *
 * <p>By default, before loading class, the bytes are
 * saved to {@code _AnonkTestee01.class} file in the current directory.
 * If JVM crashes, the bytecodes can be analysed.
 * Class saving is controlled by -saveClassFile option.
 * A prefix can be added to the file name using {@link #setFileNamePrefix}
 * function.
 *
 * <p>There is a tool to load the saved .class file.
 * The tool tries to load class using a number of class loaders. For more
 * information, please see tool documentation: {@link vm.mlvm.tools.LoadClass}.
 *
 * @see vm.mlvm.tools.LoadClass
 */
public abstract class StressClassLoadingTest extends MlvmTest {
    private static final String RESCUE_FILE_NAME = "_AnonkTestee01.class";
    private static final String HUNG_CLASS_FILE_NAME = "hang%02d.class";

    @Option(name = "iterations", default_value = "100000",
            description = "How many times generate a class and parse it")
    private static int iterations;

    @Option(name = "saveClassFile", default_value = "true",
            description = "Save generated class file before loading."
                    + " Useful when VM crashes on loading")
    private static boolean saveClassFile;

    @Option(name = "parseTimeout", default_value = "10000",
            description = "Timeout in millisectionds to detect hung parser"
                    + " thread. The parser thread is killed after the timeout")
    private static int parseTimeout;

    @Option(name = "unsafeLoad", default_value = "false",
            description = "An option for adhoc experiments: load class via "
                    + "Unsafe.defineAnonymousClass(). Since in this way the "
                    + "loading process skips several security checks, if the "
                    + "class is not valid, crashes and assertions are normal.")
    private static boolean unsafeLoad;

    private String fileNamePrefix = "";

    private final static AtomicBoolean classFileMessagePrinted
            = new AtomicBoolean(false);

    /**
     * Sets prefix for names of the files, created by test:
     * _AnonkTestee01.class and hangXX.class.
     *
     * @param p a prefix to add before file name.
     * @throws java.lang.NullPointerException if p is null
     */
    public void setFileNamePrefix(String p) {
        Objects.requireNonNull(p);
        fileNamePrefix = p;
    }

    static volatile boolean optionsSetup = false;
    public static void setupOptions(Object instance) {
        if (!optionsSetup) {
            synchronized (StressClassLoadingTest.class) {
                if (!optionsSetup) {
                    OptionSupport.setup(instance, Env.getArgParser().getRawArguments(), new IgnoreUnknownArgumentsHandler());
                    optionsSetup = true;

                    Env.traceNormal("StressClassLoadingTest options: iterations: " + iterations);
                    Env.traceNormal("StressClassLoadingTest options: unsafeLoad: " + unsafeLoad);
                    Env.traceNormal("StressClassLoadingTest options: parseTimeout: " + parseTimeout);
                    Env.traceNormal("StressClassLoadingTest options: saveClassFile: " + saveClassFile);
                }
            }
        }
    }

    public boolean run() throws Exception {
        setupOptions(this);

        int hangNum = 0;

        Stresser stresser = createStresser();
        stresser.start(iterations);

        while (stresser.continueExecution()) {
            stresser.iteration();

            byte[] classBytes = generateClassBytes();
            Class<?> hostClass = getHostClass();
            String className = hostClass.getName();
            File rescueFile = new File(String.format("%s_%d_%s",
                    fileNamePrefix, stresser.getIteration(), RESCUE_FILE_NAME));
            if (saveClassFile) {
                // Write out the class file being loaded.  It's useful
                // to have if the JVM crashes.
                FileUtils.writeBytesToFile(rescueFile, classBytes);
                if (classFileMessagePrinted.compareAndSet(false, true)) {
                    Env.traceImportant("If the JVM crashes then "
                            + "the class file causing the crash is saved as *_*_"
                            + RESCUE_FILE_NAME);
                }
            }

            Thread parserThread  = new Thread() {
                public void run() {
                    try {
                        Class<?> c;
                        if (unsafeLoad) {
                            c = UnsafeAccess.unsafe.defineAnonymousClass(hostClass, classBytes, null);
                        } else {
                            c = CustomClassLoaders.makeClassBytesLoader(classBytes, className)
                                    .loadClass(className);
                        }
                        c.newInstance();
                    } catch (Throwable e) {
                        Env.traceVerbose(e, "parser caught exception");
                    }
                }
            };

            parserThread.setDaemon(true);
            parserThread.start();
            parserThread.join(parseTimeout);

            if (parserThread.isAlive()) {
                Env.complain("Killing parser thread");
                StackTraceElement[] stack = parserThread.getStackTrace();
                Env.traceImportant(parserThread + " stack trace:");
                for (int i = 0; i < stack.length; ++i) {
                    Env.traceImportant(parserThread + "\tat " + stack[i]);
                }

                if (saveClassFile) {
                    Files.move(rescueFile.toPath(), Paths.get(fileNamePrefix
                            + String.format(HUNG_CLASS_FILE_NAME, hangNum)));
                }
                ++hangNum;
            } else if (saveClassFile) {
                rescueFile.delete();
            }
        }

        stresser.finish();

        if (hangNum > 0) {
            Env.complain("There were " + hangNum + " hangups during parsing."
                    + " The class files, which produced hangup were saved as "
                    + fileNamePrefix + String.format(HUNG_CLASS_FILE_NAME, 0)
                    + "... in the test directory. You may want to analyse them."
                    + " Failing this test because of hangups.");
            return false;
        }

        return true;
    }

    /**
     * Generated class bytes. The method is called for each iteration.
     *
     * @return Byte array with the generated class
     */
    protected abstract byte[] generateClassBytes();

    /**
     * Returns a host class for the generated class.
     *
     * @return A host class that for the generated class
     */
    protected abstract Class<?> getHostClass();
}
