All Downloads are FREE. Search and download functionalities are using the official Maven repository.

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.

There is a newer version: 1.0.0-rc7
Show newest version
/*
 * Copyright (c) 2017, 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.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * 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.
 */
package com.oracle.truffle.tck;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
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.regex.Matcher;
import java.util.regex.Pattern;

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.SuspendedCallback;
import com.oracle.truffle.api.debug.SuspendedEvent;
import com.oracle.truffle.api.debug.SourceElement;
import com.oracle.truffle.api.source.SourceSection;

import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.Engine;
import org.graalvm.polyglot.PolyglotException;
import org.graalvm.polyglot.Source;

/**
 * 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: {@link com.oracle.truffle.tck.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); } /** * @since 0.16 * @deprecated Use {@link #startEval(org.graalvm.polyglot.Source)} instead. */ @Deprecated public void startEval(com.oracle.truffle.api.source.Source s) { if (this.executingSource != null) { throw new IllegalStateException("Already executing other source " + s); } throw new UnsupportedOperationException("Call startEval(org.graalvm.polyglot.Source) instead."); } /** * 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. Throws an {@link IllegalStateException} if * another evaluation is still executing or the tester is already closed. * * @since 0.27 */ public void startEval(Source s) { if (this.executingSource != null) { throw new IllegalStateException("Already executing other source " + s); } this.executingSource = new ExecutingSource(s); } /** * Expects an 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) { 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()) { 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(error.getMessage(), error.getMessage().contains("KillException")); 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) throws IOException { 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).build(); com.oracle.truffle.api.source.Source tsource = DebuggerTester.getSourceImpl(source); for (int l = 1; l < source.getLineCount(); l++) { if (!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) throws IOException { 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).build(); 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[] resolvedIndexPtr = new int[]{0}; Breakpoint breakpoint = session.install( Breakpoint.newBuilder(DebuggerTester.getSourceImpl(source)).lineIs(line).columnIs(column).oneShot().resolveListener(new Breakpoint.ResolveListener() { @Override public void breakpointResolved(Breakpoint brkp, SourceSection section) { resolvedIndexPtr[0] = section.getCharIndex() + 1; if (TRACE) { trace(" resolved: " + (resolvedIndexPtr[0])); } } }).build()); expectSuspended((SuspendedEvent event) -> { Assert.assertEquals("Expected " + bp[0] + " => " + bp[1] + ", resolved at " + resolvedIndexPtr[0], bp[1], event.getSourceSection().getCharIndex() + 1); Assert.assertSame(breakpoint, event.getBreakpoints().iterator().next()); event.prepareContinue(); }); expectDone(); Assert.assertEquals("Expected resolved " + bp[0] + " => " + bp[1], bp[1], resolvedIndexPtr[0]); } } } /** * 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) { 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 Set breakpointsResolved = new HashSet<>(); final List breakpointsHit = new ArrayList<>(); Breakpoint.ResolveListener resolveListener = new Breakpoint.ResolveListener() { @Override public void breakpointResolved(Breakpoint breakpoint, SourceSection section) { Assert.assertFalse(breakpointsResolved.contains(breakpoint)); breakpointsResolved.add(breakpoint); } }; // Test all line breakpoints for (int l = 1; l < (numLines + 5); 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 + 5); l++) { for (int c = 1; c < (numColumns + 5); 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, Set 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, "impl"); } // 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) { if (!Java8OrEarlier) { openForReflectionTo(field.getDeclaringClass(), DebuggerTester.class); } field.setAccessible(flag); } private static final boolean Java8OrEarlier = System.getProperty("java.specification.version").compareTo("1.9") < 0; /** * 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 " + this + ": " + Thread.currentThread()); if (event instanceof SuspendedEvent) { try { 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) { 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 Source source; private Throwable error; private String returnValue; ExecutingSource(Source source) { this.source = source; } } 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 " + this); s.returnValue = context.eval(s.source).toString(); trace("Done executing " + this); } catch (Throwable e) { s.error = e; } finally { putEvent(s); } } } finally { if (context != null) { context.close(); } } } } } class DebuggerTesterSnippets { // BEGIN: 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: DebuggerTesterSnippets.testDebugging }