/*
* Copyright (c) 2016, 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.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicReference;
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.vm.PolyglotEngine;
import java.util.function.Function;
/**
* 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.
* <p>
* 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<Object> newEvent;
private final Semaphore executing;
private final Semaphore initialized;
private final Thread evalThread;
private final PolyglotEngine engine;
private final ByteArrayOutputStream out = new ByteArrayOutputStream();
private final ByteArrayOutputStream err = new ByteArrayOutputStream();
private volatile boolean closed;
private volatile ExecutingSource source;
private static void trace(String message) {
if (TRACE) {
PrintStream out = System.out;
out.println("DebuggerTester: " + message);
}
}
private final ExecutingLoop executingLoop;
private SuspendedCallback handler;
/**
* Constructs a new debugger tester instance. Boots up a new {@link PolyglotEngine engine} 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. Boots up a new {@link PolyglotEngine engine} on
* Thread in the background. The tester instance needs to be {@link #close() closed} after use.
* Throws an AssertionError if the engine initialization fails.
*
* @param engineBuilderDecorator a decorator function that allows to customize the engine
* builder
* @since 0.26
*/
public DebuggerTester(Function<PolyglotEngine.Builder, PolyglotEngine.Builder> engineBuilderDecorator) {
this.newEvent = new ArrayBlockingQueue<>(1);
this.executing = new Semaphore(0);
this.initialized = new Semaphore(0);
final AtomicReference<PolyglotEngine> engineRef = new AtomicReference<>();
final AtomicReference<Throwable> error = new AtomicReference<>();
this.executingLoop = new ExecutingLoop(engineRef, engineBuilderDecorator, 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 PolyglotEngine engine}.
*
* @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 PolyglotEngine engine}.
*
* @since 0.16
*/
public String getOut() {
try {
out.flush();
} catch (IOException e) {
throw new RuntimeException(e);
}
return new String(out.toByteArray());
}
/**
* Starts a new {@link Debugger#startSession(SuspendedCallback) debugger session} in the
* {@link PolyglotEngine 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 Debugger.find(engine).startSession(new SuspendedCallback() {
public void onSuspend(SuspendedEvent event) {
DebuggerTester.this.onSuspend(event);
}
});
}
/**
* Starts a new {@link PolyglotEngine#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.16
*/
public void startEval(Source s) {
if (this.source != null) {
throw new IllegalStateException("Already executing other source " + s);
}
this.source = 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 {
throw new AssertionError("Got unknown");
}
} finally {
source = 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.getClass().getSimpleName().equals("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;
}
/**
* 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();
}
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 AtomicReference<PolyglotEngine> engineRef;
private final Function<PolyglotEngine.Builder, PolyglotEngine.Builder> engineBuilderDecorator;
private final AtomicReference<Throwable> error;
ExecutingLoop(AtomicReference<PolyglotEngine> engineRef, Function<PolyglotEngine.Builder, PolyglotEngine.Builder> engineBuilderDecorator, AtomicReference<Throwable> error) {
this.engineRef = engineRef;
this.engineBuilderDecorator = engineBuilderDecorator;
this.error = error;
}
@Override
public void run() {
PolyglotEngine localEngine;
try {
PolyglotEngine.Builder builder = PolyglotEngine.newBuilder().setOut(out).setErr(err);
if (engineBuilderDecorator != null) {
builder = engineBuilderDecorator.apply(builder);
}
localEngine = builder.build();
engineRef.set(localEngine);
} catch (Throwable t) {
error.set(t);
return;
} finally {
initialized.release();
}
while (true) {
waitForExecuting();
if (closed) {
return;
}
try {
trace("Start executing " + this);
source.returnValue = localEngine.eval(source.source).as(String.class);
trace("Done executing " + this);
} catch (Throwable e) {
source.error = e;
} finally {
putEvent(source);
}
}
}
}
}
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
}