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

/**
 * @test
 * @summary Call popFrames() on threads in various states not covered
 *          well by other tests. Most notably, this test includes
 *          test cases for a suspended but unmounted virtual thread.
 *          It is mostly for testing for OpaqueFrameException and
 *          NativeMethodException.
 *
 * @run build TestScaffold VMConnection TargetListener TargetAdapter
 * @run compile -g PopFramesTest.java
 * @run driver PopFramesTest SLEEP_NATIVE
 * @run driver PopFramesTest LOOP_NATIVE
 * @run driver PopFramesTest SLEEP_PRENATIVE
 * @run driver PopFramesTest LOOP_PRENATIVE
 * @run driver PopFramesTest SLEEP_NONATIVE
 * @run driver PopFramesTest LOOP_NONATIVE
 */
import com.sun.jdi.*;
import com.sun.jdi.event.*;
import java.util.*;

/*
 * There are six test modes covered by this test:
 *   SLEEP_NATIVE
 *   LOOP_NATIVE
 *   SLEEP_PRENATIVE
 *   LOOP_PRENATIVE
 *   SLEEP_NONATIVE
 *   LOOP_NONATIVE
 *
 * SLEEP:     the debuggee blocks in Thread.sleep().
 * LOOP:      the debuggee sits in a tight loop.
 * NATIVE:    there is a native frame within the set of frames to pop.
 * PRENATIVE: there is a native frame before the set of frames to pop.
 * NONATIVE:  there is no native frame (purposefully) present in the stack.
 *
 * In all cases the thread is suspended and errors such as IllegalArgumentException
 * and InvalidStackFrameException should not happen. The popFrames() calls should
 * either pass, or produce OpaqueFrameException or NativeMethodException.
 *
 * Call stacks for each test mode (and expected result):
 *  - Note in all cases the popMethod() frame is the frame passed to popFrames().
 *  - Note that Thread.sleep() usually results in the native Thread.sleep0() frame
 *    being at the top of the stack. However, for a mounted virtual thread that is
 *    not pinned, it does not result in any native frames due to how the VM
 *    parks non-pinned virtual threads.
 *
 * SLEEP_NATIVE (NativeMethodException):
 *   Thread.sleep() + methods called by Thread.sleep()
 *   loopOrSleep()
 *   upcallMethod()
 *   doUpcall()  <-- native method
 *   popMethod()
 *   main()
 *
 * LOOP_NATIVE (NativeMethodException):
 *   loopOrSleep()  <-- tight loop
 *   upcallMethod()
 *   doUpcall()  <-- native method
 *   popMethod()
 *   main()
 *
 * SLEEP_PRENATIVE (NativeMethodException due to Thread.sleep() blocking in a native method):
 *   Thread.sleep() + methods called by Thread.sleep()
 *   loopOrSleep()
 *   popMethod()
 *   upcallMethod()
 *   doUpcall()  <-- native method
 *   main()
 *
 * LOOP_PRENATIVE (no exception):
 *   loopOrSleep()  <-- tight loop
 *   popMethod()
 *   upcallMethod()
 *   doUpcall()  <-- native method
 *   main()
 *
 * SLEEP_NONATIVE (NativeMethodException for platform thread or OpaqueFrameException
 * for virtual thread. See explanation in runTests().):
 *   Thread.sleep() + methods called by Thread.sleep()
 *   loopOrSleep()
 *   popMethod()
 *   main()
 *
 * LOOP_NONATIVE (no exception):
 *   loopOrSleep()  <-- tight loop
 *   popMethod()
 *   main()
 */

class PopFramesTestTarg {
    static TestMode mode;

    static {
        System.loadLibrary("PopFramesTestTarg");
    }

    /*
     * This is the method whose frame (and all those after it) will be popped.
     */
    public static void popMethod() {
        System.out.println("    debuggee: in popMethod");
        if (mode.isCallNative()) {
            doUpcall();
        } else {
            loopOrSleep();
        }
    }

    public static void loopOrSleep() {
        if (mode.isDoLoop()) {
            while (true);
        } else {
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }

    public static native void doUpcall(); // native method that will call upcallMethod()

    public static void upcallMethod() {
        if (mode.isCallPrenative()) {
            popMethod();
        } else {
            loopOrSleep();
        }
    }

    public static void main(String[] args) {
        System.out.println("    debuggee: Howdy!");

        // We expect just one argument, which is the test mode, such as SLEEP_NONATIVE.
        if (args.length != 1) {
            throw new RuntimeException("Must pass 1 arguments to PopFramesTestTarg");
        }
        System.out.println("    debuggee: args[0]: " + args[0]);
        mode = Enum.valueOf(TestMode.class, args[0]); // convert test mode string to an enum
        System.out.println("    debuggee: test mode: " + mode);

        if (mode.isCallNative()) {
            popMethod(); // call popMethod() directly, and it will call out to native
        } else if (mode.isCallPrenative()) {
            doUpcall();  // call native method that will call back into java to call popMethod()
        } else {
            popMethod(); // call popMethod() directly
        }

        System.out.println("    debuggee: Goodbye from PopFramesTest!");
    }
}

/*
 * The different modes the test can be run in. See test description comment above.
 */
enum TestMode {
    SLEEP_NATIVE,
    LOOP_NATIVE,
    SLEEP_PRENATIVE,
    LOOP_PRENATIVE,
    SLEEP_NONATIVE,
    LOOP_NONATIVE;

    // Returns true if debuggee should block in an infinite loop. Otherwise it calls Thread.sleep().
    boolean isDoLoop() {
        return this == LOOP_NATIVE || this == LOOP_PRENATIVE || this == LOOP_NONATIVE;
    }

    // Returns true if debuggee should introduce a native frame within the set of frames to pop.
    boolean isCallNative() {
        return this == LOOP_NATIVE || this == SLEEP_NATIVE;
    }

    // Returns true if debuggee should introduce a native frame before the set of frames to pop.
    // The purpose is to cause the virtual thread to be pinned.
    boolean isCallPrenative() {
        return this == LOOP_PRENATIVE || this == SLEEP_PRENATIVE;
    }
}

/********** test program **********/

public class PopFramesTest extends TestScaffold {
    private static TestMode mode;

    PopFramesTest(String args[]) {
        super(args);
    }

    public static void main(String[] args) throws Exception {
        // We should get one argument that indicates the test mode, such as SLEEP_NONATIVE.
        if (args.length != 1) {
            throw new RuntimeException("Must pass one argument to PopFramesTestTarg");
        }
        mode = Enum.valueOf(TestMode.class, args[0]); // convert test mode string to an enum

        /*
         * The @run command looks something like:
         *   @run driver PopFramesTest SLEEP_NONATIVE
         * We need to pass SLEEP_NONATIVE to the debuggee. We also need to insert
         * -Djava.library.path so the native method can be accessed if called.
         */
        String nativePath = "-Djava.library.path=" + System.getProperty("java.library.path");
        String[] newArgs = new String[2];
        newArgs[0] = nativePath;
        newArgs[1] = args[0]; // pass test mode, such as SLEEP_NONATIVE

        new PopFramesTest(newArgs).startTests();
    }

    StackFrame frameFor(ThreadReference thread, String methodName) throws Exception {
        Iterator it = thread.frames().iterator();

        while (it.hasNext()) {
            StackFrame frame = (StackFrame)it.next();
            if (frame.location().method().name().equals(methodName)) {
                return frame;
            }
        }
        failure("FAIL: " + methodName + " not on stack");
        return null;
    }

    public void printStack(ThreadReference thread, String msg) throws Exception {
        System.out.println(msg);
        List<StackFrame> stack_frames = thread.frames();
        int i = 0;
        String sourceName;
        for (StackFrame f : stack_frames) {
            try {
                sourceName = f.location().sourceName();
            } catch (AbsentInformationException aie) {
                sourceName = "Unknown source";
            }
            System.out.println("frame[" + i++ +"]: " + f.location().method() +
                               " (bci:"+ f.location().codeIndex() + ")" +
                               " (" + sourceName + ":"+ f.location().lineNumber() + ")");
        }
    }

    /********** test core **********/

    protected void runTests() throws Exception {
        BreakpointEvent bpe = startTo("PopFramesTestTarg", "loopOrSleep", "()V");
        ThreadReference mainThread = bpe.thread();

        // Resume main thread until it is in Thread.sleep() or the infinite loop.
        mainThread.resume();
        try {
            Thread.sleep(1000); // give thread chance to get into Thread.sleep() or loop
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        mainThread.suspend(); // Suspend thread while in Thread.sleep() or loop
        printStack(mainThread, "Debuggee stack before popFrames():");

        /*
         * Figure out which exception popFrames() should throw.
         */
        Class expected_exception;
        switch(mode) {
        case SLEEP_NATIVE:
        case LOOP_NATIVE:
        case SLEEP_PRENATIVE:
            /*
             * For the two NATIVE cases, there is a native frame within the set of frames
             * to pop. For the SLEEP_PRENATIVE case, there also ends up being a native
             * frame. It will either be Thread.sleep0() for platform threads or
             * Unsafe.park() for virtual threads. See the SLEEP_NATIVE comment below
             * for more details.
             */
            expected_exception = NativeMethodException.class;
            break;
        case LOOP_PRENATIVE:
        case LOOP_NONATIVE:
            /*
             * For these two test cases, there are no native frames within the set of
             * frames to pop, nor in the frame previous to the frame to pop, so no
             * exception is expected.
             */
            expected_exception = null;
            break;
        case SLEEP_NONATIVE:
            /*
             * For platform threads, Thread.sleep() results in the Thread.sleep0() native
             * frame on the stack, so the end result is NativeMethodException. For virtual
             * threads it is not quite so simple. If the thead is pinned (such as when
             * there is already a native method on the stack), you end up in
             * VirtualThread.parkOnCarrierThread(), which calls Unsafe.park(), which is a
             * native method, so again this results in NativeMethodException. However, for
             * a virtual thread that is not pinned (which is true for this test case), you
             * end up with no native methods on the stack due to how Continuation.yield()
             * works. So you have an unmounted virtual thread with no native frames, which
             * results in OpaqueFrameException being thrown.
             */
            if (DebuggeeWrapper.isVirtual()) {
                expected_exception = OpaqueFrameException.class;
            } else {
                expected_exception = NativeMethodException.class;
            }
            break;
        default:
            throw new RuntimeException("Bad test mode: " + mode);
        }

        /*
         * Pop all the frames up to and including the popMethod() frame.
         */
        try {
            mainThread.popFrames(frameFor(mainThread, "popMethod"));
            if (expected_exception != null) {
                failure("failure: popFrames() did not get expected exception: " + expected_exception);
            }
        } catch (Exception ex) {
            if (expected_exception == ex.getClass()) {
                System.out.println("success: popFrames() got expected exception: " + ex);
            } else {
                failure("failure: popFrames() got unexpected exception: " + ex);
            }
        }

        printStack(mainThread, "Debuggee stack after popFrames():");

        /*
         * Most tests do a listenUntilVMDisconnect() here, but there is no real need for it
         * with this test, and doing so would require finding a way to get the debuggee
         * to exit the endless loop it might be in. When we return, TestScaffold will
         * call TestScaffold.shutdown(), causing the debuggee process to be terminated quickly.
         */

        if (testFailed) {
            throw new Exception("PopFramesTest failed");
        }
        System.out.println("Passed:");
    }
}