aboutsummaryrefslogtreecommitdiffhomepage
path: root/src
diff options
context:
space:
mode:
authorGravatar Googler <noreply@google.com>2018-06-07 14:07:17 -0700
committerGravatar Copybara-Service <copybara-piper@google.com>2018-06-07 14:08:44 -0700
commit5e893626640351de0f12e36bb14d80af0ff1e036 (patch)
treeb9a7083618290b867bd40383881a06e89060ce27 /src
parent755278df00f65818dc092fe4f8a31bdec1aaaab5 (diff)
Add an initial skylark debug server implementation.
Not intending this to be a complete implementation yet. Among the things still to do: - officially add support for debugging aspects, rules, etc., with corresponding tests. - handle breakpoints at return statements (an edge case which bypasses Eval). PiperOrigin-RevId: 199692670
Diffstat (limited to 'src')
-rw-r--r--src/main/java/com/google/devtools/build/lib/skylarkdebug/module/BUILD2
-rw-r--r--src/main/java/com/google/devtools/build/lib/skylarkdebug/module/SkylarkDebuggerModule.java14
-rw-r--r--src/main/java/com/google/devtools/build/lib/skylarkdebug/proto/skylark_debugging.proto15
-rw-r--r--src/main/java/com/google/devtools/build/lib/skylarkdebug/server/BUILD1
-rw-r--r--src/main/java/com/google/devtools/build/lib/skylarkdebug/server/DebugEventHelper.java203
-rw-r--r--src/main/java/com/google/devtools/build/lib/skylarkdebug/server/DebugRequestException.java26
-rw-r--r--src/main/java/com/google/devtools/build/lib/skylarkdebug/server/DebugServerTransport.java106
-rw-r--r--src/main/java/com/google/devtools/build/lib/skylarkdebug/server/SkylarkDebugServer.java261
-rw-r--r--src/main/java/com/google/devtools/build/lib/skylarkdebug/server/ThreadHandler.java334
-rw-r--r--src/main/java/com/google/devtools/build/lib/syntax/DebugServer.java14
-rw-r--r--src/main/java/com/google/devtools/build/lib/syntax/DebugServerUtils.java23
-rw-r--r--src/test/java/com/google/devtools/build/lib/skylarkdebug/server/BUILD49
-rw-r--r--src/test/java/com/google/devtools/build/lib/skylarkdebug/server/DebugServerTransportTest.java134
-rw-r--r--src/test/java/com/google/devtools/build/lib/skylarkdebug/server/MockDebugClient.java157
-rw-r--r--src/test/java/com/google/devtools/build/lib/skylarkdebug/server/SkylarkDebugServerTest.java572
15 files changed, 1895 insertions, 16 deletions
diff --git a/src/main/java/com/google/devtools/build/lib/skylarkdebug/module/BUILD b/src/main/java/com/google/devtools/build/lib/skylarkdebug/module/BUILD
index 00fae167a0..0fc7f18c04 100644
--- a/src/main/java/com/google/devtools/build/lib/skylarkdebug/module/BUILD
+++ b/src/main/java/com/google/devtools/build/lib/skylarkdebug/module/BUILD
@@ -13,6 +13,8 @@ java_library(
":options",
"//src/main/java/com/google/devtools/build/lib:events",
"//src/main/java/com/google/devtools/build/lib:runtime",
+ "//src/main/java/com/google/devtools/build/lib:syntax",
+ "//src/main/java/com/google/devtools/build/lib/skylarkdebug/server",
],
)
diff --git a/src/main/java/com/google/devtools/build/lib/skylarkdebug/module/SkylarkDebuggerModule.java b/src/main/java/com/google/devtools/build/lib/skylarkdebug/module/SkylarkDebuggerModule.java
index 167d3c853b..8dbe9e020a 100644
--- a/src/main/java/com/google/devtools/build/lib/skylarkdebug/module/SkylarkDebuggerModule.java
+++ b/src/main/java/com/google/devtools/build/lib/skylarkdebug/module/SkylarkDebuggerModule.java
@@ -14,9 +14,13 @@
package com.google.devtools.build.lib.skylarkdebug.module;
+import com.google.devtools.build.lib.events.Event;
import com.google.devtools.build.lib.events.Reporter;
import com.google.devtools.build.lib.runtime.BlazeModule;
import com.google.devtools.build.lib.runtime.CommandEnvironment;
+import com.google.devtools.build.lib.skylarkdebug.server.SkylarkDebugServer;
+import com.google.devtools.build.lib.syntax.DebugServerUtils;
+import java.io.IOException;
/** Blaze module for setting up Skylark debugging. */
public final class SkylarkDebuggerModule extends BlazeModule {
@@ -38,10 +42,16 @@ public final class SkylarkDebuggerModule extends BlazeModule {
}
private static void initializeDebugging(Reporter reporter, int debugPort) {
- // TODO(brendandouglas): implement a debug server
+ try {
+ SkylarkDebugServer server =
+ SkylarkDebugServer.createAndWaitForConnection(reporter, debugPort);
+ DebugServerUtils.initializeDebugServer(server);
+ } catch (IOException e) {
+ reporter.handle(Event.error("Error while setting up the debug server: " + e.getMessage()));
+ }
}
private static void disableDebugging() {
- // TODO(brendandouglas): implement a debug server
+ DebugServerUtils.disableDebugging();
}
}
diff --git a/src/main/java/com/google/devtools/build/lib/skylarkdebug/proto/skylark_debugging.proto b/src/main/java/com/google/devtools/build/lib/skylarkdebug/proto/skylark_debugging.proto
index 804319397a..ed398e70fa 100644
--- a/src/main/java/com/google/devtools/build/lib/skylarkdebug/proto/skylark_debugging.proto
+++ b/src/main/java/com/google/devtools/build/lib/skylarkdebug/proto/skylark_debugging.proto
@@ -89,8 +89,9 @@ message StartDebuggingRequest {
message PauseThreadRequest {
// The identifier of the thread to be paused.
//
- // If not set (i.e. zero), all current and future Skylark threads will be
- // paused until resumed via a ContinueExecutionRequest.
+ // If not set (i.e. zero), all current Skylark threads will be paused, and
+ // until a ContinueExecutionRequest is sent, any future Skylark threads will
+ // also start off paused.
int64 thread_id = 1;
}
@@ -195,6 +196,7 @@ message ThreadContinuedEvent {
message Breakpoint {
oneof condition {
// A breakpoint that is triggered when a particular line is reached.
+ // Column index will be ignored for breakpoints.
Location location = 1;
}
// An optional condition for the breakpoint. When present, the breakpoint will
@@ -220,11 +222,12 @@ message Location {
// The path of the Skylark source file.
string path = 1;
- // A 0-based line index in the file denoted by path.
- uint32 line_index = 2;
+ // A 1-indexed line number in the file denoted by path.
+ uint32 line_number = 2;
- // A 0-based column index in the file denoted by path.
- uint32 column_index = 3;
+ // A 1-indexed column number in the file denoted by path. 0 (/unset) indicates
+ // column number is unknown or irrelevant.
+ uint32 column_number = 3;
}
// A scope that contains value bindings accessible in a frame.
diff --git a/src/main/java/com/google/devtools/build/lib/skylarkdebug/server/BUILD b/src/main/java/com/google/devtools/build/lib/skylarkdebug/server/BUILD
index 42768bb69d..5122e25329 100644
--- a/src/main/java/com/google/devtools/build/lib/skylarkdebug/server/BUILD
+++ b/src/main/java/com/google/devtools/build/lib/skylarkdebug/server/BUILD
@@ -10,6 +10,7 @@ java_library(
name = "server",
srcs = glob(["*.java"]),
deps = [
+ "//src/main/java/com/google/devtools/build/lib:events",
"//src/main/java/com/google/devtools/build/lib:skylarkinterface",
"//src/main/java/com/google/devtools/build/lib:syntax",
"//src/main/java/com/google/devtools/build/lib/collect/nestedset",
diff --git a/src/main/java/com/google/devtools/build/lib/skylarkdebug/server/DebugEventHelper.java b/src/main/java/com/google/devtools/build/lib/skylarkdebug/server/DebugEventHelper.java
new file mode 100644
index 0000000000..0254484062
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skylarkdebug/server/DebugEventHelper.java
@@ -0,0 +1,203 @@
+// Copyright 2018 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.devtools.build.lib.skylarkdebug.server;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.devtools.build.lib.events.Location;
+import com.google.devtools.build.lib.skylarkdebugging.SkylarkDebuggingProtos;
+import com.google.devtools.build.lib.skylarkdebugging.SkylarkDebuggingProtos.ContinueExecutionResponse;
+import com.google.devtools.build.lib.skylarkdebugging.SkylarkDebuggingProtos.DebugEvent;
+import com.google.devtools.build.lib.skylarkdebugging.SkylarkDebuggingProtos.Error;
+import com.google.devtools.build.lib.skylarkdebugging.SkylarkDebuggingProtos.EvaluateResponse;
+import com.google.devtools.build.lib.skylarkdebugging.SkylarkDebuggingProtos.Frame;
+import com.google.devtools.build.lib.skylarkdebugging.SkylarkDebuggingProtos.ListFramesResponse;
+import com.google.devtools.build.lib.skylarkdebugging.SkylarkDebuggingProtos.ListThreadsResponse;
+import com.google.devtools.build.lib.skylarkdebugging.SkylarkDebuggingProtos.PauseThreadResponse;
+import com.google.devtools.build.lib.skylarkdebugging.SkylarkDebuggingProtos.Scope;
+import com.google.devtools.build.lib.skylarkdebugging.SkylarkDebuggingProtos.SetBreakpointsResponse;
+import com.google.devtools.build.lib.skylarkdebugging.SkylarkDebuggingProtos.StartDebuggingResponse;
+import com.google.devtools.build.lib.skylarkdebugging.SkylarkDebuggingProtos.Thread;
+import com.google.devtools.build.lib.skylarkdebugging.SkylarkDebuggingProtos.ThreadContinuedEvent;
+import com.google.devtools.build.lib.skylarkdebugging.SkylarkDebuggingProtos.ThreadEndedEvent;
+import com.google.devtools.build.lib.skylarkdebugging.SkylarkDebuggingProtos.ThreadPausedEvent;
+import com.google.devtools.build.lib.skylarkdebugging.SkylarkDebuggingProtos.ThreadStartedEvent;
+import com.google.devtools.build.lib.skylarkdebugging.SkylarkDebuggingProtos.Value;
+import com.google.devtools.build.lib.syntax.DebugFrame;
+import com.google.devtools.build.lib.syntax.Debuggable.Stepping;
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import javax.annotation.Nullable;
+
+/**
+ * Helper class for constructing event or response protos to be sent from the debug server to a
+ * debugger client.
+ */
+final class DebugEventHelper {
+ private DebugEventHelper() {}
+
+ private static final long NO_SEQUENCE_NUMBER = 0;
+
+ static DebugEvent error(String message) {
+ return error(NO_SEQUENCE_NUMBER, message);
+ }
+
+ static DebugEvent error(long sequenceNumber, String message) {
+ return DebugEvent.newBuilder()
+ .setSequenceNumber(sequenceNumber)
+ .setError(Error.newBuilder().setMessage(message))
+ .build();
+ }
+
+ static DebugEvent listThreadsResponse(long sequenceNumber, List<Thread> threads) {
+ return DebugEvent.newBuilder()
+ .setSequenceNumber(sequenceNumber)
+ .setListThreads(ListThreadsResponse.newBuilder().addAllThread(threads).build())
+ .build();
+ }
+
+ static DebugEvent setBreakpointsResponse(long sequenceNumber) {
+ return DebugEvent.newBuilder()
+ .setSequenceNumber(sequenceNumber)
+ .setSetBreakpoints(SetBreakpointsResponse.newBuilder())
+ .build();
+ }
+
+ static DebugEvent continueExecutionResponse(long sequenceNumber) {
+ return DebugEvent.newBuilder()
+ .setSequenceNumber(sequenceNumber)
+ .setContinueExecution(ContinueExecutionResponse.newBuilder())
+ .build();
+ }
+
+ static DebugEvent evaluateResponse(long sequenceNumber, Value value) {
+ return DebugEvent.newBuilder()
+ .setSequenceNumber(sequenceNumber)
+ .setEvaluate(EvaluateResponse.newBuilder().setResult(value))
+ .build();
+ }
+
+ static DebugEvent listFramesResponse(long sequenceNumber, Collection<Frame> frames) {
+ return DebugEvent.newBuilder()
+ .setSequenceNumber(sequenceNumber)
+ .setListFrames(ListFramesResponse.newBuilder().addAllFrame(frames))
+ .build();
+ }
+
+ static DebugEvent startDebuggingResponse(long sequenceNumber) {
+ return DebugEvent.newBuilder()
+ .setSequenceNumber(sequenceNumber)
+ .setStartDebugging(StartDebuggingResponse.newBuilder())
+ .build();
+ }
+
+ static DebugEvent pauseThreadResponse(long sequenceNumber) {
+ return DebugEvent.newBuilder()
+ .setSequenceNumber(sequenceNumber)
+ .setPauseThread(PauseThreadResponse.newBuilder())
+ .build();
+ }
+
+ static DebugEvent threadStartedEvent(long threadId, String threadName) {
+ return DebugEvent.newBuilder()
+ .setThreadStarted(
+ ThreadStartedEvent.newBuilder()
+ .setThread(Thread.newBuilder().setId(threadId).setName(threadName))
+ .build())
+ .build();
+ }
+
+ static DebugEvent threadEndedEvent(long threadId, String threadName) {
+ return DebugEvent.newBuilder()
+ .setThreadEnded(
+ ThreadEndedEvent.newBuilder()
+ .setThread(Thread.newBuilder().setId(threadId).setName(threadName))
+ .build())
+ .build();
+ }
+
+ static DebugEvent threadPausedEvent(Thread thread) {
+ return DebugEvent.newBuilder()
+ .setThreadPaused(ThreadPausedEvent.newBuilder().setThread(thread))
+ .build();
+ }
+
+ static DebugEvent threadContinuedEvent(Thread thread) {
+ return DebugEvent.newBuilder()
+ .setThreadContinued(ThreadContinuedEvent.newBuilder().setThread(thread))
+ .build();
+ }
+
+ @Nullable
+ static SkylarkDebuggingProtos.Location getLocationProto(@Nullable Location location) {
+ if (location == null) {
+ return null;
+ }
+ Location.LineAndColumn lineAndColumn = location.getStartLineAndColumn();
+ if (lineAndColumn == null) {
+ return null;
+ }
+ return SkylarkDebuggingProtos.Location.newBuilder()
+ .setLineNumber(lineAndColumn.getLine())
+ .setColumnNumber(lineAndColumn.getColumn())
+ .setPath(location.getPath().getPathString())
+ .build();
+ }
+
+ static SkylarkDebuggingProtos.Frame getFrameProto(DebugFrame frame) {
+ return SkylarkDebuggingProtos.Frame.newBuilder()
+ .setFunctionName(frame.functionName())
+ .setLocation(getLocationProto(frame.location()))
+ .addAllScope(getScopes(frame))
+ .build();
+ }
+
+ private static ImmutableList<Scope> getScopes(DebugFrame frame) {
+ ImmutableMap<String, Object> localVars = frame.lexicalFrameBindings();
+ if (localVars.isEmpty()) {
+ return ImmutableList.of(getScope("global", frame.globalBindings()));
+ }
+ Map<String, Object> globalVars = new LinkedHashMap<>(frame.globalBindings());
+ // remove shadowed bindings
+ localVars.keySet().forEach(globalVars::remove);
+
+ return ImmutableList.of(getScope("local", localVars), getScope("global", globalVars));
+ }
+
+ private static SkylarkDebuggingProtos.Scope getScope(String name, Map<String, Object> bindings) {
+ SkylarkDebuggingProtos.Scope.Builder builder =
+ SkylarkDebuggingProtos.Scope.newBuilder().setName(name);
+ bindings.forEach((s, o) -> builder.addBinding(DebuggerSerialization.getValueProto(s, o)));
+ return builder.build();
+ }
+
+ static Stepping convertSteppingEnum(SkylarkDebuggingProtos.Stepping stepping) {
+ switch (stepping) {
+ case INTO:
+ return Stepping.INTO;
+ case OUT:
+ return Stepping.OUT;
+ case OVER:
+ return Stepping.OVER;
+ case NONE:
+ return Stepping.NONE;
+ case UNRECOGNIZED:
+ // fall through to exception
+ }
+ throw new IllegalArgumentException("Unsupported stepping type");
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skylarkdebug/server/DebugRequestException.java b/src/main/java/com/google/devtools/build/lib/skylarkdebug/server/DebugRequestException.java
new file mode 100644
index 0000000000..092ac14fe5
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skylarkdebug/server/DebugRequestException.java
@@ -0,0 +1,26 @@
+// Copyright 2018 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.devtools.build.lib.skylarkdebug.server;
+
+/**
+ * An exception that occurs while performing a debug request. Should be caught and communicated back
+ * to the debug client.
+ */
+final class DebugRequestException extends Exception {
+
+ DebugRequestException(String message) {
+ super(message);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skylarkdebug/server/DebugServerTransport.java b/src/main/java/com/google/devtools/build/lib/skylarkdebug/server/DebugServerTransport.java
new file mode 100644
index 0000000000..0763fd5047
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skylarkdebug/server/DebugServerTransport.java
@@ -0,0 +1,106 @@
+// Copyright 2018 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.devtools.build.lib.skylarkdebug.server;
+
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.skylarkdebugging.SkylarkDebuggingProtos.DebugEvent;
+import com.google.devtools.build.lib.skylarkdebugging.SkylarkDebuggingProtos.DebugRequest;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.ServerSocket;
+import java.net.Socket;
+import javax.annotation.Nullable;
+
+/**
+ * Manages the connection to and communication to/from the debugger client. Reading and writing are
+ * internally synchronized by {@link DebugServerTransport}.
+ */
+final class DebugServerTransport {
+
+ /** Sets up the server transport and blocks while waiting for an incoming connection. */
+ static DebugServerTransport createAndWaitForClient(
+ EventHandler eventHandler, ServerSocket serverSocket) throws IOException {
+ // TODO(bazel-team): reject all connections after the first
+ eventHandler.handle(Event.progress("Waiting for debugger..."));
+ Socket clientSocket = serverSocket.accept();
+ return new DebugServerTransport(
+ eventHandler,
+ serverSocket,
+ clientSocket,
+ clientSocket.getInputStream(),
+ clientSocket.getOutputStream());
+ }
+
+ private final EventHandler eventHandler;
+ private final ServerSocket serverSocket;
+ private final Socket clientSocket;
+ private final InputStream requestStream;
+ private final OutputStream eventStream;
+
+ private DebugServerTransport(
+ EventHandler eventHandler,
+ ServerSocket serverSocket,
+ Socket clientSocket,
+ InputStream requestStream,
+ OutputStream eventStream) {
+ this.eventHandler = eventHandler;
+ this.serverSocket = serverSocket;
+ this.clientSocket = clientSocket;
+ this.requestStream = requestStream;
+ this.eventStream = eventStream;
+ }
+
+ /**
+ * Blocks waiting for a properly-formed client request. Returns null if the client connection is
+ * closed.
+ */
+ @Nullable
+ DebugRequest readClientRequest() {
+ synchronized (requestStream) {
+ while (true) {
+ if (!clientSocket.isConnected() || clientSocket.isClosed()) {
+ return null;
+ }
+ try {
+ return DebugRequest.parseDelimitedFrom(requestStream);
+ } catch (IOException e) {
+ postEvent(DebugEventHelper.error("Error parsing debug request: " + e.getMessage()));
+ }
+ }
+ }
+ }
+
+ /** Posts a debug event. */
+ void postEvent(DebugEvent event) {
+ synchronized (eventStream) {
+ try {
+ event.writeDelimitedTo(eventStream);
+ } catch (IOException e) {
+ eventHandler.handle(Event.error("Failed to send debug event to client: " + e.getMessage()));
+ }
+ }
+ }
+
+ void close() throws IOException {
+ clientSocket.close();
+ serverSocket.close();
+ }
+
+ boolean isClosed() {
+ return serverSocket.isClosed() || clientSocket.isClosed();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skylarkdebug/server/SkylarkDebugServer.java b/src/main/java/com/google/devtools/build/lib/skylarkdebug/server/SkylarkDebugServer.java
new file mode 100644
index 0000000000..71a3d7b41f
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skylarkdebug/server/SkylarkDebugServer.java
@@ -0,0 +1,261 @@
+// Copyright 2018 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.devtools.build.lib.skylarkdebug.server;
+
+import static com.google.common.collect.ImmutableSet.toImmutableSet;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Throwables;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.events.Location;
+import com.google.devtools.build.lib.skylarkdebugging.SkylarkDebuggingProtos;
+import com.google.devtools.build.lib.skylarkdebugging.SkylarkDebuggingProtos.Breakpoint.ConditionCase;
+import com.google.devtools.build.lib.syntax.DebugServer;
+import com.google.devtools.build.lib.syntax.Environment;
+import com.google.devtools.build.lib.syntax.Eval;
+import com.google.devtools.build.lib.syntax.EvalException;
+import com.google.devtools.build.lib.syntax.Statement;
+import java.io.IOException;
+import java.net.ServerSocket;
+import java.util.List;
+import java.util.function.Function;
+import javax.annotation.Nullable;
+
+/** Manages the network socket and debugging state for threads running Skylark code. */
+public final class SkylarkDebugServer implements DebugServer {
+
+ /**
+ * Initializes debugging support, setting up any debugging-specific overrides, then opens the
+ * debug server socket and blocks waiting for an incoming connection.
+ *
+ * @param port the port on which the server should listen for connections
+ * @throws IOException if an I/O error occurs while opening the socket or waiting for a connection
+ */
+ public static SkylarkDebugServer createAndWaitForConnection(EventHandler eventHandler, int port)
+ throws IOException {
+ ServerSocket serverSocket = new ServerSocket(port, /* backlog */ 1);
+ return createAndWaitForConnection(eventHandler, serverSocket);
+ }
+
+ /**
+ * Initializes debugging support, setting up any debugging-specific overrides, then opens the
+ * debug server socket and blocks waiting for an incoming connection.
+ *
+ * @throws IOException if an I/O error occurs while waiting for a connection
+ */
+ @VisibleForTesting
+ static SkylarkDebugServer createAndWaitForConnection(
+ EventHandler eventHandler, ServerSocket serverSocket) throws IOException {
+ DebugServerTransport transport =
+ DebugServerTransport.createAndWaitForClient(eventHandler, serverSocket);
+ return new SkylarkDebugServer(eventHandler, transport);
+ }
+
+ private final EventHandler eventHandler;
+ /** Handles all thread-related state. */
+ private final ThreadHandler threadHandler;
+ /** The server socket for the debug server. */
+ private final DebugServerTransport transport;
+
+ private SkylarkDebugServer(EventHandler eventHandler, DebugServerTransport transport) {
+ this.eventHandler = eventHandler;
+ this.threadHandler = new ThreadHandler();
+ this.transport = transport;
+ listenForClientRequests();
+ }
+
+ /**
+ * Starts a worker thread to listen for and handle incoming client requests, returning any
+ * relevant responses.
+ */
+ private void listenForClientRequests() {
+ Thread clientThread =
+ new Thread(
+ () -> {
+ try {
+ while (true) {
+ SkylarkDebuggingProtos.DebugRequest request = transport.readClientRequest();
+ if (request == null) {
+ return;
+ }
+ SkylarkDebuggingProtos.DebugEvent response = handleClientRequest(request);
+ if (response != null) {
+ transport.postEvent(response);
+ }
+ }
+ } catch (Throwable e) {
+ if (!transport.isClosed()) {
+ eventHandler.handle(
+ Event.error(
+ "Debug server listener thread died: "
+ + Throwables.getStackTraceAsString(e)));
+ }
+ }
+ });
+
+ clientThread.setDaemon(true);
+ clientThread.start();
+ }
+
+ @Override
+ public void close() {
+ try {
+ transport.close();
+
+ } catch (IOException e) {
+ eventHandler.handle(
+ Event.error(
+ "Error shutting down the debug server: " + Throwables.getStackTraceAsString(e)));
+ }
+ }
+
+ @Override
+ public Function<Environment, Eval> evalOverride() {
+ return DebugAwareEval::new;
+ }
+
+ @Override
+ public <T> T runWithDebugging(Environment env, String threadName, DebugCallable<T> callable)
+ throws EvalException, InterruptedException {
+ long threadId = Thread.currentThread().getId();
+ threadHandler.registerThread(threadId, threadName, env);
+ transport.postEvent(DebugEventHelper.threadStartedEvent(threadId, threadName));
+ try {
+ return callable.call();
+ } finally {
+ transport.postEvent(DebugEventHelper.threadEndedEvent(threadId, threadName));
+ threadHandler.unregisterThread(threadId);
+ }
+ }
+
+ /**
+ * Pauses the execution of the current thread if there are conditions that should cause it to be
+ * paused, such as a breakpoint being reached.
+ *
+ * @param location the location of the statement or expression currently being executed
+ */
+ @VisibleForTesting
+ void pauseIfNecessary(Environment env, Location location) {
+ if (!transport.isClosed()) {
+ threadHandler.pauseIfNecessary(env, location, transport);
+ }
+ }
+
+ /** Handles a request from the client, and returns the response, where relevant. */
+ @Nullable
+ private SkylarkDebuggingProtos.DebugEvent handleClientRequest(
+ SkylarkDebuggingProtos.DebugRequest request) {
+ long sequenceNumber = request.getSequenceNumber();
+ try {
+ switch (request.getPayloadCase()) {
+ case START_DEBUGGING:
+ threadHandler.resumeAllThreads();
+ return DebugEventHelper.startDebuggingResponse(sequenceNumber);
+ case LIST_THREADS:
+ return listThreads(sequenceNumber);
+ case LIST_FRAMES:
+ return listFrames(sequenceNumber, request.getListFrames());
+ case SET_BREAKPOINTS:
+ return setBreakpoints(sequenceNumber, request.getSetBreakpoints());
+ case CONTINUE_EXECUTION:
+ return continueExecution(sequenceNumber, request.getContinueExecution());
+ case PAUSE_THREAD:
+ return pauseThread(sequenceNumber, request.getPauseThread());
+ case EVALUATE:
+ return evaluate(sequenceNumber, request.getEvaluate());
+ case PAYLOAD_NOT_SET:
+ DebugEventHelper.error(sequenceNumber, "No request payload found");
+ }
+ return DebugEventHelper.error(
+ sequenceNumber, "Unhandled request type: " + request.getPayloadCase());
+ } catch (DebugRequestException e) {
+ return DebugEventHelper.error(sequenceNumber, e.getMessage());
+ }
+ }
+
+ /** Handles a {@code ListThreadsRequest} and returns its response. */
+ private SkylarkDebuggingProtos.DebugEvent listThreads(long sequenceNumber) {
+ return DebugEventHelper.listThreadsResponse(sequenceNumber, threadHandler.listThreads());
+ }
+
+ /** Handles a {@code ListFramesRequest} and returns its response. */
+ private SkylarkDebuggingProtos.DebugEvent listFrames(
+ long sequenceNumber, SkylarkDebuggingProtos.ListFramesRequest request)
+ throws DebugRequestException {
+ List<SkylarkDebuggingProtos.Frame> frames = threadHandler.listFrames(request.getThreadId());
+ return DebugEventHelper.listFramesResponse(sequenceNumber, frames);
+ }
+
+ /** Handles a {@code SetBreakpointsRequest} and returns its response. */
+ private SkylarkDebuggingProtos.DebugEvent setBreakpoints(
+ long sequenceNumber, SkylarkDebuggingProtos.SetBreakpointsRequest request) {
+ threadHandler.setBreakpoints(
+ request
+ .getBreakpointList()
+ .stream()
+ .filter(b -> b.getConditionCase() == ConditionCase.LOCATION)
+ .map(SkylarkDebuggingProtos.Breakpoint::getLocation)
+ .collect(toImmutableSet()));
+ return DebugEventHelper.setBreakpointsResponse(sequenceNumber);
+ }
+
+ /** Handles a {@code EvaluateRequest} and returns its response. */
+ private SkylarkDebuggingProtos.DebugEvent evaluate(
+ long sequenceNumber, SkylarkDebuggingProtos.EvaluateRequest request)
+ throws DebugRequestException {
+ return DebugEventHelper.evaluateResponse(
+ sequenceNumber, threadHandler.evaluate(request.getThreadId(), request.getExpression()));
+ }
+
+ /** Handles a {@code ContinueExecutionRequest} and returns its response. */
+ private SkylarkDebuggingProtos.DebugEvent continueExecution(
+ long sequenceNumber, SkylarkDebuggingProtos.ContinueExecutionRequest request)
+ throws DebugRequestException {
+ long threadId = request.getThreadId();
+ if (threadId == 0) {
+ threadHandler.resumeAllThreads();
+ return DebugEventHelper.continueExecutionResponse(sequenceNumber);
+ }
+ threadHandler.resumeThread(threadId, request.getStepping());
+ return DebugEventHelper.continueExecutionResponse(sequenceNumber);
+ }
+
+ private SkylarkDebuggingProtos.DebugEvent pauseThread(
+ long sequenceNumber, SkylarkDebuggingProtos.PauseThreadRequest request)
+ throws DebugRequestException {
+ long threadId = request.getThreadId();
+ if (threadId == 0) {
+ threadHandler.pauseAllThreads();
+ } else {
+ threadHandler.pauseThread(threadId);
+ }
+ return DebugEventHelper.pauseThreadResponse(sequenceNumber);
+ }
+
+ /** A subclass of {@link Eval} with debugging hooks. */
+ private final class DebugAwareEval extends Eval {
+
+ DebugAwareEval(Environment env) {
+ super(env);
+ }
+
+ @Override
+ public void exec(Statement st) throws EvalException, InterruptedException {
+ pauseIfNecessary(env, st.getLocation());
+ super.exec(st);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skylarkdebug/server/ThreadHandler.java b/src/main/java/com/google/devtools/build/lib/skylarkdebug/server/ThreadHandler.java
new file mode 100644
index 0000000000..459ebae848
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skylarkdebug/server/ThreadHandler.java
@@ -0,0 +1,334 @@
+// Copyright 2018 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.devtools.build.lib.skylarkdebug.server;
+
+import static com.google.common.collect.ImmutableList.toImmutableList;
+import static com.google.common.collect.ImmutableSet.toImmutableSet;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.devtools.build.lib.events.Location;
+import com.google.devtools.build.lib.skylarkdebugging.SkylarkDebuggingProtos;
+import com.google.devtools.build.lib.syntax.Debuggable;
+import com.google.devtools.build.lib.syntax.Debuggable.ReadyToPause;
+import com.google.devtools.build.lib.syntax.Environment;
+import com.google.devtools.build.lib.syntax.EvalException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.Semaphore;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.GuardedBy;
+
+/** Handles all thread-related state and debugging tasks. */
+final class ThreadHandler {
+
+ private static class ThreadState {
+ final long id;
+ final String name;
+ final Debuggable debuggable;
+ /** Non-null if the thread is currently paused. */
+ @Nullable volatile PausedThreadState pausedState;
+ /** Determines when execution should next be paused. Non-null if currently stepping. */
+ @Nullable volatile ReadyToPause readyToPause;
+
+ ThreadState(
+ long id,
+ String name,
+ Debuggable debuggable,
+ @Nullable PausedThreadState pausedState,
+ @Nullable ReadyToPause readyToPause) {
+ this.id = id;
+ this.name = name;
+ this.debuggable = debuggable;
+ this.pausedState = pausedState;
+ this.readyToPause = readyToPause;
+ }
+ }
+
+ /** Information about a paused thread. */
+ private static class PausedThreadState {
+
+ /** The {@link Location} where execution is currently paused. */
+ final Location location;
+
+ /** Used to block execution of threads */
+ final Semaphore semaphore;
+
+ PausedThreadState(Location location) {
+ this.location = location;
+ this.semaphore = new Semaphore(0);
+ }
+ }
+
+ /**
+ * If true, all threads will pause at the earliest possible opportunity. New threads will also be
+ * immediately paused upon creation.
+ *
+ * <p>The debugger starts with all threads paused, until a StartDebuggingRequest is received.
+ */
+ private volatile boolean pausingAllThreads = true;
+
+ /** A map from thread identifiers to their state info. */
+ @GuardedBy("itself")
+ private final Map<Long, ThreadState> threads = new HashMap<>();
+
+ /** All location-based breakpoints (the only type of breakpoint currently supported). */
+ private volatile ImmutableSet<SkylarkDebuggingProtos.Location> breakpoints = ImmutableSet.of();
+
+ /**
+ * Threads which are set to be paused in the next checked execution step.
+ *
+ * <p>Invariant: Every thread id in this set is also in {@link #threads}, provided that we are not
+ * in a synchronized block on that map.
+ */
+ private final Set<Long> threadsToPause = ConcurrentHashMap.newKeySet();
+
+ /** Registers a Skylark thread with the {@link ThreadHandler}. */
+ void registerThread(long threadId, String threadName, Debuggable debuggable) {
+ doRegisterThread(threadId, threadName, debuggable);
+ }
+
+ private ThreadState doRegisterThread(long threadId, String threadName, Debuggable debuggable) {
+ ThreadState thread = new ThreadState(threadId, threadName, debuggable, null, null);
+ synchronized (threads) {
+ threads.put(threadId, thread);
+ }
+ return thread;
+ }
+
+ /** Mark all current and future threads paused. Will take effect asynchronously. */
+ void pauseAllThreads() {
+ synchronized (threads) {
+ threadsToPause.addAll(threads.keySet());
+ }
+ pausingAllThreads = true;
+ }
+
+ /** Mark the given thread paused. Will take effect asynchronously. */
+ void pauseThread(long threadId) throws DebugRequestException {
+ synchronized (threads) {
+ if (!threads.containsKey(threadId)) {
+ throw new DebugRequestException("Unknown thread: " + threadId);
+ }
+ threadsToPause.add(threadId);
+ }
+ }
+
+ /** Called when Skylark execution for this thread is complete. */
+ void unregisterThread(long threadId) {
+ synchronized (threads) {
+ threads.remove(threadId);
+ threadsToPause.remove(threadId);
+ }
+ }
+
+ void setBreakpoints(ImmutableSet<SkylarkDebuggingProtos.Location> breakpoints) {
+ // all breakpoints cover the entire line, so unset the column number.
+ this.breakpoints =
+ breakpoints
+ .stream()
+ .map(location -> location.toBuilder().clearColumnNumber().build())
+ .collect(toImmutableSet());
+ }
+
+ /** Resumes all threads. */
+ void resumeAllThreads() {
+ threadsToPause.clear();
+ pausingAllThreads = false;
+ synchronized (threads) {
+ for (ThreadState thread : threads.values()) {
+ // continue-all doesn't support stepping.
+ resumeThread(thread, SkylarkDebuggingProtos.Stepping.NONE);
+ }
+ }
+ }
+
+ /**
+ * Unpauses the given thread if it is currently paused. Also unsets {@link #pausingAllThreads}.
+ */
+ void resumeThread(long threadId, SkylarkDebuggingProtos.Stepping stepping)
+ throws DebugRequestException {
+ synchronized (threads) {
+ ThreadState thread = threads.get(threadId);
+ if (thread == null) {
+ throw new DebugRequestException(String.format("Thread %s is not running.", threadId));
+ }
+ if (thread.pausedState == null) {
+ throw new DebugRequestException(String.format("Thread %s is not paused.", threadId));
+ }
+ resumeThread(thread, stepping);
+ }
+ }
+
+ /**
+ * Unpauses the given thread if it is currently paused. Also unsets {@link #pausingAllThreads}.
+ */
+ @GuardedBy("threads")
+ private void resumeThread(ThreadState thread, SkylarkDebuggingProtos.Stepping stepping) {
+ PausedThreadState pausedState = thread.pausedState;
+ if (pausedState == null) {
+ return;
+ }
+ // once the user has resumed any thread, don't continue pausing future threads
+ pausingAllThreads = false;
+ thread.readyToPause =
+ thread.debuggable.stepControl(DebugEventHelper.convertSteppingEnum(stepping));
+ pausedState.semaphore.release();
+ thread.pausedState = null;
+ }
+
+ void pauseIfNecessary(Environment env, Location location, DebugServerTransport transport) {
+ if (shouldPauseCurrentThread(env, location)) {
+ pauseCurrentThread(env, location, transport);
+ }
+ }
+
+ ImmutableList<SkylarkDebuggingProtos.Thread> listThreads() {
+ ImmutableList.Builder<SkylarkDebuggingProtos.Thread> list = ImmutableList.builder();
+ synchronized (threads) {
+ for (ThreadState thread : threads.values()) {
+ list.add(getThreadProto(thread));
+ }
+ }
+ return list.build();
+ }
+
+ /** Handles a {@code ListFramesRequest} and returns its response. */
+ ImmutableList<SkylarkDebuggingProtos.Frame> listFrames(long threadId)
+ throws DebugRequestException {
+ Debuggable debuggable;
+ PausedThreadState pausedState;
+ synchronized (threads) {
+ ThreadState thread = threads.get(threadId);
+ if (thread == null) {
+ throw new DebugRequestException(String.format("Thread %s is not running.", threadId));
+ }
+ pausedState = thread.pausedState;
+ if (pausedState == null) {
+ throw new DebugRequestException(String.format("Thread %s is not paused.", threadId));
+ }
+ debuggable = thread.debuggable;
+ }
+ // no need to list frames within the synchronize block: threads can only be resumed in response
+ // to a client request, and requests are handled serially
+ return debuggable
+ .listFrames(pausedState.location)
+ .stream()
+ .map(DebugEventHelper::getFrameProto)
+ .collect(toImmutableList());
+ }
+
+ SkylarkDebuggingProtos.Value evaluate(long threadId, String expression)
+ throws DebugRequestException {
+ Debuggable debuggable;
+ synchronized (threads) {
+ ThreadState thread = threads.get(threadId);
+ if (thread == null) {
+ throw new DebugRequestException(String.format("Thread %s is not running.", threadId));
+ }
+ if (thread.pausedState == null) {
+ throw new DebugRequestException(String.format("Thread %s is not paused.", threadId));
+ }
+ debuggable = thread.debuggable;
+ }
+ // no need to evaluate within the synchronize block: threads can only be resumed in response
+ // to a client request, and requests are handled serially
+ try {
+ Object result = debuggable.evaluate(expression);
+ return DebuggerSerialization.getValueProto("Evaluation result", result);
+ } catch (EvalException | InterruptedException e) {
+ throw new DebugRequestException(e.getMessage());
+ }
+ }
+
+ /**
+ * Pauses the current thread's execution, blocking until it's resumed via a
+ * ContinueExecutionRequest.
+ */
+ private void pauseCurrentThread(
+ Environment env, Location location, DebugServerTransport transport) {
+ long threadId = Thread.currentThread().getId();
+
+ SkylarkDebuggingProtos.Thread threadProto;
+ PausedThreadState pausedState;
+ synchronized (threads) {
+ ThreadState thread = threads.get(threadId);
+ if (thread == null) {
+ // this skylark entry point didn't call DebugServer#runWithDebugging. Now that we've hit a
+ // breakpoint, register it anyway.
+ // TODO(bazel-team): once all skylark evaluation routes through
+ // DebugServer#runWithDebugging, report an error here instead
+ String fallbackThreadName = "Untracked thread: " + threadId;
+ transport.postEvent(DebugEventHelper.threadStartedEvent(threadId, fallbackThreadName));
+ thread = doRegisterThread(threadId, fallbackThreadName, env);
+ }
+ threadProto = getThreadProto(thread);
+ pausedState = new PausedThreadState(location);
+ thread.pausedState = pausedState;
+ }
+
+ transport.postEvent(DebugEventHelper.threadPausedEvent(threadProto));
+ pausedState.semaphore.acquireUninterruptibly();
+ transport.postEvent(DebugEventHelper.threadContinuedEvent(threadProto));
+ }
+
+ private boolean shouldPauseCurrentThread(Environment env, Location location) {
+ long threadId = Thread.currentThread().getId();
+ if (threadsToPause.remove(threadId) || pausingAllThreads) {
+ return true;
+ }
+ if (hasBreakpointAtLocation(location)) {
+ return true;
+ }
+
+ // TODO(bazel-team): if contention becomes a problem, consider changing 'threads' to a
+ // concurrent map, and synchronizing on individual entries
+ synchronized (threads) {
+ ThreadState thread = threads.get(threadId);
+ if (thread != null && thread.readyToPause != null && thread.readyToPause.test(env)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean hasBreakpointAtLocation(Location location) {
+ // breakpoints is volatile, so taking a local copy
+ ImmutableSet<SkylarkDebuggingProtos.Location> breakpoints = this.breakpoints;
+ if (breakpoints.isEmpty()) {
+ return false;
+ }
+ SkylarkDebuggingProtos.Location locationProto = DebugEventHelper.getLocationProto(location);
+ // column data ignored for breakpoints
+ return locationProto != null
+ && breakpoints.contains(locationProto.toBuilder().clearColumnNumber().build());
+ }
+
+ /** Returns a {@code Thread} proto builder with information about the given thread. */
+ private static SkylarkDebuggingProtos.Thread getThreadProto(ThreadState thread) {
+ SkylarkDebuggingProtos.Thread.Builder builder =
+ SkylarkDebuggingProtos.Thread.newBuilder().setId(thread.id).setName(thread.name);
+
+ PausedThreadState pausedState = thread.pausedState;
+ if (pausedState != null) {
+ builder
+ .setIsPaused(true)
+ .setLocation(DebugEventHelper.getLocationProto(pausedState.location));
+ }
+ return builder.build();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/DebugServer.java b/src/main/java/com/google/devtools/build/lib/syntax/DebugServer.java
index f462550797..0238e51959 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/DebugServer.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/DebugServer.java
@@ -14,11 +14,14 @@
package com.google.devtools.build.lib.syntax;
+import java.util.function.Function;
+
/** A debug server interface, called from core skylark code. */
public interface DebugServer {
/**
- * Tracks the execution of the given callable object in the debug server.
+ * Executes the given callable and returns its result, while making any skylark evaluation visible
+ * to the debugger. This method should be used to evaluate all debuggable Skylark code.
*
* @param env the Skylark execution environment
* @param threadName the descriptive name of the thread
@@ -29,6 +32,15 @@ public interface DebugServer {
<T> T runWithDebugging(Environment env, String threadName, DebugCallable<T> callable)
throws EvalException, InterruptedException;
+ /** Shuts down the debug server, closing any open sockets, etc. */
+ void close();
+
+ /**
+ * Returns a custom {@link Eval} supplier used to intercept statement execution to check for
+ * breakpoints.
+ */
+ Function<Environment, Eval> evalOverride();
+
/** Represents an invocation that will be tracked as a thread by the Skylark debug server. */
interface DebugCallable<T> {
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/DebugServerUtils.java b/src/main/java/com/google/devtools/build/lib/syntax/DebugServerUtils.java
index f653cd994f..808d10704a 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/DebugServerUtils.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/DebugServerUtils.java
@@ -16,10 +16,14 @@ package com.google.devtools.build.lib.syntax;
import com.google.devtools.build.lib.syntax.DebugServer.DebugCallable;
import java.util.concurrent.atomic.AtomicReference;
-import java.util.function.Function;
import java.util.function.Supplier;
-/** A helper class for enabling/disabling skylark debugging. */
+/**
+ * A helper class for enabling/disabling skylark debugging.
+ *
+ * <p>{@code runWithDebuggingIfEnabled} must be called after {@code initializeDebugServer}, and
+ * before {@code disableDebugging}.
+ */
public final class DebugServerUtils {
private DebugServerUtils() {}
@@ -30,15 +34,20 @@ public final class DebugServerUtils {
* Called at the start of a debuggable skylark session to enable debugging. The custom {@link
* Eval} supplier provided should intercept statement execution to check for breakpoints.
*/
- public static void initializeDebugServer(
- DebugServer server, Function<Environment, Eval> evalOverride) {
+ public static void initializeDebugServer(DebugServer server) {
instance.set(server);
- Eval.setEvalSupplier(evalOverride);
+ Eval.setEvalSupplier(server.evalOverride());
}
- /** Called at the end of a debuggable skylark session to disable debugging. */
+ /**
+ * Called at the end of a debuggable skylark session to shut down the debug server and disable
+ * debugging.
+ */
public static void disableDebugging() {
- instance.set(null);
+ DebugServer server = instance.getAndSet(null);
+ if (server != null) {
+ server.close();
+ }
Eval.removeCustomEval();
}
diff --git a/src/test/java/com/google/devtools/build/lib/skylarkdebug/server/BUILD b/src/test/java/com/google/devtools/build/lib/skylarkdebug/server/BUILD
index 5d9f881575..7f2745a9df 100644
--- a/src/test/java/com/google/devtools/build/lib/skylarkdebug/server/BUILD
+++ b/src/test/java/com/google/devtools/build/lib/skylarkdebug/server/BUILD
@@ -10,6 +10,55 @@ filegroup(
visibility = ["//src/test/java/com/google/devtools/build/lib:__pkg__"],
)
+java_library(
+ name = "utils",
+ srcs = ["MockDebugClient.java"],
+ deps = [
+ "//src/main/java/com/google/devtools/build/lib/skylarkdebug/proto:skylark_debugging_java_proto",
+ "//src/main/java/com/google/devtools/build/lib/skylarkdebug/server",
+ "//third_party:jsr305",
+ ],
+)
+
+java_test(
+ name = "SkylarkDebugServerTest",
+ size = "medium",
+ srcs = ["SkylarkDebugServerTest.java"],
+ deps = [
+ ":utils",
+ "//src/main/java/com/google/devtools/build/lib:events",
+ "//src/main/java/com/google/devtools/build/lib:syntax",
+ "//src/main/java/com/google/devtools/build/lib/cmdline",
+ "//src/main/java/com/google/devtools/build/lib/collect/nestedset",
+ "//src/main/java/com/google/devtools/build/lib/skylarkdebug/proto:skylark_debugging_java_proto",
+ "//src/main/java/com/google/devtools/build/lib/skylarkdebug/server",
+ "//src/main/java/com/google/devtools/build/lib/vfs",
+ "//src/test/java/com/google/devtools/build/lib:foundations_testutil",
+ "//src/test/java/com/google/devtools/build/lib:testutil",
+ "//third_party:guava",
+ "//third_party:jsr305",
+ "//third_party:junit4",
+ "//third_party:truth",
+ ],
+)
+
+java_test(
+ name = "DebugServerTransportTest",
+ srcs = ["DebugServerTransportTest.java"],
+ deps = [
+ "//src/main/java/com/google/devtools/build/lib:events",
+ "//src/main/java/com/google/devtools/build/lib:syntax",
+ "//src/main/java/com/google/devtools/build/lib/collect/nestedset",
+ "//src/main/java/com/google/devtools/build/lib/skylarkdebug/proto:skylark_debugging_java_proto",
+ "//src/main/java/com/google/devtools/build/lib/skylarkdebug/server",
+ "//src/test/java/com/google/devtools/build/lib:foundations_testutil",
+ "//third_party:guava",
+ "//third_party:jsr305",
+ "//third_party:junit4",
+ "//third_party:truth",
+ ],
+)
+
java_test(
name = "DebuggerSerializationTest",
srcs = glob(["DebuggerSerializationTest.java"]),
diff --git a/src/test/java/com/google/devtools/build/lib/skylarkdebug/server/DebugServerTransportTest.java b/src/test/java/com/google/devtools/build/lib/skylarkdebug/server/DebugServerTransportTest.java
new file mode 100644
index 0000000000..c2b6b4e414
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/skylarkdebug/server/DebugServerTransportTest.java
@@ -0,0 +1,134 @@
+// Copyright 2018 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.devtools.build.lib.skylarkdebug.server;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.devtools.build.lib.events.EventKind;
+import com.google.devtools.build.lib.events.util.EventCollectionApparatus;
+import com.google.devtools.build.lib.skylarkdebugging.SkylarkDebuggingProtos.ContinueExecutionResponse;
+import com.google.devtools.build.lib.skylarkdebugging.SkylarkDebuggingProtos.DebugEvent;
+import com.google.devtools.build.lib.skylarkdebugging.SkylarkDebuggingProtos.DebugRequest;
+import com.google.devtools.build.lib.skylarkdebugging.SkylarkDebuggingProtos.StartDebuggingRequest;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link DebugServerTransport}. */
+@RunWith(JUnit4.class)
+public class DebugServerTransportTest {
+
+ private static final ExecutorService executor = Executors.newFixedThreadPool(1);
+
+ private final EventCollectionApparatus events =
+ new EventCollectionApparatus(EventKind.ALL_EVENTS);
+
+ /** A simple debug client for testing purposes. */
+ private static class MockDebugClient {
+
+ Socket clientSocket;
+
+ void connect(Duration timeout, ServerSocket serverSocket) {
+ long startTimeMillis = System.currentTimeMillis();
+ IOException exception = null;
+ while (System.currentTimeMillis() - startTimeMillis < timeout.toMillis()) {
+ try {
+ clientSocket = new Socket();
+ clientSocket.connect(
+ new InetSocketAddress(serverSocket.getInetAddress(), serverSocket.getLocalPort()),
+ 100);
+ return;
+ } catch (IOException e) {
+ exception = e;
+ }
+ }
+ throw new RuntimeException("Couldn't connect to the debug server", exception);
+ }
+
+ List<DebugEvent> readEvents() throws Exception {
+ List<DebugEvent> events = new ArrayList<>();
+ while (clientSocket.getInputStream().available() != 0) {
+ events.add(DebugEvent.parseDelimitedFrom(clientSocket.getInputStream()));
+ }
+ return events;
+ }
+
+ void sendRequest(DebugRequest request) throws IOException {
+ request.writeDelimitedTo(clientSocket.getOutputStream());
+ }
+ }
+
+ @Before
+ public void setup() {
+ events.setFailFast(true);
+ }
+
+ @Test
+ public void testConnectAndReceiveRequest() throws Exception {
+ ServerSocket serverSocket = new ServerSocket(0, 1, InetAddress.getByName(null));
+ Future<DebugServerTransport> future =
+ executor.submit(
+ () -> DebugServerTransport.createAndWaitForClient(events.reporter(), serverSocket));
+ MockDebugClient client = new MockDebugClient();
+ client.connect(Duration.ofSeconds(10), serverSocket);
+
+ DebugServerTransport serverTransport = future.get(10, TimeUnit.SECONDS);
+ assertThat(serverTransport).isNotNull();
+ DebugRequest request =
+ DebugRequest.newBuilder()
+ .setSequenceNumber(10)
+ .setStartDebugging(StartDebuggingRequest.getDefaultInstance())
+ .build();
+ client.sendRequest(request);
+
+ assertThat(serverTransport.readClientRequest()).isEqualTo(request);
+ serverTransport.close();
+ }
+
+ @Test
+ public void testConnectAndPostEvent() throws Exception {
+ ServerSocket serverSocket = new ServerSocket(0, 1, InetAddress.getByName(null));
+ Future<DebugServerTransport> future =
+ executor.submit(
+ () -> DebugServerTransport.createAndWaitForClient(events.reporter(), serverSocket));
+ MockDebugClient client = new MockDebugClient();
+ client.connect(Duration.ofSeconds(10), serverSocket);
+
+ DebugServerTransport serverTransport = future.get(10, TimeUnit.SECONDS);
+ assertThat(serverTransport).isNotNull();
+ DebugEvent event =
+ DebugEvent.newBuilder()
+ .setSequenceNumber(10)
+ .setContinueExecution(ContinueExecutionResponse.getDefaultInstance())
+ .build();
+ serverTransport.postEvent(event);
+
+ assertThat(client.readEvents()).containsExactly(event);
+ serverTransport.close();
+ }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/skylarkdebug/server/MockDebugClient.java b/src/test/java/com/google/devtools/build/lib/skylarkdebug/server/MockDebugClient.java
new file mode 100644
index 0000000000..49f197d8d8
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/skylarkdebug/server/MockDebugClient.java
@@ -0,0 +1,157 @@
+// Copyright 2018 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.devtools.build.lib.skylarkdebug.server;
+
+import com.google.devtools.build.lib.skylarkdebugging.SkylarkDebuggingProtos.DebugEvent;
+import com.google.devtools.build.lib.skylarkdebugging.SkylarkDebuggingProtos.DebugRequest;
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.function.Predicate;
+import javax.annotation.Nullable;
+
+/** A basic implementation of a skylark debugging client, for use in integration tests. */
+class MockDebugClient {
+
+ private static final int RESPONSE_TIMEOUT_MILLIS = 10000;
+ private static final ExecutorService readTaskExecutor = Executors.newFixedThreadPool(1);
+
+ private Socket clientSocket;
+
+ final List<DebugEvent> unnumberedEvents = new ArrayList<>();
+ final Map<Long, DebugEvent> responses = new HashMap<>();
+
+ private Future<?> readTask;
+
+ /** Connects to the debug server, and starts listening for events. */
+ void connect(ServerSocket serverSocket, Duration timeout) {
+ long startTimeMillis = System.currentTimeMillis();
+ IOException exception = null;
+ while (System.currentTimeMillis() - startTimeMillis < timeout.toMillis()) {
+ try {
+ clientSocket = new Socket();
+ clientSocket.connect(
+ new InetSocketAddress(serverSocket.getInetAddress(), serverSocket.getLocalPort()), 100);
+ readTask =
+ readTaskExecutor.submit(
+ () -> {
+ while (true) {
+ eventReceived(DebugEvent.parseDelimitedFrom(clientSocket.getInputStream()));
+ }
+ });
+ return;
+ } catch (IOException e) {
+ exception = e;
+ }
+ }
+ throw new RuntimeException("Couldn't connect to the debug server", exception);
+ }
+
+ void close() throws IOException {
+ if (clientSocket != null) {
+ clientSocket.close();
+ }
+ if (readTask != null) {
+ readTask.cancel(true);
+ }
+ }
+
+ /**
+ * Blocks waiting for an unnumbered event (not a direct response to a request). Returns null if no
+ * event arrives before the timeout.
+ */
+ @Nullable
+ DebugEvent waitForEvent(Predicate<DebugEvent> predicate, Duration timeout) {
+ long startTime = System.currentTimeMillis();
+ synchronized (unnumberedEvents) {
+ while (unnumberedEvents.stream().noneMatch(predicate)
+ && System.currentTimeMillis() - startTime < timeout.toMillis()) {
+ try {
+ unnumberedEvents.wait(timeout.toMillis());
+ } catch (InterruptedException e) {
+ throw new AssertionError(e);
+ }
+ }
+ }
+ return unnumberedEvents.stream().filter(predicate).findFirst().orElse(null);
+ }
+
+ /**
+ * Sends a {@link DebugRequest} to the server, and blocks waiting for a response.
+ *
+ * @return the {@link DebugEvent} response from the server, or null if no response was received.
+ */
+ @Nullable
+ DebugEvent sendRequestAndWaitForResponse(DebugRequest request) throws IOException {
+ request.writeDelimitedTo(clientSocket.getOutputStream());
+ clientSocket.getOutputStream().flush();
+ return waitForResponse(request.getSequenceNumber());
+ }
+
+ private void eventReceived(DebugEvent event) {
+ if (event.getSequenceNumber() == 0) {
+ synchronized (unnumberedEvents) {
+ unnumberedEvents.add(event);
+ unnumberedEvents.notifyAll();
+ }
+ return;
+ }
+ synchronized (responses) {
+ DebugEvent existing = responses.put(event.getSequenceNumber(), event);
+ if (existing != null) {
+ throw new AssertionError(
+ "There's already an event in the response queue corresponding to sequence number "
+ + event.getSequenceNumber());
+ }
+ responses.notifyAll();
+ }
+ }
+
+ /**
+ * Wait for a response from the debug server. Returns null if no response was received, or this
+ * thread was interrupted.
+ */
+ @Nullable
+ private DebugEvent waitForResponse(long sequence) {
+ DebugEvent response = null;
+ long startTime = System.currentTimeMillis();
+ synchronized (responses) {
+ while (response == null && shouldWaitForResponse(startTime)) {
+ try {
+ responses.wait(1000);
+ } catch (InterruptedException e) {
+ throw new AssertionError(e);
+ }
+ response = responses.remove(sequence);
+ }
+ }
+ return response;
+ }
+
+ private boolean shouldWaitForResponse(long startTime) {
+ return clientSocket.isConnected()
+ && !readTask.isDone()
+ && System.currentTimeMillis() - startTime < RESPONSE_TIMEOUT_MILLIS;
+ }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/skylarkdebug/server/SkylarkDebugServerTest.java b/src/test/java/com/google/devtools/build/lib/skylarkdebug/server/SkylarkDebugServerTest.java
new file mode 100644
index 0000000000..d6674931fb
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/skylarkdebug/server/SkylarkDebugServerTest.java
@@ -0,0 +1,572 @@
+// Copyright 2018 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.devtools.build.lib.skylarkdebug.server;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.events.EventKind;
+import com.google.devtools.build.lib.events.util.EventCollectionApparatus;
+import com.google.devtools.build.lib.skylarkdebugging.SkylarkDebuggingProtos;
+import com.google.devtools.build.lib.skylarkdebugging.SkylarkDebuggingProtos.Breakpoint;
+import com.google.devtools.build.lib.skylarkdebugging.SkylarkDebuggingProtos.ContinueExecutionRequest;
+import com.google.devtools.build.lib.skylarkdebugging.SkylarkDebuggingProtos.DebugEvent;
+import com.google.devtools.build.lib.skylarkdebugging.SkylarkDebuggingProtos.DebugRequest;
+import com.google.devtools.build.lib.skylarkdebugging.SkylarkDebuggingProtos.EvaluateRequest;
+import com.google.devtools.build.lib.skylarkdebugging.SkylarkDebuggingProtos.Frame;
+import com.google.devtools.build.lib.skylarkdebugging.SkylarkDebuggingProtos.ListFramesRequest;
+import com.google.devtools.build.lib.skylarkdebugging.SkylarkDebuggingProtos.ListFramesResponse;
+import com.google.devtools.build.lib.skylarkdebugging.SkylarkDebuggingProtos.ListThreadsRequest;
+import com.google.devtools.build.lib.skylarkdebugging.SkylarkDebuggingProtos.ListThreadsResponse;
+import com.google.devtools.build.lib.skylarkdebugging.SkylarkDebuggingProtos.Location;
+import com.google.devtools.build.lib.skylarkdebugging.SkylarkDebuggingProtos.Scope;
+import com.google.devtools.build.lib.skylarkdebugging.SkylarkDebuggingProtos.SetBreakpointsRequest;
+import com.google.devtools.build.lib.skylarkdebugging.SkylarkDebuggingProtos.StartDebuggingRequest;
+import com.google.devtools.build.lib.skylarkdebugging.SkylarkDebuggingProtos.StartDebuggingResponse;
+import com.google.devtools.build.lib.skylarkdebugging.SkylarkDebuggingProtos.Stepping;
+import com.google.devtools.build.lib.syntax.BuildFileAST;
+import com.google.devtools.build.lib.syntax.DebugServerUtils;
+import com.google.devtools.build.lib.syntax.Environment;
+import com.google.devtools.build.lib.syntax.Mutability;
+import com.google.devtools.build.lib.syntax.ParserInputSource;
+import com.google.devtools.build.lib.syntax.SkylarkList;
+import com.google.devtools.build.lib.testutil.Scratch;
+import com.google.devtools.build.lib.vfs.FileSystemUtils;
+import com.google.devtools.build.lib.vfs.Path;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.ServerSocket;
+import java.time.Duration;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Integration tests for {@link SkylarkDebugServer}. */
+@RunWith(JUnit4.class)
+public class SkylarkDebugServerTest {
+
+ private final ExecutorService executor = Executors.newFixedThreadPool(2);
+ private final Scratch scratch = new Scratch();
+ private final EventCollectionApparatus events =
+ new EventCollectionApparatus(EventKind.ALL_EVENTS);
+
+ private MockDebugClient client;
+ private SkylarkDebugServer server;
+
+ @Before
+ public void setUpServerAndClient() throws Exception {
+ ServerSocket serverSocket = new ServerSocket(0, 1, InetAddress.getByName(null));
+ Future<SkylarkDebugServer> future =
+ executor.submit(
+ () -> SkylarkDebugServer.createAndWaitForConnection(events.reporter(), serverSocket));
+ client = new MockDebugClient();
+ client.connect(serverSocket, Duration.ofSeconds(10));
+
+ server = future.get(10, TimeUnit.SECONDS);
+ assertThat(server).isNotNull();
+ DebugServerUtils.initializeDebugServer(server);
+ }
+
+ @After
+ public void shutDown() throws Exception {
+ if (client != null) {
+ client.close();
+ }
+ if (server != null) {
+ server.close();
+ }
+ }
+
+ @Test
+ public void testStartDebuggingResponseReceived() throws Exception {
+ DebugEvent response =
+ client.sendRequestAndWaitForResponse(
+ DebugRequest.newBuilder()
+ .setSequenceNumber(1)
+ .setStartDebugging(StartDebuggingRequest.newBuilder())
+ .build());
+ assertThat(response)
+ .isEqualTo(
+ DebugEvent.newBuilder()
+ .setSequenceNumber(1)
+ .setStartDebugging(StartDebuggingResponse.newBuilder().build())
+ .build());
+ }
+
+ @Test
+ public void testThreadRegisteredEvents() throws Exception {
+ sendStartDebuggingRequest();
+ String threadName = Thread.currentThread().getName();
+ long threadId = Thread.currentThread().getId();
+ DebugServerUtils.runWithDebuggingIfEnabled(newEnvironment(), () -> threadName, () -> true);
+
+ client.waitForEvent(DebugEvent::hasThreadEnded, Duration.ofSeconds(5));
+
+ assertThat(client.unnumberedEvents)
+ .containsExactly(
+ DebugEventHelper.threadStartedEvent(threadId, threadName),
+ DebugEventHelper.threadEndedEvent(threadId, threadName));
+ }
+
+ @Test
+ public void testPausedUntilStartDebuggingRequestReceived() throws Exception {
+ BuildFileAST buildFile = parseBuildFile("/a/build/file/BUILD", "x = [1,2,3]");
+ Environment env = newEnvironment();
+
+ Thread evaluationThread = execInWorkerThread(buildFile, env);
+ String threadName = evaluationThread.getName();
+ long threadId = evaluationThread.getId();
+
+ // wait for BUILD evaluation to start
+ client.waitForEvent(DebugEvent::hasThreadPaused, Duration.ofSeconds(5));
+
+ assertThat(listThreads().getThreadList())
+ .containsExactly(
+ SkylarkDebuggingProtos.Thread.newBuilder()
+ .setId(threadId)
+ .setName(threadName)
+ .setLocation(
+ DebugEventHelper.getLocationProto(
+ buildFile.getStatements().get(0).getLocation()))
+ .setIsPaused(true)
+ .build());
+
+ sendStartDebuggingRequest();
+ client.waitForEvent(DebugEvent::hasThreadEnded, Duration.ofSeconds(5));
+ assertThat(listThreads().getThreadList()).isEmpty();
+ assertThat(client.unnumberedEvents)
+ .containsAllOf(
+ DebugEventHelper.threadContinuedEvent(
+ SkylarkDebuggingProtos.Thread.newBuilder()
+ .setName(threadName)
+ .setId(threadId)
+ .build()),
+ DebugEventHelper.threadEndedEvent(threadId, threadName));
+ }
+
+ @Test
+ public void testPauseAtBreakpoint() throws Exception {
+ sendStartDebuggingRequest();
+ BuildFileAST buildFile = parseBuildFile("/a/build/file/BUILD", "x = [1,2,3]", "y = [2,3,4]");
+ Environment env = newEnvironment();
+
+ Location breakpoint =
+ Location.newBuilder().setLineNumber(2).setPath("/a/build/file/BUILD").build();
+ setBreakpoints(ImmutableList.of(breakpoint));
+
+ Thread evaluationThread = execInWorkerThread(buildFile, env);
+ String threadName = evaluationThread.getName();
+ long threadId = evaluationThread.getId();
+
+ // wait for breakpoint to be hit
+ client.waitForEvent(DebugEvent::hasThreadPaused, Duration.ofSeconds(5));
+
+ assertThat(listThreads().getThreadList())
+ .containsExactly(
+ SkylarkDebuggingProtos.Thread.newBuilder()
+ .setId(threadId)
+ .setName(threadName)
+ .setLocation(breakpoint.toBuilder().setColumnNumber(1))
+ .setIsPaused(true)
+ .build());
+ }
+
+ @Test
+ public void testListFramesForInvalidThread() throws Exception {
+ sendStartDebuggingRequest();
+ DebugEvent event =
+ client.sendRequestAndWaitForResponse(
+ DebugRequest.newBuilder()
+ .setSequenceNumber(1)
+ .setListFrames(ListFramesRequest.newBuilder().setThreadId(20).build())
+ .build());
+ assertThat(event.hasError()).isTrue();
+ assertThat(event.getError().getMessage()).contains("Thread 20 is not running");
+ }
+
+ @Test
+ public void testSimpleListFramesRequest() throws Exception {
+ sendStartDebuggingRequest();
+ BuildFileAST buildFile = parseBuildFile("/a/build/file/BUILD", "x = [1,2,3]", "y = [2,3,4]");
+ Environment env = newEnvironment();
+
+ Location breakpoint =
+ Location.newBuilder().setLineNumber(2).setPath("/a/build/file/BUILD").build();
+ setBreakpoints(ImmutableList.of(breakpoint));
+
+ Thread evaluationThread = execInWorkerThread(buildFile, env);
+ long threadId = evaluationThread.getId();
+
+ // wait for breakpoint to be hit
+ client.waitForEvent(DebugEvent::hasThreadPaused, Duration.ofSeconds(5));
+
+ ListFramesResponse frames = listFrames(threadId);
+ assertThat(frames.getFrameCount()).isEqualTo(1);
+ assertThat(frames.getFrame(0))
+ .isEqualTo(
+ Frame.newBuilder()
+ .setFunctionName("<top level>")
+ .setLocation(breakpoint.toBuilder().setColumnNumber(1))
+ .addScope(
+ Scope.newBuilder()
+ .setName("global")
+ .addBinding(
+ DebuggerSerialization.getValueProto(
+ "x", SkylarkList.createImmutable(ImmutableList.of(1, 2, 3)))))
+ .build());
+ }
+
+ @Test
+ public void testListFramesShadowedBinding() throws Exception {
+ sendStartDebuggingRequest();
+ BuildFileAST bzlFile =
+ parseSkylarkFile(
+ "/a/build/file/test.bzl",
+ "a = 1",
+ "c = 3",
+ "def fn():",
+ " a = 2",
+ " b = 1",
+ " b + 1",
+ "fn()");
+ Environment env = newEnvironment();
+
+ Location breakpoint =
+ Location.newBuilder().setPath("/a/build/file/test.bzl").setLineNumber(6).build();
+ setBreakpoints(ImmutableList.of(breakpoint));
+
+ Thread evaluationThread = execInWorkerThread(bzlFile, env);
+ long threadId = evaluationThread.getId();
+
+ // wait for breakpoint to be hit
+ client.waitForEvent(DebugEvent::hasThreadPaused, Duration.ofSeconds(5));
+
+ ListFramesResponse frames = listFrames(threadId);
+ assertThat(frames.getFrameCount()).isEqualTo(2);
+
+ assertThat(frames.getFrame(0))
+ .isEqualTo(
+ Frame.newBuilder()
+ .setFunctionName("fn")
+ .setLocation(breakpoint.toBuilder().setColumnNumber(3))
+ .addScope(
+ Scope.newBuilder()
+ .setName("local")
+ .addBinding(DebuggerSerialization.getValueProto("a", 2))
+ .addBinding(DebuggerSerialization.getValueProto("b", 1)))
+ .addScope(
+ Scope.newBuilder()
+ .setName("global")
+ .addBinding(DebuggerSerialization.getValueProto("c", 3))
+ .addBinding(DebuggerSerialization.getValueProto("fn", env.lookup("fn"))))
+ .build());
+
+ assertThat(frames.getFrame(1))
+ .isEqualTo(
+ Frame.newBuilder()
+ .setFunctionName("<top level>")
+ .setLocation(
+ Location.newBuilder()
+ .setPath("/a/build/file/test.bzl")
+ .setLineNumber(7)
+ .setColumnNumber(1))
+ .addScope(
+ Scope.newBuilder()
+ .setName("global")
+ .addBinding(DebuggerSerialization.getValueProto("a", 1))
+ .addBinding(DebuggerSerialization.getValueProto("c", 3))
+ .addBinding(DebuggerSerialization.getValueProto("fn", env.lookup("fn"))))
+ .build());
+ }
+
+ @Test
+ public void testEvaluateRequest() throws Exception {
+ sendStartDebuggingRequest();
+ BuildFileAST buildFile = parseBuildFile("/a/build/file/BUILD", "x = [1,2,3]", "y = [2,3,4]");
+ Environment env = newEnvironment();
+
+ Location breakpoint =
+ Location.newBuilder().setLineNumber(2).setPath("/a/build/file/BUILD").build();
+ setBreakpoints(ImmutableList.of(breakpoint));
+
+ Thread evaluationThread = execInWorkerThread(buildFile, env);
+ long threadId = evaluationThread.getId();
+
+ // wait for breakpoint to be hit
+ client.waitForEvent(DebugEvent::hasThreadPaused, Duration.ofSeconds(5));
+
+ DebugEvent response =
+ client.sendRequestAndWaitForResponse(
+ DebugRequest.newBuilder()
+ .setSequenceNumber(123)
+ .setEvaluate(
+ EvaluateRequest.newBuilder()
+ .setThreadId(threadId)
+ .setExpression("x[1]")
+ .build())
+ .build());
+ assertThat(response.hasEvaluate()).isTrue();
+ assertThat(response.getEvaluate().getResult())
+ .isEqualTo(DebuggerSerialization.getValueProto("Evaluation result", 2));
+ }
+
+ @Test
+ public void testEvaluateRequestThrowingException() throws Exception {
+ sendStartDebuggingRequest();
+ BuildFileAST buildFile = parseBuildFile("/a/build/file/BUILD", "x = [1,2,3]", "y = [2,3,4]");
+ Environment env = newEnvironment();
+
+ Location breakpoint =
+ Location.newBuilder().setLineNumber(2).setPath("/a/build/file/BUILD").build();
+ setBreakpoints(ImmutableList.of(breakpoint));
+
+ Thread evaluationThread = execInWorkerThread(buildFile, env);
+ long threadId = evaluationThread.getId();
+
+ // wait for breakpoint to be hit
+ client.waitForEvent(DebugEvent::hasThreadPaused, Duration.ofSeconds(5));
+
+ DebugEvent response =
+ client.sendRequestAndWaitForResponse(
+ DebugRequest.newBuilder()
+ .setSequenceNumber(123)
+ .setEvaluate(
+ EvaluateRequest.newBuilder()
+ .setThreadId(threadId)
+ .setExpression("z[0]")
+ .build())
+ .build());
+ assertThat(response.hasError()).isTrue();
+ assertThat(response.getError().getMessage()).isEqualTo("name 'z' is not defined");
+ }
+
+ @Test
+ public void testStepIntoFunction() throws Exception {
+ sendStartDebuggingRequest();
+ BuildFileAST bzlFile =
+ parseSkylarkFile(
+ "/a/build/file/test.bzl",
+ "def fn():",
+ " a = 2",
+ " return a",
+ "x = fn()",
+ "y = [2,3,4]");
+ Environment env = newEnvironment();
+
+ Location breakpoint =
+ Location.newBuilder().setLineNumber(4).setPath("/a/build/file/test.bzl").build();
+ setBreakpoints(ImmutableList.of(breakpoint));
+
+ Thread evaluationThread = execInWorkerThread(bzlFile, env);
+ long threadId = evaluationThread.getId();
+
+ // wait for breakpoint to be hit
+ client.waitForEvent(DebugEvent::hasThreadPaused, Duration.ofSeconds(5));
+
+ assertThat(listThreads().getThread(0).getLocation().getLineNumber()).isEqualTo(4);
+
+ client.unnumberedEvents.clear();
+ client.sendRequestAndWaitForResponse(
+ DebugRequest.newBuilder()
+ .setSequenceNumber(2)
+ .setContinueExecution(
+ ContinueExecutionRequest.newBuilder()
+ .setThreadId(threadId)
+ .setStepping(Stepping.INTO)
+ .build())
+ .build());
+ client.waitForEvent(DebugEvent::hasThreadPaused, Duration.ofSeconds(5));
+
+ // check we're paused inside the function
+ assertThat(listFrames(threadId).getFrameCount()).isEqualTo(2);
+
+ // and verify the exact line index as well
+ ListThreadsResponse threads = listThreads();
+ assertThat(threads.getThreadList()).hasSize(1);
+ assertThat(threads.getThread(0).getIsPaused()).isTrue();
+ assertThat(threads.getThread(0).getLocation().getLineNumber()).isEqualTo(2);
+ }
+
+ @Test
+ public void testStepOverFunction() throws Exception {
+ sendStartDebuggingRequest();
+ BuildFileAST bzlFile =
+ parseSkylarkFile(
+ "/a/build/file/test.bzl",
+ "def fn():",
+ " a = 2",
+ " return a",
+ "x = fn()",
+ "y = [2,3,4]");
+ Environment env = newEnvironment();
+
+ Location breakpoint =
+ Location.newBuilder().setLineNumber(4).setPath("/a/build/file/test.bzl").build();
+ setBreakpoints(ImmutableList.of(breakpoint));
+
+ Thread evaluationThread = execInWorkerThread(bzlFile, env);
+ long threadId = evaluationThread.getId();
+
+ // wait for breakpoint to be hit
+ client.waitForEvent(DebugEvent::hasThreadPaused, Duration.ofSeconds(5));
+
+ assertThat(listThreads().getThread(0).getLocation().getLineNumber()).isEqualTo(4);
+
+ client.unnumberedEvents.clear();
+ client.sendRequestAndWaitForResponse(
+ DebugRequest.newBuilder()
+ .setSequenceNumber(2)
+ .setContinueExecution(
+ ContinueExecutionRequest.newBuilder()
+ .setThreadId(threadId)
+ .setStepping(Stepping.OVER)
+ .build())
+ .build());
+ client.waitForEvent(DebugEvent::hasThreadPaused, Duration.ofSeconds(5));
+
+ ListThreadsResponse threads = listThreads();
+ assertThat(threads.getThreadList()).hasSize(1);
+ assertThat(threads.getThread(0).getIsPaused()).isTrue();
+ assertThat(threads.getThread(0).getLocation().getLineNumber()).isEqualTo(5);
+ }
+
+ @Test
+ public void testStepOutOfFunction() throws Exception {
+ sendStartDebuggingRequest();
+ BuildFileAST bzlFile =
+ parseSkylarkFile(
+ "/a/build/file/test.bzl",
+ "def fn():",
+ " a = 2",
+ " return a",
+ "x = fn()",
+ "y = [2,3,4]");
+ Environment env = newEnvironment();
+
+ Location breakpoint =
+ Location.newBuilder().setLineNumber(2).setPath("/a/build/file/test.bzl").build();
+ setBreakpoints(ImmutableList.of(breakpoint));
+
+ Thread evaluationThread = execInWorkerThread(bzlFile, env);
+ long threadId = evaluationThread.getId();
+
+ // wait for breakpoint to be hit
+ client.waitForEvent(DebugEvent::hasThreadPaused, Duration.ofSeconds(5));
+
+ assertThat(listFrames(threadId).getFrameCount()).isEqualTo(2);
+
+ client.unnumberedEvents.clear();
+ client.sendRequestAndWaitForResponse(
+ DebugRequest.newBuilder()
+ .setSequenceNumber(2)
+ .setContinueExecution(
+ ContinueExecutionRequest.newBuilder()
+ .setThreadId(threadId)
+ .setStepping(Stepping.OUT)
+ .build())
+ .build());
+ client.waitForEvent(DebugEvent::hasThreadPaused, Duration.ofSeconds(5));
+
+ ListThreadsResponse threads = listThreads();
+ assertThat(threads.getThreadList()).hasSize(1);
+ assertThat(threads.getThread(0).getIsPaused()).isTrue();
+ assertThat(threads.getThread(0).getLocation().getLineNumber()).isEqualTo(5);
+ }
+
+ private void setBreakpoints(Iterable<Location> locations) throws Exception {
+ SetBreakpointsRequest.Builder request = SetBreakpointsRequest.newBuilder();
+ locations.forEach(l -> request.addBreakpoint(Breakpoint.newBuilder().setLocation(l)));
+ DebugEvent response =
+ client.sendRequestAndWaitForResponse(
+ DebugRequest.newBuilder().setSequenceNumber(10).setSetBreakpoints(request).build());
+ assertThat(response.hasSetBreakpoints()).isTrue();
+ assertThat(response.getSequenceNumber()).isEqualTo(10);
+ }
+
+ private void sendStartDebuggingRequest() throws Exception {
+ client.sendRequestAndWaitForResponse(
+ DebugRequest.newBuilder()
+ .setSequenceNumber(1)
+ .setStartDebugging(StartDebuggingRequest.newBuilder())
+ .build());
+ }
+
+ private ListThreadsResponse listThreads() throws Exception {
+ DebugEvent event =
+ client.sendRequestAndWaitForResponse(
+ DebugRequest.newBuilder()
+ .setSequenceNumber(1)
+ .setListThreads(ListThreadsRequest.newBuilder())
+ .build());
+ assertThat(event.hasListThreads()).isTrue();
+ assertThat(event.getSequenceNumber()).isEqualTo(1);
+ return event.getListThreads();
+ }
+
+ private ListFramesResponse listFrames(long threadId) throws Exception {
+ DebugEvent event =
+ client.sendRequestAndWaitForResponse(
+ DebugRequest.newBuilder()
+ .setSequenceNumber(1)
+ .setListFrames(ListFramesRequest.newBuilder().setThreadId(threadId).build())
+ .build());
+ assertThat(event.hasListFrames()).isTrue();
+ assertThat(event.getSequenceNumber()).isEqualTo(1);
+ return event.getListFrames();
+ }
+
+ private static Environment newEnvironment() {
+ Mutability mutability = Mutability.create("test");
+ return Environment.builder(mutability).useDefaultSemantics().build();
+ }
+
+ private BuildFileAST parseBuildFile(String path, String... lines) throws IOException {
+ Path file = scratch.file(path, lines);
+ byte[] bytes = FileSystemUtils.readWithKnownFileSize(file, file.getFileSize());
+ ParserInputSource inputSource = ParserInputSource.create(bytes, file.asFragment());
+ return BuildFileAST.parseBuildFile(inputSource, events.reporter());
+ }
+
+ private BuildFileAST parseSkylarkFile(String path, String... lines) throws IOException {
+ Path file = scratch.file(path, lines);
+ byte[] bytes = FileSystemUtils.readWithKnownFileSize(file, file.getFileSize());
+ ParserInputSource inputSource = ParserInputSource.create(bytes, file.asFragment());
+ return BuildFileAST.parseSkylarkFile(inputSource, events.reporter());
+ }
+
+ /**
+ * Creates and starts a worker thread executing the given {@link BuildFileAST} in the given
+ * environment.
+ */
+ private Thread execInWorkerThread(BuildFileAST ast, Environment env) {
+ Thread thread =
+ new Thread(
+ () -> {
+ try {
+ ast.exec(env, events.collector());
+ } catch (Throwable e) {
+ throw new AssertionError(e);
+ }
+ });
+ thread.start();
+ return thread;
+ }
+}