/*
* 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.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
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.SuspendedCallback;
import com.oracle.truffle.api.debug.SuspendedEvent;
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.sl.SLLanguage;
import com.oracle.truffle.tck.DebuggerTester;
import java.util.Collection;
import java.util.Iterator;
import static org.junit.Assert.fail;
public class SLDebugTest {
private DebuggerTester tester;
@Before
public void before() {
tester = new DebuggerTester();
}
@After
public void dispose() {
tester.close();
}
private void startEval(Source code) {
tester.startEval(code);
}
private static Source slCode(String code) {
return Source.newBuilder(code).name("testing").mimeType(SLLanguage.MIME_TYPE).build();
}
private DebuggerSession startSession() {
return tester.startSession();
}
private String expectDone() {
return tester.expectDone();
}
private void expectSuspended(SuspendedCallback callback) {
tester.expectSuspended(callback);
}
protected SuspendedEvent checkState(SuspendedEvent suspendedEvent, String name, final int expectedLineNumber, final boolean expectedIsBefore, final String expectedCode,
final String... expectedFrame) {
final int actualLineNumber = suspendedEvent.getSourceSection().getStartLine();
Assert.assertEquals(expectedLineNumber, actualLineNumber);
final String actualCode = suspendedEvent.getSourceSection().getCode();
Assert.assertEquals(expectedCode, actualCode);
final boolean actualIsBefore = suspendedEvent.isHaltedBefore();
Assert.assertEquals(expectedIsBefore, actualIsBefore);
checkStack(suspendedEvent.getTopStackFrame(), name, expectedFrame);
return suspendedEvent;
}
protected void checkStack(DebugStackFrame frame, String name, String... expectedFrame) {
assertEquals(name, frame.getName());
checkDebugValues("variables", frame, expectedFrame);
}
protected void checkArgs(DebugStackFrame frame, String... expectedArgs) {
Iterable<DebugValue> arguments = null;
DebugScope scope = frame.getScope();
while (scope != null) {
if (scope.isFunctionScope()) {
arguments = scope.getArguments();
break;
}
scope = scope.getParent();
}
checkDebugValues("arguments", arguments, expectedArgs);
}
private static void checkDebugValues(String msg, Iterable<DebugValue> values, String... expected) {
Map<String, DebugValue> valMap = new HashMap<>();
for (DebugValue value : values) {
valMap.put(value.getName(), value);
}
String message = String.format("Frame %s expected %s got %s", msg, Arrays.toString(expected), valMap.toString());
Assert.assertEquals(message, expected.length / 2, valMap.size());
for (int i = 0; i < expected.length; i = i + 2) {
String expectedIdentifier = expected[i];
String expectedValue = expected[i + 1];
DebugValue value = valMap.get(expectedIdentifier);
Assert.assertNotNull(value);
Assert.assertEquals(expectedValue, value.as(String.class));
}
}
@Test
public void testBreakpoint() throws Throwable {
/*
* Wrappers need to remain inserted for recursive functions to work for debugging. Like in
* this test case when the breakpoint is in the exit condition and we want to step out.
*/
final Source factorial = slCode("function main() {\n" +
" return fac(5);\n" +
"}\n" +
"function fac(n) {\n" +
" if (n <= 1) {\n" +
" return 1;\n" + // break
" }\n" +
" return n * fac(n - 1);\n" +
"}\n");
try (DebuggerSession session = startSession()) {
startEval(factorial);
Breakpoint breakpoint = session.install(Breakpoint.newBuilder(factorial).lineIs(6).build());
expectSuspended((SuspendedEvent event) -> {
checkState(event, "fac", 6, true, "return 1", "n", "1");
checkArgs(event.getTopStackFrame(), "n", "1");
Iterator<DebugStackFrame> sfi = event.getStackFrames().iterator();
for (int i = 1; i <= 5; i++) {
checkArgs(sfi.next(), "n", Integer.toString(i));
}
checkArgs(sfi.next()); // main
assertSame(breakpoint, event.getBreakpoints().iterator().next());
event.prepareStepOver(1);
});
expectSuspended((SuspendedEvent event) -> {
checkState(event, "fac", 8, false, "fac(n - 1)", "n", "2");
checkArgs(event.getTopStackFrame(), "n", "2");
assertEquals("1", event.getReturnValue().as(String.class));
assertTrue(event.getBreakpoints().isEmpty());
event.prepareStepOut(1);
});
expectSuspended((SuspendedEvent event) -> {
checkState(event, "fac", 8, false, "fac(n - 1)", "n", "3");
assertEquals("2", event.getReturnValue().as(String.class));
assertTrue(event.getBreakpoints().isEmpty());
event.prepareStepOut(1);
});
expectSuspended((SuspendedEvent event) -> {
checkState(event, "fac", 8, false, "fac(n - 1)", "n", "4");
assertEquals("6", event.getReturnValue().as(String.class));
assertTrue(event.getBreakpoints().isEmpty());
event.prepareStepOut(1);
});
expectSuspended((SuspendedEvent event) -> {
checkState(event, "fac", 8, false, "fac(n - 1)", "n", "5");
assertEquals("24", event.getReturnValue().as(String.class));
assertTrue(event.getBreakpoints().isEmpty());
event.prepareStepOut(1);
});
expectSuspended((SuspendedEvent event) -> {
checkState(event, "main", 2, false, "fac(5)");
checkArgs(event.getTopStackFrame());
assertEquals("120", event.getReturnValue().as(String.class));
assertTrue(event.getBreakpoints().isEmpty());
event.prepareStepOut(1);
});
assertEquals("120", expectDone());
}
}
@Test
public void testStepInOver() throws Throwable {
/*
* For recursive function we want to ensure that we don't step when we step over a function.
*/
final Source factorial = slCode("function main() {\n" +
" return fac(5);\n" +
"}\n" +
"function fac(n) {\n" +
" if (n <= 1) {\n" +
" return 1;\n" + // break
" }\n" +
" return n * fac(n - 1);\n" +
"}\n");
try (DebuggerSession session = startSession()) {
session.suspendNextExecution();
startEval(factorial);
expectSuspended((SuspendedEvent event) -> {
checkState(event, "main", 2, true, "return fac(5)").prepareStepInto(1);
});
expectSuspended((SuspendedEvent event) -> {
checkState(event, "fac", 5, true, "n <= 1", "n", "5").prepareStepOver(1);
});
expectSuspended((SuspendedEvent event) -> {
checkState(event, "fac", 8, true, "return n * fac(n - 1)", "n", "5").prepareStepOver(1);
});
expectSuspended((SuspendedEvent event) -> {
checkState(event, "main", 2, false, "fac(5)").prepareStepInto(1);
assertEquals("120", event.getReturnValue().as(String.class));
});
expectDone();
}
}
@Test
public void testDebugger() throws Throwable {
/*
* Test AlwaysHalt is working.
*/
final Source factorial = slCode("function main() {\n" +
" return fac(5);\n" +
"}\n" +
"function fac(n) {\n" +
" if (n <= 1) {\n" +
" debugger; return 1;\n" + // // break
" }\n" +
" return n * fac(n - 1);\n" +
"}\n");
try (DebuggerSession session = startSession()) {
startEval(factorial);
// make javac happy and use the session
session.getBreakpoints();
expectSuspended((SuspendedEvent event) -> {
checkState(event, "fac", 6, true, "debugger", "n", "1").prepareContinue();
});
expectDone();
}
}
@Test(expected = ThreadDeath.class)
public void testTimeboxing() throws Throwable {
final Source endlessLoop = slCode("function main() {\n" +
" i = 1; \n" +
" while(i > 0) {\n" +
" i = i + 1;\n" +
" }\n" +
" return i; \n" +
"}\n");
final PolyglotEngine engine = PolyglotEngine.newBuilder().build();
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
Debugger.find(engine).startSession(new SuspendedCallback() {
public void onSuspend(SuspendedEvent event) {
event.prepareKill();
}
}).suspendNextExecution();
}
}, 1000);
engine.eval(endlessLoop);
}
@Test
public void testNull() throws Throwable {
final Source factorial = slCode("function main() {\n" +
" res = doNull();\n" +
" return res;\n" +
"}\n" +
"function doNull() {}\n");
try (DebuggerSession session = startSession()) {
session.suspendNextExecution();
startEval(factorial);
expectSuspended((SuspendedEvent event) -> {
checkState(event, "main", 2, true, "res = doNull()").prepareStepInto(1);
});
expectSuspended((SuspendedEvent event) -> {
checkState(event, "main", 3, true, "return res", "res", "NULL").prepareContinue();
});
assertEquals("NULL", expectDone());
}
}
@Test
public void testDebugValue() throws Throwable {
final Source varsSource = slCode("function main() {\n" +
" a = doNull();\n" +
" b = 10 == 10;\n" +
" c = 10;\n" +
" d = \"str\";\n" +
" e = new();\n" +
" e.p1 = 1;\n" +
" e.p2 = new();\n" +
" e.p2.p21 = 21;\n" +
" return;\n" +
"}\n" +
"function doNull() {}\n");
try (DebuggerSession session = startSession()) {
session.install(Breakpoint.newBuilder(varsSource).lineIs(10).build());
startEval(varsSource);
expectSuspended((SuspendedEvent event) -> {
DebugStackFrame frame = event.getTopStackFrame();
DebugScope scope = frame.getScope();
DebugValue a = scope.getDeclaredValue("a");
assertFalse(a.isArray());
assertNull(a.getArray());
assertNull(a.getProperties());
DebugValue b = scope.getDeclaredValue("b");
assertFalse(b.isArray());
assertNull(b.getArray());
assertNull(b.getProperties());
DebugValue c = scope.getDeclaredValue("c");
assertFalse(c.isArray());
assertEquals("10", c.as(String.class));
assertNull(c.getArray());
assertNull(c.getProperties());
DebugValue d = scope.getDeclaredValue("d");
assertFalse(d.isArray());
assertEquals("str", d.as(String.class));
assertNull(d.getArray());
assertNull(d.getProperties());
DebugValue e = scope.getDeclaredValue("e");
assertFalse(e.isArray());
assertNull(e.getArray());
assertEquals(scope, e.getScope());
Collection<DebugValue> propertyValues = e.getProperties();
assertEquals(2, propertyValues.size());
Iterator<DebugValue> propertiesIt = propertyValues.iterator();
assertTrue(propertiesIt.hasNext());
DebugValue p1 = propertiesIt.next();
assertEquals("p1", p1.getName());
assertEquals("1", p1.as(String.class));
assertNull(p1.getScope());
assertTrue(propertiesIt.hasNext());
DebugValue p2 = propertiesIt.next();
assertEquals("p2", p2.getName());
assertNull(p2.getScope());
assertFalse(propertiesIt.hasNext());
propertyValues = p2.getProperties();
assertEquals(1, propertyValues.size());
propertiesIt = propertyValues.iterator();
assertTrue(propertiesIt.hasNext());
DebugValue p21 = propertiesIt.next();
assertEquals("p21", p21.getName());
assertEquals("21", p21.as(String.class));
assertNull(p21.getScope());
assertFalse(propertiesIt.hasNext());
});
expectDone();
}
}
@Test
public void testValuesScope() throws Throwable {
final Source varsSource = slCode("function main() {\n" +
" a = 1;\n" +
" if (a > 0) {\n" +
" b = 10;\n" +
" println(b);\n" +
" }\n" +
" println(b);\n" +
" println(a);\n" +
" println(\"END.\");\n" +
"}");
try (DebuggerSession session = startSession()) {
session.suspendNextExecution();
startEval(varsSource);
expectSuspended((SuspendedEvent event) -> {
DebugStackFrame frame = event.getTopStackFrame();
// No variables first:
assertFalse(frame.getScope().getDeclaredValues().iterator().hasNext());
event.prepareStepOver(1);
});
expectSuspended((SuspendedEvent event) -> {
DebugStackFrame frame = event.getTopStackFrame();
// "a" only:
DebugScope scope = frame.getScope();
Iterator<DebugValue> varIt = scope.getDeclaredValues().iterator();
assertTrue(varIt.hasNext());
DebugValue a = varIt.next();
assertEquals("a", a.getName());
assertEquals(scope, a.getScope());
assertFalse(varIt.hasNext());
event.prepareStepOver(1);
});
expectSuspended((SuspendedEvent event) -> {
DebugStackFrame frame = event.getTopStackFrame();
// "a" only:
DebugScope scope = frame.getScope();
Iterator<DebugValue> varIt = scope.getParent().getDeclaredValues().iterator();
assertTrue(varIt.hasNext());
DebugValue a = varIt.next();
assertEquals("a", a.getName());
assertEquals(scope.getParent(), a.getScope());
assertFalse(varIt.hasNext());
event.prepareStepOver(1);
});
expectSuspended((SuspendedEvent event) -> {
DebugStackFrame frame = event.getTopStackFrame();
// "a" and "b":
DebugScope scope = frame.getScope();
Iterator<DebugValue> varIt = scope.getDeclaredValues().iterator();
assertTrue(varIt.hasNext());
DebugValue b = varIt.next();
assertEquals("b", b.getName());
assertEquals(scope, b.getScope());
// "a" is in the parent:
assertFalse(varIt.hasNext());
varIt = scope.getParent().getDeclaredValues().iterator();
assertTrue(varIt.hasNext());
DebugValue a = varIt.next();
assertEquals("a", a.getName());
assertEquals(scope.getParent(), a.getScope());
assertFalse(varIt.hasNext());
event.prepareStepOver(1);
});
expectSuspended((SuspendedEvent event) -> {
DebugStackFrame frame = event.getTopStackFrame();
// "a" only again:
DebugScope scope = frame.getScope();
Iterator<DebugValue> varIt = scope.getDeclaredValues().iterator();
assertTrue(varIt.hasNext());
DebugValue a = varIt.next();
assertEquals("a", a.getName());
assertEquals(scope, a.getScope());
assertFalse(varIt.hasNext());
event.prepareContinue();
});
expectDone();
}
}
@Test
public void testMetaObjects() {
final Source varsSource = slCode("function main() {\n" +
" a = doNull();\n" +
" b = 10 == 10;\n" +
" c = 10;\n" +
" cBig = 1000000000*1000000000*1000000000*1000000000;\n" +
" d = \"str\";\n" +
" e = new();\n" +
" f = doNull;\n" +
" return;\n" +
"}\n" +
"function doNull() {}\n");
try (DebuggerSession session = startSession()) {
session.install(Breakpoint.newBuilder(varsSource).lineIs(9).build());
startEval(varsSource);
expectSuspended((SuspendedEvent event) -> {
DebugStackFrame frame = event.getTopStackFrame();
DebugScope scope = frame.getScope();
DebugValue v = scope.getDeclaredValue("a");
assertEquals("Null", v.getMetaObject().as(String.class));
v = scope.getDeclaredValue("b");
assertEquals("Boolean", v.getMetaObject().as(String.class));
v = scope.getDeclaredValue("c");
assertEquals("Number", v.getMetaObject().as(String.class));
v = scope.getDeclaredValue("cBig");
assertEquals("Number", v.getMetaObject().as(String.class));
v = scope.getDeclaredValue("d");
assertEquals("String", v.getMetaObject().as(String.class));
v = scope.getDeclaredValue("e");
assertEquals("Object", v.getMetaObject().as(String.class));
v = scope.getDeclaredValue("f");
assertEquals("Function", v.getMetaObject().as(String.class));
});
expectDone();
}
}
@Test
public void testSourceLocation() {
final Source varsSource = slCode("function main() {\n" +
" a = doNull();\n" +
" c = 10;\n" +
" d = \"str\";\n" +
" e = new();\n" +
" f = doNull;\n" +
" return;\n" +
"}\n" +
"function doNull() {}\n");
try (DebuggerSession session = startSession()) {
session.install(Breakpoint.newBuilder(varsSource).lineIs(7).build());
startEval(varsSource);
expectSuspended((SuspendedEvent event) -> {
DebugStackFrame frame = event.getTopStackFrame();
DebugScope scope = frame.getScope();
DebugValue v = scope.getDeclaredValue("a");
assertNull(v.getSourceLocation());
v = scope.getDeclaredValue("c");
assertNull(v.getSourceLocation());
v = scope.getDeclaredValue("d");
assertNull(v.getSourceLocation());
v = scope.getDeclaredValue("e");
assertNull(v.getSourceLocation());
v = scope.getDeclaredValue("f");
SourceSection sourceLocation = v.getSourceLocation();
Assert.assertNotNull(sourceLocation);
assertEquals(9, sourceLocation.getStartLine());
assertEquals(9, sourceLocation.getEndLine());
assertEquals("doNull() {}", sourceLocation.getCode());
});
expectDone();
}
}
@Test
public void testStack() {
final Source stackSource = slCode("function main() {\n" +
" return fac(10);\n" +
"}\n" +
"function fac(n) {\n" +
" if (n <= 1) {\n" +
" return 1;\n" + // break
" }\n" +
" return n * fac(n - 1);\n" +
"}\n");
try (DebuggerSession session = startSession()) {
session.install(Breakpoint.newBuilder(stackSource).lineIs(6).build());
startEval(stackSource);
expectSuspended((SuspendedEvent event) -> {
Iterator<DebugStackFrame> sfIt = event.getStackFrames().iterator();
assertTrue(sfIt.hasNext());
DebugStackFrame dsf = sfIt.next();
assertEquals("fac", dsf.getName());
assertEquals(6, dsf.getSourceSection().getStartLine());
assertFalse(dsf.isInternal());
int numStacksAt8 = 10 - 1;
for (int i = 0; i < numStacksAt8; i++) {
assertTrue(sfIt.hasNext());
dsf = sfIt.next();
assertEquals("fac", dsf.getName());
assertEquals(8, dsf.getSourceSection().getStartLine());
assertFalse(dsf.isInternal());
}
assertTrue(sfIt.hasNext());
dsf = sfIt.next();
assertEquals("main", dsf.getName());
assertEquals(2, dsf.getSourceSection().getStartLine());
assertFalse(dsf.isInternal());
assertFalse(sfIt.hasNext());
});
expectDone();
}
}
@Test
public void testStackInterop() {
final Source stackSource = slCode("function fac(n, multiply) {\n" +
" if (n <= 1) {\n" +
" debugger;\n" +
" return 1;\n" +
" }\n" +
" return multiply.multiply(n, fac, n - 1);\n" +
"}\n");
PolyglotEngine engine = PolyglotEngine.newBuilder().setOut(System.out).setErr(System.err).build();
engine.eval(stackSource);
PolyglotEngine.Value fac = engine.findGlobalSymbol("fac");
Object multiply = new Multiply();
Debugger debugger = Debugger.find(engine);
boolean[] done = new boolean[1];
try (DebuggerSession session = debugger.startSession((event) -> {
Iterator<DebugStackFrame> sfIt = event.getStackFrames().iterator();
assertTrue(sfIt.hasNext());
DebugStackFrame dsf = sfIt.next();
assertEquals("fac", dsf.getName());
assertEquals(3, dsf.getSourceSection().getStartLine());
assertFalse(dsf.isInternal());
int numStacksAt6 = 10 - 1;
int numInteropStacks = 0;
for (int i = 0; i < numStacksAt6;) {
assertTrue(sfIt.hasNext());
dsf = sfIt.next();
boolean inFac = dsf.getName() != null;
if (inFac) {
// Frame in fac function
assertEquals("fac", dsf.getName());
assertEquals(6, dsf.getSourceSection().getStartLine());
assertFalse(dsf.isInternal());
i++;
} else {
// Frame in an interop method, internal
assertEquals(null, dsf.getName());
assertNull(dsf.getSourceSection());
assertTrue(dsf.isInternal());
numInteropStacks++;
}
}
// There were at least as many interop internal frames as frames in fac function:
assertTrue("numInteropStacks = " + numInteropStacks, numInteropStacks >= numStacksAt6);
// Some more internal frames remain
while (sfIt.hasNext()) {
dsf = sfIt.next();
assertEquals(null, dsf.getName());
assertNull(dsf.getSourceSection());
assertTrue(dsf.isInternal());
}
done[0] = true;
})) {
Assert.assertNotNull(session);
PolyglotEngine.Value ret = fac.execute(new Object[]{10, multiply});
assertNumber(ret.get(), 3628800L);
}
assertTrue(done[0]);
}
private static void assertNumber(Object real, double expected) {
if (real instanceof Number) {
assertEquals(expected, ((Number) real).doubleValue(), 0.1);
} else {
fail("Expecting a number " + real);
}
}
public static class Multiply {
public long multiply(long n, Fac fce, long i) {
return n * fce.fac(i, this);
}
}
public interface Fac {
long fac(long n, Multiply multiply);
}
}