/*
 * Copyright (c) 2017, 2022, 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.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.ArrayList;

import jdk.test.lib.apps.LingeredApp;
import jdk.test.lib.Platform;
import jdk.test.lib.util.CoreUtils;
import jtreg.SkippedException;

/**
 * @test id=xcomp-process
 * @bug 8193124
 * @summary Test the clhsdb 'findpc' command with Xcomp on live process
 * @requires vm.compMode != "Xcomp"
 * @requires vm.hasSA
 * @requires (os.arch != "riscv64" | !(vm.cpu.features ~= ".*qemu.*"))
 * @requires vm.compiler1.enabled
 * @requires vm.opt.DeoptimizeALot != true
 * @library /test/lib
 * @run main/othervm/timeout=480 ClhsdbFindPC true false
 */

/**
 * @test id=xcomp-core
 * @bug 8193124
 * @summary Test the clhsdb 'findpc' command with Xcomp on core file
 * @requires vm.compMode != "Xcomp"
 * @requires vm.hasSA
 * @requires vm.compiler1.enabled
 * @requires vm.opt.DeoptimizeALot != true
 * @library /test/lib
 * @run main/othervm/timeout=480 ClhsdbFindPC true true
 */

/**
 * @test id=no-xcomp-process
 * @bug 8193124
 * @summary Test the clhsdb 'findpc' command w/o Xcomp on live process
 * @requires vm.hasSA
 * @requires (os.arch != "riscv64" | !(vm.cpu.features ~= ".*qemu.*"))
 * @requires vm.compiler1.enabled
 * @library /test/lib
 * @run main/othervm/timeout=480 ClhsdbFindPC false false
 */

/**
 * @test id=no-xcomp-core
 * @bug 8193124
 * @summary Test the clhsdb 'findpc' command w/o Xcomp on core file
 * @requires vm.hasSA
 * @requires vm.compiler1.enabled
 * @library /test/lib
 * @run main/othervm/timeout=480 ClhsdbFindPC false true
 */

public class ClhsdbFindPC {
    static LingeredApp theApp = null;
    static String coreFileName = null;
    static ClhsdbLauncher test = null;

    private static void testFindPC(boolean withXcomp, boolean withCore) throws Exception {
        try {
            String linesep = System.getProperty("line.separator");
            String segvAddress = null;
            List<String> cmds = null;
            String cmdStr = null;
            Map<String, List<String>> expStrMap = null;

            test = new ClhsdbLauncher();

            theApp = new LingeredApp();
            theApp.setForceCrash(withCore);
            if (withXcomp) {
                LingeredApp.startApp(theApp, "-Xcomp", CoreUtils.getAlwaysPretouchArg(withCore));
            } else {
                LingeredApp.startApp(theApp, "-Xint", CoreUtils.getAlwaysPretouchArg(withCore));
            }
            System.out.print("Started LingeredApp ");
            if (withXcomp) {
                System.out.print("(-Xcomp) ");
            } else {
                System.out.print("(-Xint) ");
            }
            System.out.println("with pid " + theApp.getPid());

            if (withCore) {
                String crashOutput = theApp.getOutput().getStdout();
                // Get the core file name if we are debugging a core instead of live process
                coreFileName = CoreUtils.getCoreFileLocation(crashOutput, theApp.getPid());
                // Get the SEGV Address from the following line:
                //  #  SIGSEGV (0xb) at pc=0x00007f20a897f7f4, pid=8561, tid=8562
                String[] parts = crashOutput.split(" pc=");
                String[] tokens = parts[1].split(",");
                segvAddress = tokens[0];

                // Test the 'findpc' command passing in the SEGV address
                cmds = new ArrayList<String>();
                cmdStr = "findpc " + segvAddress;
                cmds.add(cmdStr);
                expStrMap = new HashMap<>();
                if (Platform.isOSX()) {
                    // OSX will only find addresses in JVM libraries, not user or system libraries
                    expStrMap.put(cmdStr, List.of("In unknown location"));
                } else { // symbol lookups not supported with OSX live process
                    expStrMap.put(cmdStr, List.of("Java_jdk_test_lib_apps_LingeredApp_crash"));
                }
                runTest(withCore, cmds, expStrMap);
            }

            // Run 'jstack -v' command to get the pc and other useful values
            cmds = List.of("jstack -v");
            String jStackOutput = runTest(withCore, cmds, null);

            // Extract pc address from the following line:
            //   - LingeredApp.steadyState(java.lang.Object) @bci=1, line=33, pc=0x00007ff18ff519f0, ...
            String pcAddress = null;
            String[] parts = jStackOutput.split("LingeredApp.steadyState");
            String[] tokens = parts[1].split(" ");
            for (String token : tokens) {
                if (token.contains("pc")) {
                    String[] addresses = token.split("=");
                    // addresses[1] represents the address of the Method
                    pcAddress = addresses[1].replace(",","");
                    break;
                }
            }
            if (pcAddress == null) {
                throw new RuntimeException("Cannot find LingeredApp.steadyState pc in output");
            }

            // Test the 'findpc' command passing in the pc obtained from jstack above
            cmds = new ArrayList<String>();
            cmdStr = "findpc " + pcAddress;
            cmds.add(cmdStr);
            expStrMap = new HashMap<>();
            if (withXcomp) {
                expStrMap.put(cmdStr, List.of(
                            "In code in NMethod for jdk/test/lib/apps/LingeredApp.steadyState",
                            "content:",
                            "oops:",
                            "frame size:"));
            } else {
                expStrMap.put(cmdStr, List.of(
                            "In interpreter codelet"));
            }
            runTest(withCore, cmds, expStrMap);

            // Run findpc on a Method*. We can find one in the jstack output. For example:
            // - LingeredApp.steadyState(java.lang.Object) @bci=1, line=33, pc=..., Method*=0x0000008041000208 ...
            // This is testing the PointerFinder support for C++ MetaData types.
            parts = jStackOutput.split("LingeredApp.steadyState");
            parts = parts[1].split("Method\\*=");
            parts = parts[1].split(" ");
            String methodAddr = parts[0];
            cmdStr = "findpc " + methodAddr;
            cmds = List.of(cmdStr);
            expStrMap = new HashMap<>();
            expStrMap.put(cmdStr, List.of("Method ",
                                          "LingeredApp.steadyState",
                                          methodAddr));
            runTest(withCore, cmds, expStrMap);

            // Rerun above findpc command, but this time using "whatis", which is an alias for "findpc".
            cmdStr = "whatis " + methodAddr;
            cmds = List.of(cmdStr);
            expStrMap = new HashMap<>();
            expStrMap.put(cmdStr, List.of("Method ",
                                          "LingeredApp.steadyState",
                                          methodAddr));
            runTest(withCore, cmds, expStrMap);

            // Run "mem -v <addr>/30" on a Method*. The first line will look like:
            //   Address 0x0000152e30403530: Method jdk/test/lib/apps/LingeredApp.steadyState(Ljava/lang/Object;)V@0x0000152e30403530
            // Followed by lines displaying the memory contents, including interpretation
            // of any contents that are addresses.
            cmdStr = "mem -v " + methodAddr + "/30";
            cmds = List.of(cmdStr);
            expStrMap = new HashMap<>();
            expStrMap.put(cmdStr, List.of("Method jdk/test/lib/apps/LingeredApp.steadyState",
                                          methodAddr,
                                          /* The following is from a field in the Method object. */
                                          "In interpreter codelet: method entry point"));
            runTest(withCore, cmds, expStrMap);

            // Run findpc on a JavaThread*. We can find one in the jstack output.
            // The tid for a thread is it's JavaThread*. For example:
            //  "main" #1 prio=5 tid=0x00000080263398f0 nid=0x277e0 ...
            // This is testing the PointerFinder support for all C++ types other than MetaData types.
            parts = jStackOutput.split("tid=");
            parts = parts[1].split(" ");
            String tid = parts[0];  // address of the JavaThread
            cmdStr = "findpc " + tid;
            cmds = List.of(cmdStr);
            expStrMap = new HashMap<>();
            expStrMap.put(cmdStr, List.of("Is of type JavaThread"));
            runTest(withCore, cmds, expStrMap);

            // Use findpc on an address that is only 4 byte aligned and is the
            // last 4 bytes of a 4k page. This is for testing JDK-8292201.
            String badAddress = tid.substring(0, tid.length() - 3) + "ffc";
            cmdStr = "findpc " + badAddress;
            cmds = List.of(cmdStr);
            expStrMap = new HashMap<>();
            expStrMap.put(cmdStr, List.of("In unknown location"));
            runTest(withCore, cmds, expStrMap);

            // Run findpc on a java stack address. We can find one in the jstack output.
            //   "main" #1 prio=5 tid=... nid=0x277e0 waiting on condition [0x0000008025aef000]
            // The stack address is the last word between the brackets.
            // This is testing the PointerFinder support for thread stack addresses.
            parts = jStackOutput.split("tid=");
            parts = parts[1].split(" \\[");
            parts = parts[1].split("\\]");
            String stackAddress = parts[0];  // address of the thread's stack
            if (Long.decode(stackAddress) == 0L) {
                System.out.println("Stack address is " + stackAddress + ". Skipping test.");
            } else {
                cmdStr = "findpc " + stackAddress;
                cmds = List.of(cmdStr);
                expStrMap = new HashMap<>();
                // Note, sometimes a stack address points to a hotspot type, thus allow for "Is of type".
                expStrMap.put(cmdStr, List.of("(In java stack)|(Is of type)"));
                runTest(withCore, cmds, expStrMap);
            }

            // Run 'examine <addr>' using a thread's tid as the address. The
            // examine output will be the of the form:
            //    <tid>: <value>
            // Where <value> is the word stored at <tid>. <value> also happens to
            // be the vtable address. We then run findpc on this vtable address.
            // This tests PointerFinder support for native C++ symbols.
            cmds = List.of("examine " + tid);
            String examineOutput = runTest(withCore, cmds, null);
            // Extract <value>.
            parts = examineOutput.split(tid + ": ");
            String value = parts[1].split(linesep)[0];
            // Use findpc on <value>. The output should look something like:
            //    Address 0x00007fed86f610b8: vtable for JavaThread + 0x10
            cmdStr = "findpc " + value;
            cmds = List.of(cmdStr);
            expStrMap = new HashMap<>();
            if (Platform.isWindows()) {
                expStrMap.put(cmdStr, List.of("jvm.+JavaThread"));
            } else if (Platform.isOSX()) {
                if (withCore) {
                    expStrMap.put(cmdStr, List.of("__ZTV10JavaThread"));
                } else { // address -> symbol lookups not supported with OSX live process
                    expStrMap.put(cmdStr, List.of("In unknown location"));
                }
            } else {
                expStrMap.put(cmdStr, List.of("vtable for JavaThread"));
            }
            String findpcOutput = runTest(withCore, cmds, expStrMap);

            // Determine if we have symbol support. Currently we assume yes except on windows.
            boolean hasSymbols = true;
            if (Platform.isWindows()) {
                if (findpcOutput.indexOf("jvm!JavaThread::`vftable'") == -1) {
                    hasSymbols = false;
                }
            }

            // Run "findsym MaxJNILocalCapacity". The output should look something like:
            //   0x00007eff8e1a0da0: <jdk-dir>/lib/server/libjvm.so + 0x1d81da0
            String symbol = "MaxJNILocalCapacity";
            cmds = List.of("findsym " + symbol);
            expStrMap = new HashMap<>();
            if (!hasSymbols) {
                expStrMap.put(cmdStr, List.of("Symbol not found"));
            }
            String findsymOutput = runTest(withCore, cmds, expStrMap);
            // Run findpc on the result of "findsym MaxJNILocalCapacity". The output
            // should look something like:
            //   Address 0x00007eff8e1a0da0: MaxJNILocalCapacity
            if (hasSymbols) {
                parts = findsymOutput.split("findsym " + symbol + linesep);
                parts = parts[1].split(":");
                String findsymAddress = parts[0].split(linesep)[0];
                cmdStr = "findpc " + findsymAddress;
                cmds = List.of(cmdStr);
                expStrMap = new HashMap<>();
                if (Platform.isOSX() && !withCore) {
                    // address -> symbol lookups not supported with OSX live process
                    expStrMap.put(cmdStr, List.of("Address " + findsymAddress + ": In unknown location"));
                } else {
                    expStrMap.put(cmdStr, List.of("Address " + findsymAddress + ": .*" + symbol));
                }
                runTest(withCore, cmds, expStrMap);
            }
        } catch (SkippedException se) {
            throw se;
        } catch (Exception ex) {
            throw new RuntimeException("Test ERROR " + ex, ex);
        } finally {
            if (!withCore) {
                LingeredApp.stopApp(theApp);
            }
        }
    }

    private static String runTest(boolean withCore, List<String> cmds, Map<String, List<String>> expStrMap)
        throws Exception
    {
        if (withCore) {
            return test.runOnCore(coreFileName, cmds, expStrMap, null);
        } else {
            return test.run(theApp.getPid(), cmds, expStrMap, null);
        }
    }

    public static void main(String[] args) throws Exception {
        boolean withXcomp = Boolean.parseBoolean(args[0]);
        boolean withCore = Boolean.parseBoolean(args[1]);
        System.out.println("Starting the ClhsdbFindPC test");
        testFindPC(withXcomp, withCore);
        System.out.println("Test PASSED");
    }
}
