/* * Copyright (c) 2012, 2016, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * The Universal Permissive License (UPL), Version 1.0 * * Subject to the condition set forth below, permission is hereby granted to any * person obtaining a copy of this software, associated documentation and/or * data (collectively the "Software"), free of charge and under any and all * copyright rights in the Software, and any and all patent rights owned or * freely licensable by each licensor hereunder covering either (i) the * unmodified Software as contributed to or provided by such licensor, or (ii) * the Larger Works (as defined below), to deal in both * * (a) the Software, and * * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if * one is included with the Software each a "Larger Work" to which the Software * is contributed by such licensors), * * without restriction, including without limitation the rights to copy, create * derivative works of, display, perform, and distribute the Software and make, * use, sell, offer for sale, import, export, have made, and have sold the * Software and the Larger Work(s), and to sublicense the foregoing rights on * either these or other terms. * * This license is subject to the following condition: * * The above copyright notice and either this complete permission notice or at a * minimum a reference to the UPL must be included in all copies or substantial * portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ package com.oracle.truffle.sl.test; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.LinkedList; import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import com.oracle.truffle.api.CallTarget; import com.oracle.truffle.api.Truffle; import com.oracle.truffle.api.debug.Breakpoint; import com.oracle.truffle.api.debug.DebugScope; import com.oracle.truffle.api.debug.DebugStackFrame; import com.oracle.truffle.api.debug.DebugValue; import com.oracle.truffle.api.debug.Debugger; import com.oracle.truffle.api.debug.DebuggerSession; import com.oracle.truffle.api.debug.SuspendedEvent; import com.oracle.truffle.api.frame.VirtualFrame; import com.oracle.truffle.api.interop.ForeignAccess; import com.oracle.truffle.api.interop.ForeignAccess.Factory; import com.oracle.truffle.api.interop.ForeignAccess.Factory26; import com.oracle.truffle.api.interop.Message; import com.oracle.truffle.api.interop.TruffleObject; import com.oracle.truffle.api.interop.java.JavaInterop; import com.oracle.truffle.api.nodes.RootNode; import com.oracle.truffle.api.source.Source; import com.oracle.truffle.api.source.SourceSection; import com.oracle.truffle.api.vm.PolyglotEngine; import com.oracle.truffle.api.vm.PolyglotEngine.Value; import com.oracle.truffle.sl.SLLanguage; import java.util.concurrent.Callable; public class SLDebugDirectTest { private static final Object UNASSIGNED = new Object(); private Debugger debugger; private final LinkedList<Runnable> run = new LinkedList<>(); private SuspendedEvent suspendedEvent; private Throwable ex; protected PolyglotEngine engine; protected final ByteArrayOutputStream out = new ByteArrayOutputStream(); protected final ByteArrayOutputStream err = new ByteArrayOutputStream(); private DebuggerSession session; @Before public void before() { suspendedEvent = null; engine = PolyglotEngine.newBuilder().setOut(out).setErr(err).build(); debugger = Debugger.find(engine); session = debugger.startSession((event) -> { suspendedEvent = event; performWork(); suspendedEvent = null; }); run.clear(); } @After public void dispose() { if (engine != null) { engine.dispose(); } } private static Source createFactorial() { return Source.newBuilder("function test() {\n" + " res = fac(2);\n" + " println(res);\n" + " return res;\n" + "}\n" + "function fac(n) {\n" + " if (n <= 1) {\n" + " return 1;\n" + " }\n" + " nMinusOne = n - 1;\n" + " nMOFact = fac(nMinusOne);\n" + " res = n * nMOFact;\n" + " return res;\n" + "}\n").name("factorial.sl").mimeType(SLLanguage.MIME_TYPE).build(); } private static Source createFactorialWithDebugger() { return Source.newBuilder("function test() {\n" + " res = fac(2);\n" + " println(res);\n" + " return res;\n" + "}\n" + "function fac(n) {\n" + " if (n <= 1) {\n" + " return 1;\n" + " }\n" + " nMinusOne = n - 1;\n" + " nMOFact = fac(nMinusOne);\n" + " debugger;\n" + " res = n * nMOFact;\n" + " return res;\n" + "}\n").name("factorial.sl").mimeType(SLLanguage.MIME_TYPE).build(); } private static Source createInteropComputation() { return Source.newBuilder("function test() {\n" + "}\n" + "function interopFunction(notifyHandler) {\n" + " executing = true;\n" + " while (executing == true || executing) {\n" + " executing = notifyHandler.isExecuting;\n" + " }\n" + " return executing;\n" + "}\n").name("interopComputation.sl").mimeType(SLLanguage.MIME_TYPE).build(); } protected final String getOut() { return new String(out.toByteArray()); } protected final String getErr() { try { err.flush(); } catch (IOException e) { } return new String(err.toByteArray()); } @Test public void testBreakpoint() throws Throwable { final Source factorial = createFactorial(); session.install(Breakpoint.newBuilder(factorial).lineIs(8).build()); engine.eval(factorial); assertExecutedOK(); assertLocation("fac", 8, true, "return 1", "n", "1", "nMinusOne", UNASSIGNED, "nMOFact", UNASSIGNED, "res", UNASSIGNED); continueExecution(); Value value = engine.findGlobalSymbol("test").execute(); assertExecutedOK(); Assert.assertEquals("2\n", getOut()); Number n = value.as(Number.class); assertNotNull(n); assertEquals("Factorial computed OK", 2, n.intValue()); } @Test public void testDebuggerBreakpoint() throws Throwable { final Source factorial = createFactorialWithDebugger(); engine.eval(factorial); assertExecutedOK(); assertLocation("fac", 12, true, "debugger", "n", "2", "nMinusOne", "1", "nMOFact", "1", "res", UNASSIGNED); continueExecution(); Value value = engine.findGlobalSymbol("test").execute(); assertExecutedOK(); Assert.assertEquals("2\n", getOut()); Number n = value.as(Number.class); assertNotNull(n); assertEquals("Factorial computed OK", 2, n.intValue()); } @Test public void stepInStepOver() throws Throwable { doStepInStepOver(true); } @Test public void stepInStepOverWithJavaInterop() throws Throwable { doStepInStepOver(false); } private void doStepInStepOver(boolean direct) throws Throwable { final Source factorial = createFactorial(); engine.eval(factorial); session.suspendNextExecution(); assertLocation("test", 2, true, "res = fac(2)", "res", UNASSIGNED); stepInto(1); assertLocation("fac", 7, true, "n <= 1", "n", "2", "nMinusOne", UNASSIGNED, "nMOFact", UNASSIGNED, "res", UNASSIGNED); stepOver(1); assertLocation("fac", 10, true, "nMinusOne = n - 1", "n", "2", "nMinusOne", UNASSIGNED, "nMOFact", UNASSIGNED, "res", UNASSIGNED); stepOver(1); assertLocation("fac", 11, true, "nMOFact = fac(nMinusOne)", "n", "2", "nMinusOne", "1", "nMOFact", UNASSIGNED, "res", UNASSIGNED); stepOver(1); assertLocation("fac", 12, true, "res = n * nMOFact", "n", "2", "nMinusOne", "1", "nMOFact", "1", "res", UNASSIGNED); stepOver(1); assertLocation("fac", 13, true, "return res", "n", "2", "nMinusOne", "1", "nMOFact", "1", "res", "2"); stepOver(1); assertLocation("test", 2, false, "fac(2)", "res", UNASSIGNED); stepOver(1); assertLocation("test", 3, true, "println(res)", "res", "2"); stepOut(); Value value = engine.findGlobalSymbol("test"); Number result; if (direct) { value = value.execute(); result = value.as(Number.class); } else { final TruffleObject fn = (TruffleObject) value.get(); Callable<?> test = JavaInterop.asJavaFunction(Callable.class, fn); result = (Number) test.call(); } assertExecutedOK(); assertNotNull(result); assertEquals("Factorial computed OK", 2, result.intValue()); } @Test public void testPause() throws Throwable { final Source interopComp = createInteropComputation(); engine.eval(interopComp); assertExecutedOK(); final ExecNotifyHandler nh = new ExecNotifyHandler(); // Do pause after execution has really started new Thread() { @Override public void run() { nh.waitTillCanPause(); session.suspendNextExecution(); } }.start(); run.addLast(() -> { // paused assertNotNull(suspendedEvent); int line = suspendedEvent.getSourceSection().getStartLine(); Assert.assertTrue("Unexpected line: " + line, 5 <= line && line <= 6); final DebugStackFrame frame = suspendedEvent.getTopStackFrame(); DebugScope scope = frame.getScope(); DebugValue slot = scope.getDeclaredValue("executing"); if (slot == null) { slot = scope.getParent().getDeclaredValue("executing"); } Assert.assertNotNull(slot); Assert.assertNotNull("Value is null", slot.toString()); suspendedEvent.prepareContinue(); nh.pauseDone(); }); Value value = engine.findGlobalSymbol("interopFunction").execute(nh); assertExecutedOK(); // Assert.assertFalse(debugger.isExecuting()); Boolean n = value.as(Boolean.class); assertNotNull(n); assertTrue("Interop computation OK", !n); } private static Source createNull() { return Source.newBuilder("function nullTest() {\n" + " res = doNull();\n" + " return res;\n" + "}\n" + "function doNull() {\n" + "}\n").name("nullTest.sl").mimeType(SLLanguage.MIME_TYPE).build(); } @Test public void testNull() throws Throwable { final Source nullTest = createNull(); engine.eval(nullTest); session.suspendNextExecution(); assertLocation("nullTest", 2, true, "res = doNull()", "res", UNASSIGNED); stepInto(1); assertLocation("nullTest", 3, true, "return res", "res", "NULL"); continueExecution(); Value value = engine.findGlobalSymbol("nullTest").execute(); assertExecutedOK(); String val = value.as(String.class); assertNotNull(val); assertEquals("SL displays null as NULL", "NULL", val); } private void performWork() { try { if (ex == null && !run.isEmpty()) { Runnable c = run.removeFirst(); c.run(); } } catch (Throwable e) { ex = e; } } private void stepOver(final int size) { run.addLast(() -> { suspendedEvent.prepareStepOver(size); }); } private void stepOut() { run.addLast(() -> { suspendedEvent.prepareStepOut(1); }); } private void continueExecution() { run.addLast(() -> { suspendedEvent.prepareContinue(); }); } private void stepInto(final int size) { run.addLast(() -> { suspendedEvent.prepareStepInto(size); }); } private void assertLocation(final String name, final int line, final boolean isBefore, final String code, final Object... expectedFrame) { run.addLast(() -> { assertNotNull(suspendedEvent); final SourceSection suspendedSourceSection = suspendedEvent.getSourceSection(); Assert.assertEquals(line, suspendedSourceSection.getStartLine()); Assert.assertEquals(code, suspendedSourceSection.getCode()); Assert.assertEquals(isBefore, suspendedEvent.isHaltedBefore()); final DebugStackFrame frame = suspendedEvent.getTopStackFrame(); assertEquals(name, frame.getName()); for (int i = 0; i < expectedFrame.length; i = i + 2) { final String expectedIdentifier = (String) expectedFrame[i]; final Object expectedValue = expectedFrame[i + 1]; DebugScope scope = frame.getScope(); DebugValue slot = scope.getDeclaredValue(expectedIdentifier); while (slot == null && (scope = scope.getParent()) != null) { slot = scope.getDeclaredValue(expectedIdentifier); } if (expectedValue != UNASSIGNED) { Assert.assertNotNull(expectedIdentifier, slot); final String slotValue = slot.as(String.class); Assert.assertEquals(expectedValue, slotValue); } else { Assert.assertNull(expectedIdentifier, slot); } } run.removeFirst().run(); }); } private void assertExecutedOK() throws Throwable { Assert.assertTrue(getErr(), getErr().isEmpty()); if (ex != null) { if (ex instanceof AssertionError) { throw ex; } else { throw new AssertionError("Error during execution", ex); } } assertTrue("Assuming all requests processed: " + run, run.isEmpty()); } private static class ExecNotifyHandler implements TruffleObject { private final ExecNotifyHandlerForeign nhf = new ExecNotifyHandlerForeign(this); private final ForeignAccess access = ForeignAccess.create(null, nhf); private final Object pauseLock = new Object(); private boolean canPause; private volatile boolean pauseDone; @Override public ForeignAccess getForeignAccess() { return access; } private void waitTillCanPause() { synchronized (pauseLock) { while (!canPause) { try { pauseLock.wait(); } catch (InterruptedException iex) { } } } } void setCanPause() { synchronized (pauseLock) { canPause = true; pauseLock.notifyAll(); } } private void pauseDone() { pauseDone = true; } boolean isPauseDone() { return pauseDone; } } private static class ExecNotifyHandlerForeign implements Factory26, Factory { private final ExecNotifyHandler nh; ExecNotifyHandlerForeign(ExecNotifyHandler nh) { this.nh = nh; } @Override public CallTarget accessIsNull() { throw new UnsupportedOperationException("Not supported yet."); } @Override public CallTarget accessIsExecutable() { throw new UnsupportedOperationException("Not supported yet."); } @Override public CallTarget accessIsBoxed() { throw new UnsupportedOperationException("Not supported yet."); } @Override public CallTarget accessHasSize() { throw new UnsupportedOperationException("Not supported yet."); } @Override public CallTarget accessGetSize() { throw new UnsupportedOperationException("Not supported yet."); } @Override public CallTarget accessUnbox() { throw new UnsupportedOperationException("Not supported yet."); } @Override public CallTarget accessRead() { return Truffle.getRuntime().createCallTarget(new ExecNotifyReadNode(nh)); } @Override public CallTarget accessWrite() { throw new UnsupportedOperationException("Not supported yet."); } @Override public CallTarget accessExecute(int i) { throw new UnsupportedOperationException("Not supported yet."); } @Override public CallTarget accessInvoke(int i) { throw new UnsupportedOperationException("Not supported yet."); } @Override public CallTarget accessNew(int i) { throw new UnsupportedOperationException("Not supported yet."); } @Override public CallTarget accessMessage(Message msg) { throw new UnsupportedOperationException("Not supported yet."); } @Override public boolean canHandle(TruffleObject to) { return (to instanceof ExecNotifyHandler); } @Override public CallTarget accessKeyInfo() { return null; } @Override public CallTarget accessKeys() { return null; } @Override public CallTarget accessIsPointer() { throw new UnsupportedOperationException("Not supported yet."); } @Override public CallTarget accessAsPointer() { throw new UnsupportedOperationException("Not supported yet."); } @Override public CallTarget accessToNative() { throw new UnsupportedOperationException("Not supported yet."); } } private static class ExecNotifyReadNode extends RootNode { private final ExecNotifyHandler nh; ExecNotifyReadNode(ExecNotifyHandler nh) { super(null); this.nh = nh; } @Override public Object execute(VirtualFrame vf) { nh.setCanPause(); return !nh.isPauseDone(); } } }