Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
com.oracle.truffle.tck.DebuggerTester Maven / Gradle / Ivy
Go to download
A collection of tests that can certify language implementation to be compliant
with most recent requirements of the Truffle infrastructure and tooling.
/*
* Copyright (c) 2017, 2024, 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.tck;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.URI;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.graalvm.collections.Pair;
import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.Engine;
import org.graalvm.polyglot.PolyglotException;
import org.graalvm.polyglot.Source;
import org.graalvm.polyglot.Value;
import org.junit.Assert;
import com.oracle.truffle.api.debug.Breakpoint;
import com.oracle.truffle.api.debug.Debugger;
import com.oracle.truffle.api.debug.DebuggerSession;
import com.oracle.truffle.api.debug.SourceElement;
import com.oracle.truffle.api.debug.SuspendedCallback;
import com.oracle.truffle.api.debug.SuspendedEvent;
import com.oracle.truffle.api.source.SourceSection;
/**
* Test utility class that makes it easier to test and debug debugger functionality for guest
* languages. Testing suspended callbacks can be cumbersome when having to assert multiple
* sequential events. The debugger tester allows to test suspended callbacks using sequential code
* that allows to assert multiple {@link SuspendedEvent events}. It does so by running the engine on
* a separate thread and it uses internal APIs to allow access to the {@link SuspendedEvent} from
* another Thread. Do not use this class for anything else than testing.
*
* The debugger tester can print debug traces to standard output with -Dtruffle.debug.trace=true.
*
* Example usage:
*
* {@snippet file="com/oracle/truffle/tck/DebuggerTester.java"
* region="DebuggerTesterSnippets#testDebugging"}
*
* @since 0.16
*/
public final class DebuggerTester implements AutoCloseable {
static final boolean TRACE = Boolean.getBoolean("truffle.debug.trace");
private final BlockingQueue newEvent;
private final Semaphore executing;
private final Semaphore initialized;
private final Thread evalThread;
private final Engine engine;
private final ByteArrayOutputStream out = new ByteArrayOutputStream();
private final ByteArrayOutputStream err = new ByteArrayOutputStream();
private volatile boolean closed;
private volatile ExecutingSource executingSource;
private static void trace(String message) {
if (TRACE) {
PrintStream out = System.out;
out.println("DebuggerTester: " + message);
}
}
private static void err(String message) {
PrintStream out = System.err;
out.println("DebuggerTester: " + message);
}
private final ExecutingLoop executingLoop;
private SuspendedCallback handler;
/**
* Constructs a new debugger tester instance. Boots up a new {@link Context context} on Thread
* in the background. The tester instance needs to be {@link #close() closed} after use. Throws
* an AssertionError if the engine initialization fails.
*
* @since 0.16
*/
public DebuggerTester() {
this(null);
}
/**
* Constructs a new debugger tester instance with a pre-set context builder.
*
* @param contextBuilder a pre-set context builder. Only out and err streams are set on this
* builder prior the {@link Context} instance creation.
*
* @see #DebuggerTester()
* @since 0.31
*/
public DebuggerTester(Context.Builder contextBuilder) {
this.newEvent = new ArrayBlockingQueue<>(1);
this.executing = new Semaphore(0);
this.initialized = new Semaphore(0);
final AtomicReference engineRef = new AtomicReference<>();
final AtomicReference error = new AtomicReference<>();
this.executingLoop = new ExecutingLoop(contextBuilder, engineRef, error);
this.evalThread = new Thread(executingLoop);
this.evalThread.start();
try {
initialized.acquire();
} catch (InterruptedException e) {
throw new AssertionError(e);
}
this.engine = engineRef.get();
if (error.get() != null) {
throw new AssertionError("Engine initialization failed", error.get());
}
}
/**
* Returns the error output of the underlying {@link Context context}.
*
* @since 0.16
*/
public String getErr() {
try {
err.flush();
} catch (IOException e) {
throw new RuntimeException(e);
}
return new String(err.toByteArray());
}
/**
* Returns the standard output of the underlying {@link Context context}.
*
* @since 0.16
*/
public String getOut() {
try {
out.flush();
} catch (IOException e) {
throw new RuntimeException(e);
}
return new String(out.toByteArray());
}
/**
* Get the Debugger instance associated with the current engine.
*
* @since 0.27
*/
public Debugger getDebugger() {
return engine.getInstruments().get("debugger").lookup(Debugger.class);
}
/**
* Starts a new {@link Debugger#startSession(SuspendedCallback) debugger session} in the
* context's {@link Engine engine}. The debugger session allows to suspend the execution and to
* install breakpoints. If multiple sessions are created for one {@link #startEval(Source)
* evaluation} then all suspended events are delegated to this debugger tester instance.
*
* @return a new debugger session
* @since 0.16
*/
public DebuggerSession startSession() {
return getDebugger().startSession(new SuspendedCallback() {
public void onSuspend(SuspendedEvent event) {
DebuggerTester.this.onSuspend(event);
}
});
}
/**
* Starts a new {@link Debugger#startSession(SuspendedCallback, SourceElement...) debugger
* session} in the context's {@link Engine engine}. The debugger session allows to suspend the
* execution on the provided source elements and to install breakpoints. If multiple sessions
* are created for one {@link #startEval(Source) evaluation} then all suspended events are
* delegated to this debugger tester instance.
*
* @param sourceElements a list of source elements
* @return a new debugger session
* @since 0.33
*/
public DebuggerSession startSession(SourceElement... sourceElements) {
return getDebugger().startSession(new SuspendedCallback() {
public void onSuspend(SuspendedEvent event) {
DebuggerTester.this.onSuspend(event);
}
}, sourceElements);
}
/**
* Starts a new {@link Context#eval(Source) evaluation} on the background thread. Only one
* evaluation can be active at a time. Please ensure that {@link #expectDone()} completed
* successfully before starting a new evaluation. When no source is available please refer to
* {@link DebuggerTester#startExecute(Function)}. Throws an {@link IllegalStateException} if
* another evaluation is still executing or the tester is already closed.
*
* @since 0.27
*/
public void startEval(Source s) {
startExecute(new Function() {
public Value apply(Context c) {
return c.eval(s);
}
});
}
/**
* Starts a new script evaluation on the background thread. Only one evaluation can be active at
* a time. Please ensure that {@link #expectDone()} completed successfully before starting a new
* evaluation. If a Source is available please refer to {@link DebuggerTester#startEval(Source)}
* . Throws an {@link IllegalStateException} if another evaluation is still executing or the
* tester is already closed.
*
* @since 20.0
*/
public void startExecute(Function script) {
if (this.executingSource != null) {
throw new IllegalStateException("Already executing other source ");
}
this.executingSource = new ExecutingSource(script);
}
/**
* Expects a suspended event and returns it for potential assertions. If the execution completed
* or was killed instead then an assertion error is thrown. The returned suspended event is only
* valid until on of {@link #expectKilled()}, {@link #expectSuspended(SuspendedCallback)} or
* {@link #expectDone()} is called again. Throws an {@link IllegalStateException} if the tester
* is already closed.
*
* @param callback handler to be called when the execution is suspended
* @since 0.16
*/
public void expectSuspended(SuspendedCallback callback) {
expectSuspended(callback, null);
}
/**
* Expects a suspended event and returns it for potential assertions, allows to verify errors
* printed to the error output.
*
* @param callback handler to be called when the execution is suspended
* @param errorVerifier handler, which returns true
when the error is expected
* @since 24.1
*/
public void expectSuspended(SuspendedCallback callback, Function errorVerifier) {
if (closed) {
throw new IllegalStateException("Already closed.");
}
SuspendedCallback previous = this.handler;
this.handler = callback;
notifyNextAction();
Object event;
try {
event = takeEvent();
String e = getErr();
if (!e.isEmpty()) {
if (errorVerifier != null) {
Assert.assertTrue(e, errorVerifier.apply(e));
err.reset();
} else {
throw new AssertionError("Error output is not empty: " + e);
}
}
} catch (InterruptedException e) {
throw new AssertionError(e);
}
if (event instanceof ExecutingSource) {
ExecutingSource s = (ExecutingSource) event;
if (s.error != null) {
throw new AssertionError("Error in eval", s.error);
}
throw new AssertionError("Expected suspended event got return value " + s.returnValue);
} else if (event instanceof SuspendedEvent) {
this.handler = previous;
} else {
if (event instanceof Error) {
throw (Error) event;
}
if (event instanceof RuntimeException) {
throw (RuntimeException) event;
}
throw new AssertionError("Got unknown event.", (event instanceof Throwable ? (Throwable) event : null));
}
}
/**
* Expects the current evaluation to be completed with an error and not be killed or to produce
* further suspended events. It returns a string representation of the result value to be
* asserted. If the evaluation caused any errors they are thrown as {@link AssertionError}.
* Throws an {@link IllegalStateException} if the tester is already closed.
*
* @since 0.16
*/
public Throwable expectThrowable() {
return (Throwable) expectDoneImpl(true);
}
/**
* Expects the current evaluation to be completed successfully and not be killed or to produce
* further suspended events. It returns a string representation of the result value to be
* asserted. If the evaluation caused any errors they are thrown as {@link AssertionError}.
* Throws an {@link IllegalStateException} if the tester is already closed.
*
* @since 0.16
*/
public String expectDone() {
return (String) expectDoneImpl(false);
}
private Object expectDoneImpl(boolean expectError) throws AssertionError {
if (closed) {
throw new IllegalStateException("Already closed.");
}
try {
notifyNextAction();
Object event;
try {
event = takeEvent(); // waits for next event.
String e = getErr();
if (!e.isEmpty()) {
throw new AssertionError("Error output is not empty: " + e);
}
} catch (InterruptedException e) {
throw new AssertionError(e);
}
if (event instanceof ExecutingSource) {
ExecutingSource s = (ExecutingSource) event;
if (expectError) {
if (s.error == null) {
throw new AssertionError("Error expected exception bug got return value: " + s.returnValue);
}
return s.error;
} else {
if (s.error != null) {
throw new AssertionError("Error in eval", s.error);
}
return s.returnValue;
}
} else if (event instanceof SuspendedEvent) {
throw new AssertionError("Expected done but got " + event);
} else if (event instanceof Throwable) {
throw new AssertionError("Got exception", (Throwable) event);
} else {
throw new AssertionError("Got unknown: " + event);
}
} finally {
executingSource = null;
}
}
/**
* Expects the current evaluation to be killed and not be completed or to produce further
* suspended events. Throws an {@link IllegalStateException} if the tester is already closed. If
* the evaluation caused any errors besides the kill exception then they are thrown as
* {@link AssertionError}.
*
* @since 0.16
*/
public void expectKilled() {
Throwable error = expectThrowable();
if (error instanceof PolyglotException) {
Assert.assertTrue(((PolyglotException) error).isCancelled());
Assert.assertTrue(error.getMessage(), error.getMessage().contains("Execution cancelled by a debugging session."));
return;
}
throw new AssertionError("Expected killed bug got error: " + error, error);
}
/**
* Returns the thread that the execution started with {@link #startEval(Source)} is running on.
*
* @return the thread instance
* @since 0.16
*/
public Thread getEvalThread() {
return evalThread;
}
/**
* Close the engine. It's not possible to evaluate code after engine is closed, use it when the
* engine needs to be closed before the debugger session.
*
* @since 0.30
*/
public void closeEngine() {
engine.close();
}
/**
* Closes the current debugger tester session and all its associated resources like the
* background thread. The debugger tester becomes unusable after closing.
*
* @since 0.16
*/
public void close() {
if (closed) {
throw new IllegalStateException("Already closed.");
}
closed = true;
trace("kill session " + this);
// trying to interrupt if execution is in IO.
notifyNextAction();
try {
evalThread.join();
} catch (InterruptedException iex) {
throw new AssertionError("Interrupted while joining eval thread.", iex);
}
engine.close();
}
/**
* Utility method that checks proper resolution of line breakpoints. Breakpoints are submitted
* to every line of the source and their resolution location is checked.
*
* The source need to contain resolution marks in the form of
* "R<line number>_
" where <line number> is line number of the original
* breakpoint position and R
is the resolved mark name. These marks need to be
* placed at the proper breakpoint resolution line/column position, for a breakpoint submitted
* to every line. When several submitted breakpoints resolve to the same position, an interval
* can be specified in the form of
* "R<line start number>-<line end number>_
", where both start and end
* line numbers are inclusive. The marks are stripped off before execution.
*
* The guest language code with marks may look like:
*
*
* // test
* function test(n) {
* if (R1-3_n <= 1) {
* return R4_2 * n;
* }
* return R5-7_n - 1;
* }
*
*
* @param sourceWithMarks a source text, which contains the resolution marks
* @param resolvedMarkName the mark name. It is used in a regular expression, therefore the mark
* must not have a special meaning in a regular expression.
* @param language the source language
* @since 0.33
*/
public void assertLineBreakpointsResolution(String sourceWithMarks, String resolvedMarkName, String language) {
assertLineBreakpointsResolution(sourceWithMarks, null, resolvedMarkName, language);
}
/**
* @param positionPredicate null
to test line breakpoints on all lines, or a
* predicate that limits the testable lines.
* @see #assertLineBreakpointsResolution(java.lang.String, java.lang.String, java.lang.String)
* @since 19.3.0
*/
public void assertLineBreakpointsResolution(String sourceWithMarks, PositionPredicate positionPredicate, String resolvedMarkName, String language) {
Pattern br = Pattern.compile("(" + resolvedMarkName + "\\d+_|" + resolvedMarkName + "\\d+-\\d+_)");
Map bps = new HashMap<>();
String sourceString = sourceWithMarks;
Matcher bm = br.matcher(sourceString);
while (bm.find()) {
String bg = bm.group();
int index = bm.start();
int bn1;
int bn2;
String bpNums = bg.substring(1, bg.length() - 1);
int rangeIndex = bpNums.indexOf('-');
if (rangeIndex > 0) {
bn1 = Integer.parseInt(bpNums.substring(0, rangeIndex));
bn2 = Integer.parseInt(bpNums.substring(rangeIndex + 1));
} else {
bn1 = bn2 = Integer.parseInt(bpNums);
}
for (int bn = bn1; bn <= bn2; bn++) {
Integer bp = bps.get(bn);
if (bp == null) {
bps.put(bn, index + 1);
} else {
Assert.fail(bg + " specified more than once.");
}
}
sourceString = bm.replaceFirst("");
bm.reset(sourceString);
}
if (TRACE) {
trace("sourceString = '" + sourceString + "'");
}
final Source source = Source.newBuilder(language, sourceString, "testMisplacedLineBreakpoint." + language).buildLiteral();
com.oracle.truffle.api.source.Source tsource = DebuggerTester.getSourceImpl(source);
for (int l = 1; l < source.getLineCount(); l++) {
if ((positionPredicate == null || positionPredicate.testLine(l)) && !bps.containsKey(l)) {
Assert.fail("Line " + l + " is missing.");
}
}
for (Map.Entry bentry : bps.entrySet()) {
int line = bentry.getKey();
int indexResolved = bentry.getValue();
int lineResolved = source.getLineNumber(indexResolved - 1);
if (TRACE) {
trace("TESTING breakpoint '" + line + "' => " + lineResolved + ":");
}
try (DebuggerSession session = startSession()) {
startEval(source);
int[] resolvedIndexPtr = new int[]{0};
Breakpoint breakpoint = session.install(Breakpoint.newBuilder(tsource).lineIs(line).oneShot().resolveListener(new Breakpoint.ResolveListener() {
@Override
public void breakpointResolved(Breakpoint brkp, SourceSection section) {
resolvedIndexPtr[0] = section.getCharIndex() + 1;
if (TRACE) {
trace("BREAKPOINT resolved to " + section.getStartLine() + ":" + section.getStartColumn());
}
}
}).build());
expectSuspended((SuspendedEvent event) -> {
Assert.assertEquals("Expected " + line + " => " + lineResolved,
lineResolved, event.getSourceSection().getStartLine());
Assert.assertSame(breakpoint, event.getBreakpoints().iterator().next());
event.prepareContinue();
});
expectDone();
Assert.assertEquals("Expected resolved " + line + " => " + indexResolved,
indexResolved, resolvedIndexPtr[0]);
}
}
}
/**
* Utility method that checks proper resolution of column breakpoints. Breakpoints are submitted
* to marked positions and their resolution location is checked.
*
* The source need to contain both breakpoint submission marks in the form of "B<number>_"
* and breakpoint resolution marks in the form of "R<number>_" where <number> is an
* identification of the breakpoint. These marks need to be placed at the proper breakpoint
* submission/resolution line/column position. When several submitted breakpoints resolve to the
* same position, an interval can be specified in the form of "R<start number>-<end
* number>_", where both start and end line numbers are inclusive. The marks are stripped off
* before execution.
*
* The guest language code with marks may look like:
*
*
* // B1_test
* function B2_test(n) {B3_
* if (R1-4_n <= B4_1) {B5_
* return R5_2 * n;
* }
* return R6_n - 1;
* B6_}
*
*
* @param sourceWithMarks a source text, which contains the resolution marks
* @param breakpointMarkName the breakpoint submission mark name. It is used in a regular
* expression, therefore the mark must not have a special meaning in a regular
* expression.
* @param resolvedMarkName the resolved mark name. It is used in a regular expression, therefore
* the mark must not have a special meaning in a regular expression.
* @param language the source language
* @since 0.33
*/
public void assertColumnBreakpointsResolution(String sourceWithMarks, String breakpointMarkName, String resolvedMarkName, String language) {
assertColumnBreakpointsResolution(sourceWithMarks, breakpointMarkName, resolvedMarkName, language, null);
}
public void assertColumnBreakpointsResolution(String sourceWithMarks, String breakpointMarkName, String resolvedMarkName, String language, URI bpURI) {
Pattern br = Pattern.compile("([" + breakpointMarkName + resolvedMarkName + "]\\d+_|" + resolvedMarkName + "\\d+-\\d+_)");
Map bps = new HashMap<>();
String sourceString = sourceWithMarks;
Matcher bm = br.matcher(sourceString);
while (bm.find()) {
String bg = bm.group();
int index = bm.start();
int state = (bg.charAt(0) == 'B') ? 0 : 1;
String bpNums = bg.substring(1, bg.length() - 1);
int bn1;
int bn2;
int rangeIndex = bpNums.indexOf('-');
if (rangeIndex > 0) {
bn1 = Integer.parseInt(bpNums.substring(0, rangeIndex));
bn2 = Integer.parseInt(bpNums.substring(rangeIndex + 1));
} else {
bn1 = bn2 = Integer.parseInt(bpNums);
}
for (int bn = bn1; bn <= bn2; bn++) {
int[] bp = bps.get(bn);
if (bp == null) {
bp = new int[2];
bps.put(bn, bp);
}
if (bp[state] > 0) {
Assert.fail(bg + " specified more than once.");
}
bp[state] = index + 1;
}
sourceString = bm.replaceFirst("");
bm.reset(sourceString);
}
if (TRACE) {
trace("sourceString = '" + sourceString + "'");
}
final Source source = Source.newBuilder(language, sourceString, "testMisplacedColumnBreakpoint." + language).buildLiteral();
for (Map.Entry bentry : bps.entrySet()) {
int bpId = bentry.getKey();
int[] bp = bentry.getValue();
Assert.assertTrue(Integer.toString(bpId), bp[0] > 0);
Assert.assertTrue(Integer.toString(bpId), bp[1] > 0);
int line = source.getLineNumber(bp[0] - 1);
int column = source.getColumnNumber(bp[0] - 1);
if (TRACE) {
trace("TESTING BP_" + bpId + ": " + bp[0] + " (" + line + ":" + column + ") => " + bp[1] + ":");
}
try (DebuggerSession session = startSession()) {
startEval(source);
int[] resolvedLineColumn = new int[]{0, 0};
Breakpoint.Builder bpBuilder = (bpURI != null) ? Breakpoint.newBuilder(bpURI) : Breakpoint.newBuilder(DebuggerTester.getSourceImpl(source));
Breakpoint breakpoint = session.install(
bpBuilder.lineIs(line).columnIs(column).oneShot().resolveListener(new Breakpoint.ResolveListener() {
@Override
public void breakpointResolved(Breakpoint brkp, SourceSection section) {
resolvedLineColumn[0] = section.getStartLine();
resolvedLineColumn[1] = section.getStartColumn();
if (TRACE) {
trace(" resolved: " + resolvedLineColumn[0] + ":" + resolvedLineColumn[1]);
}
}
}).build());
int bpLine = source.getLineNumber(bp[1] - 1);
int bpColumn = source.getColumnNumber(bp[1] - 1);
Pair breakpointPosition = Pair.create(bpLine, bpColumn);
expectSuspended((SuspendedEvent event) -> {
Pair eventPosition = Pair.create(event.getSourceSection().getStartLine(), event.getSourceSection().getStartColumn());
Assert.assertEquals("B" + bpId + ": Expected " + bp[0] + " => " + breakpointPosition + ", event at " + eventPosition,
breakpointPosition, eventPosition);
Assert.assertSame(breakpoint, event.getBreakpoints().iterator().next());
event.prepareContinue();
});
expectDone();
Pair resolvedPosition = Pair.create(resolvedLineColumn[0], resolvedLineColumn[1]);
Assert.assertEquals("B" + bpId + ": Expected resolved " + bp[0] + " => " + bp[1],
breakpointPosition, resolvedPosition);
}
}
}
/**
* Utility method that tests if a breakpoint submitted to any location in the source code
* suspends the execution. A two-pass test is performed. In the first pass, line breakpoints are
* submitted to every line. In the second pass, breakpoints are submitted to every line and
* column combination, even outside the source scope. It is expected that the breakpoints
* resolve to a nearest suspendable location and it is checked that all breakpoints are hit.
*
* @param source a source to evaluate with breakpoints submitted everywhere
* @since 0.33
*/
public void assertBreakpointsBreakEverywhere(Source source) {
assertBreakpointsBreakEverywhere(source, null);
}
/**
* Utility method that tests if a breakpoint submitted to any location permitted by the
* {@link PositionPredicate} in the source code suspends the execution. A two-pass test is
* performed. In the first pass, line breakpoints are submitted to every testable line. In the
* second pass, breakpoints are submitted to every testable line and column combination, even
* outside the source scope, if permitted by the {@link PositionPredicate}. It is expected that
* the breakpoints resolve to a nearest suspendable location and it is checked that all
* submitted breakpoints are hit.
*
* @param source a source to evaluate with breakpoints submitted everywhere
* @param positionPredicate null
to submit breakpoints everywhere, or a predicate
* that limits the testable positions.
* @since 19.3.0
*/
public void assertBreakpointsBreakEverywhere(Source source, PositionPredicate positionPredicate) {
int numLines = source.getLineCount();
int numColumns = 0;
for (int i = 1; i <= numLines; i++) {
int ll = source.getLineLength(i);
if (ll > numColumns) {
numColumns = ll;
}
}
com.oracle.truffle.api.source.Source tsource = DebuggerTester.getSourceImpl(source);
final List breakpoints = new ArrayList<>();
final Map breakpointsResolved = new HashMap<>();
final List breakpointsHit = new ArrayList<>();
Breakpoint.ResolveListener resolveListener = new Breakpoint.ResolveListener() {
@Override
public void breakpointResolved(Breakpoint breakpoint, SourceSection section) {
Assert.assertTrue("Resolved at " + section + ", have " + breakpointsResolved.get(breakpoint),
!breakpointsResolved.containsKey(breakpoint) || !section.equals(breakpointsResolved.get(breakpoint)));
breakpointsResolved.put(breakpoint, section);
}
};
// Test all line breakpoints
for (int l = 1; l <= numLines; l++) {
if (positionPredicate == null || positionPredicate.testLine(l)) {
Breakpoint breakpoint = Breakpoint.newBuilder(tsource).lineIs(l).oneShot().resolveListener(resolveListener).build();
breakpoints.add(breakpoint);
}
}
assertBreakpoints(source, breakpoints, breakpointsResolved, breakpointsHit);
breakpoints.clear();
breakpointsResolved.clear();
breakpointsHit.clear();
// Test all line/column breakpoints
for (int l = 1; l <= numLines; l++) {
int endColumn = (l == numLines) ? source.getLineLength(l) : numColumns + 5;
for (int c = 1; c < endColumn; c++) {
if (positionPredicate == null || positionPredicate.testLineColumn(l, c)) {
Breakpoint breakpoint = Breakpoint.newBuilder(tsource).lineIs(l).columnIs(c).oneShot().resolveListener(resolveListener).build();
breakpoints.add(breakpoint);
}
}
}
assertBreakpoints(source, breakpoints, breakpointsResolved, breakpointsHit);
}
private void assertBreakpoints(Source source, List breakpoints, Map breakpointsResolved, List breakpointsHit) {
try (DebuggerSession session = startSession(new SourceElement[0])) {
for (Breakpoint breakpoint : breakpoints) {
session.install(breakpoint);
}
startEval(source);
while (breakpointsHit.size() != breakpoints.size()) {
try {
expectSuspended((SuspendedEvent event) -> {
breakpointsHit.addAll(event.getBreakpoints());
event.prepareContinue();
});
} catch (Throwable t) {
Set notHit = new HashSet<>(breakpoints);
notHit.removeAll(breakpointsHit);
for (Breakpoint b : notHit) {
err("Not hit " + b + ": " + b.getLocationDescription());
}
err("---");
for (Breakpoint b : breakpointsHit) {
err("Hit " + b + ": " + b.getLocationDescription());
}
throw t;
}
}
expectDone();
}
Assert.assertEquals(breakpoints.size(), breakpointsResolved.size());
Assert.assertEquals(breakpoints.size(), breakpointsHit.size());
}
/**
* Get {@link com.oracle.truffle.api.source.Source Truffle Source} that corresponds to the
* {@link Source Polyglot Source}. This is a bridge between the two Source implementations.
*
* @since 0.28
*/
public static com.oracle.truffle.api.source.Source getSourceImpl(Source source) {
return (com.oracle.truffle.api.source.Source) getField(source, "receiver");
}
// Copied from ReflectionUtils.
private static Object getField(Object value, String name) {
try {
Field f = value.getClass().getDeclaredField(name);
setAccessible(f, true);
return f.get(value);
} catch (Exception e) {
throw new AssertionError(e);
}
}
private static void setAccessible(Field field, boolean flag) {
openForReflectionTo(field.getDeclaringClass(), DebuggerTester.class);
field.setAccessible(flag);
}
/**
* Opens {@code declaringClass}'s package to allow a method declared in {@code accessor} to call
* {@link AccessibleObject#setAccessible(boolean)} on an {@link AccessibleObject} representing a
* field or method declared by {@code declaringClass}.
*/
private static void openForReflectionTo(Class> declaringClass, Class> accessor) {
try {
Method getModule = Class.class.getMethod("getModule");
Class> moduleClass = getModule.getReturnType();
Class> modulesClass = Class.forName("jdk.internal.module.Modules");
Method addOpens = maybeGetAddOpensMethod(moduleClass, modulesClass);
if (addOpens != null) {
Object moduleToOpen = getModule.invoke(declaringClass);
Object accessorModule = getModule.invoke(accessor);
if (moduleToOpen != accessorModule) {
addOpens.invoke(null, moduleToOpen, declaringClass.getPackage().getName(), accessorModule);
}
}
} catch (Exception e) {
throw new AssertionError(e);
}
}
private static Method maybeGetAddOpensMethod(Class> moduleClass, Class> modulesClass) {
try {
return modulesClass.getDeclaredMethod("addOpens", moduleClass, String.class, moduleClass);
} catch (NoSuchMethodException e) {
// This method was introduced by JDK-8169069
return null;
}
}
private void putEvent(Object event) {
trace("Put event " + event + " to " + this + ": " + Thread.currentThread());
if (event instanceof SuspendedEvent) {
try {
if (handler == null) {
throw new AssertionError("Expected done but got event " + event);
}
handler.onSuspend((SuspendedEvent) event);
} catch (Throwable e) {
newEvent.add(e);
return;
}
}
newEvent.add(event);
}
private Object takeEvent() throws InterruptedException {
trace("Take event " + this + ": " + Thread.currentThread());
try {
return newEvent.take();
} finally {
trace("Taken event " + this + ": " + Thread.currentThread());
}
}
private void onSuspend(SuspendedEvent event) {
trace("On SUSPEND " + event + " of " + this + ": " + Thread.currentThread());
if (closed) {
return;
}
try {
putEvent(event);
} finally {
waitForExecuting();
}
}
private void waitForExecuting() {
trace("Wait for executing " + this + ": " + Thread.currentThread());
if (closed) {
return;
}
try {
executing.acquire();
} catch (InterruptedException e) {
throw new AssertionError(e);
}
trace("Wait for executing released " + this + ": " + Thread.currentThread());
}
private void notifyNextAction() {
trace("Notify next action " + this + ": " + Thread.currentThread());
executing.release();
}
private static final class ExecutingSource {
private final Function function;
private Throwable error;
private String returnValue;
ExecutingSource(Function function) {
this.function = function;
}
@Override
public String toString() {
return "ExecutingSource[" + function + "], error = " + error + ", returnValue = " + returnValue;
}
}
/**
* Predicate of testable positions.
*
* @since 19.3.0
*/
public interface PositionPredicate {
/**
* Whether to test at the line.
*
* @since 19.3.0
*/
boolean testLine(int line);
/**
* Whether to test at the line and column position.
*
* @since 19.3.0
*/
boolean testLineColumn(int line, int column);
}
class ExecutingLoop implements Runnable {
private final Context.Builder contextBuilder;
private final AtomicReference engineRef;
private final AtomicReference error;
ExecutingLoop(Context.Builder contextBuilder, AtomicReference engineRef, AtomicReference error) {
this.contextBuilder = (contextBuilder != null) ? contextBuilder : Context.newBuilder();
this.engineRef = engineRef;
this.error = error;
}
@Override
public void run() {
Context context = null;
try {
try {
context = contextBuilder.out(out).err(err).build();
engineRef.set(context.getEngine());
} catch (Throwable t) {
error.set(t);
return;
} finally {
initialized.release();
}
while (true) {
waitForExecuting();
if (closed) {
return;
}
ExecutingSource s = executingSource;
try {
trace("Start executing " + s + " on " + DebuggerTester.this + ": " + Thread.currentThread());
s.returnValue = s.function.apply(context).toString();
} catch (Throwable e) {
s.error = e;
} finally {
trace("Done executing " + s + " on " + DebuggerTester.this + ": " + Thread.currentThread());
putEvent(s);
}
}
} finally {
if (context != null) {
try {
context.close();
} catch (PolyglotException pe) {
if (!pe.isCancelled() && !pe.isExit()) {
throw pe;
}
}
}
}
}
}
}
class DebuggerTesterSnippets {
// @start region = "DebuggerTesterSnippets#testDebugging"
public void testDebugging() {
try (DebuggerTester tester = new DebuggerTester()) {
// use your guest language source here
Source source = null;
try (DebuggerSession session = tester.startSession()) {
session.suspendNextExecution();
tester.startEval(source);
tester.expectSuspended(new SuspendedCallback() {
@Override
public void onSuspend(SuspendedEvent event) {
// assert suspended event is proper here
event.prepareStepInto(1);
}
});
tester.expectSuspended(new SuspendedCallback() {
@Override
public void onSuspend(SuspendedEvent event) {
// assert another suspended event is proper here
event.prepareContinue();
}
});
// expect no more suspended events
tester.expectDone();
}
}
}
// @end region = "DebuggerTesterSnippets#testDebugging"
}