blob: 8e385df153015f264624fe63ff6fdf8a810ab156 [file] [log] [blame]
// 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.starlarkdebug.server;
import static com.google.common.truth.Truth.assertThat;
import com.google.common.base.Joiner;
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.starlarkdebugging.StarlarkDebuggingProtos;
import com.google.devtools.build.lib.starlarkdebugging.StarlarkDebuggingProtos.Breakpoint;
import com.google.devtools.build.lib.starlarkdebugging.StarlarkDebuggingProtos.ContinueExecutionRequest;
import com.google.devtools.build.lib.starlarkdebugging.StarlarkDebuggingProtos.DebugEvent;
import com.google.devtools.build.lib.starlarkdebugging.StarlarkDebuggingProtos.DebugRequest;
import com.google.devtools.build.lib.starlarkdebugging.StarlarkDebuggingProtos.EvaluateRequest;
import com.google.devtools.build.lib.starlarkdebugging.StarlarkDebuggingProtos.Frame;
import com.google.devtools.build.lib.starlarkdebugging.StarlarkDebuggingProtos.ListFramesRequest;
import com.google.devtools.build.lib.starlarkdebugging.StarlarkDebuggingProtos.ListFramesResponse;
import com.google.devtools.build.lib.starlarkdebugging.StarlarkDebuggingProtos.Location;
import com.google.devtools.build.lib.starlarkdebugging.StarlarkDebuggingProtos.PauseReason;
import com.google.devtools.build.lib.starlarkdebugging.StarlarkDebuggingProtos.PausedThread;
import com.google.devtools.build.lib.starlarkdebugging.StarlarkDebuggingProtos.Scope;
import com.google.devtools.build.lib.starlarkdebugging.StarlarkDebuggingProtos.SetBreakpointsRequest;
import com.google.devtools.build.lib.starlarkdebugging.StarlarkDebuggingProtos.StartDebuggingRequest;
import com.google.devtools.build.lib.starlarkdebugging.StarlarkDebuggingProtos.StartDebuggingResponse;
import com.google.devtools.build.lib.starlarkdebugging.StarlarkDebuggingProtos.Stepping;
import com.google.devtools.build.lib.starlarkdebugging.StarlarkDebuggingProtos.Value;
import com.google.devtools.build.lib.syntax.Debug;
import com.google.devtools.build.lib.syntax.EvalException;
import com.google.devtools.build.lib.syntax.EvalUtils;
import com.google.devtools.build.lib.syntax.FileOptions;
import com.google.devtools.build.lib.syntax.Module;
import com.google.devtools.build.lib.syntax.Mutability;
import com.google.devtools.build.lib.syntax.ParserInput;
import com.google.devtools.build.lib.syntax.Starlark;
import com.google.devtools.build.lib.syntax.StarlarkList;
import com.google.devtools.build.lib.syntax.StarlarkSemantics;
import com.google.devtools.build.lib.syntax.StarlarkThread;
import com.google.devtools.build.lib.syntax.SyntaxError;
import java.io.IOException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.SocketException;
import java.time.Duration;
import java.util.Collection;
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 java.util.stream.Collectors;
import javax.annotation.Nullable;
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 StarlarkDebugServer}. */
@RunWith(JUnit4.class)
public class StarlarkDebugServerTest {
private final ExecutorService executor = Executors.newFixedThreadPool(2);
private final EventCollectionApparatus events =
new EventCollectionApparatus(EventKind.ALL_EVENTS);
private final ThreadObjectMap dummyObjectMap = new ThreadObjectMap();
private MockDebugClient client;
private StarlarkDebugServer server;
/**
* Returns the {@link Value} proto message corresponding to the given object and label. Subsequent
* calls may return values with different IDs.
*/
private Value getValueProto(String label, Object value) {
return DebuggerSerialization.getValueProto(dummyObjectMap, label, value);
}
private ImmutableList<Value> getChildren(Value value) {
Object object = dummyObjectMap.getValue(value.getId());
return object != null
? DebuggerSerialization.getChildren(dummyObjectMap, object)
: ImmutableList.of();
}
private static ServerSocket getServerSocket() throws IOException {
// For reasons only Apple knows, you cannot bind to IPv4-localhost when you run in a sandbox
// that only allows loopback traffic, but binding to IPv6-localhost works fine. This would
// however break on systems that don't support IPv6. So what we'll do is to try to bind to IPv6
// and if that fails, try again with IPv4.
try {
return new ServerSocket(0, 1, InetAddress.getByName("[::1]"));
} catch (SocketException e) {
return new ServerSocket(0, 1, InetAddress.getByName("127.0.0.1"));
}
}
@Before
public void setUpServerAndClient() throws Exception {
ServerSocket serverSocket = getServerSocket();
Future<StarlarkDebugServer> future =
executor.submit(
() ->
StarlarkDebugServer.createAndWaitForConnection(
events.reporter(), serverSocket, false));
client = new MockDebugClient();
client.connect(serverSocket, Duration.ofSeconds(10));
server = future.get(10, TimeUnit.SECONDS);
assertThat(server).isNotNull();
Debug.setDebugger(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 testPausedUntilStartDebuggingRequestReceived() throws Exception {
ParserInput buildFile = createInput("/a/build/file/BUILD", "x = [1,2,3]");
Thread evaluationThread = execInWorkerThread(buildFile, null);
String threadName = evaluationThread.getName();
long threadId = evaluationThread.getId();
// wait for BUILD evaluation to start
DebugEvent event = client.waitForEvent(DebugEvent::hasThreadPaused, Duration.ofSeconds(5));
Location expectedLocation =
DebugEventHelper.getLocationProto(
com.google.devtools.build.lib.syntax.Location.fromFileLineColumn(
"/a/build/file/BUILD", 1, 1));
assertThat(event)
.isEqualTo(
DebugEventHelper.threadPausedEvent(
StarlarkDebuggingProtos.PausedThread.newBuilder()
.setId(threadId)
.setName(threadName)
.setPauseReason(PauseReason.INITIALIZING)
.setLocation(expectedLocation)
.build()));
sendStartDebuggingRequest();
event = client.waitForEvent(DebugEvent::hasThreadContinued, Duration.ofSeconds(5));
assertThat(event).isEqualTo(DebugEventHelper.threadContinuedEvent(threadId));
}
@Test
public void testResumeAllThreads() throws Exception {
sendStartDebuggingRequest();
ParserInput buildFile = createInput("/a/build/file/BUILD", "x = [1,2,3]", "y = [2,3,4]");
Location breakpoint =
Location.newBuilder().setLineNumber(2).setPath("/a/build/file/BUILD").build();
setBreakpoints(ImmutableList.of(breakpoint));
// evaluate in two separate worker threads
execInWorkerThread(buildFile, null);
execInWorkerThread(buildFile, null);
// wait for both breakpoints to be hit
boolean paused =
client.waitForEvents(
list -> list.stream().filter(DebugEvent::hasThreadPaused).count() == 2,
Duration.ofSeconds(5));
assertThat(paused).isTrue();
client.sendRequestAndWaitForResponse(
DebugRequest.newBuilder()
.setSequenceNumber(45)
.setContinueExecution(ContinueExecutionRequest.newBuilder())
.build());
boolean resumed =
client.waitForEvents(
list -> list.stream().filter(DebugEvent::hasThreadContinued).count() == 2,
Duration.ofSeconds(5));
assertThat(resumed).isTrue();
}
@Test
public void testPauseAtBreakpoint() throws Exception {
sendStartDebuggingRequest();
ParserInput buildFile = createInput("/a/build/file/BUILD", "x = [1,2,3]", "y = [2,3,4]");
Location breakpoint =
Location.newBuilder().setLineNumber(2).setPath("/a/build/file/BUILD").build();
setBreakpoints(ImmutableList.of(breakpoint));
Thread evaluationThread = execInWorkerThread(buildFile, null);
String threadName = evaluationThread.getName();
long threadId = evaluationThread.getId();
// wait for breakpoint to be hit
DebugEvent event = client.waitForEvent(DebugEvent::hasThreadPaused, Duration.ofSeconds(5));
StarlarkDebuggingProtos.PausedThread expectedThreadState =
StarlarkDebuggingProtos.PausedThread.newBuilder()
.setName(threadName)
.setId(threadId)
.setPauseReason(PauseReason.HIT_BREAKPOINT)
.setLocation(breakpoint.toBuilder().setColumnNumber(1))
.build();
assertThat(event).isEqualTo(DebugEventHelper.threadPausedEvent(expectedThreadState));
}
@Test
public void testDoNotPauseAtUnsatisfiedConditionalBreakpoint() throws Exception {
sendStartDebuggingRequest();
ParserInput buildFile =
createInput("/a/build/file/BUILD", "x = [1,2,3]", "y = [2,3,4]", "z = 1");
ImmutableList<Breakpoint> breakpoints =
ImmutableList.of(
Breakpoint.newBuilder()
.setLocation(Location.newBuilder().setLineNumber(2).setPath("/a/build/file/BUILD"))
.setExpression("x[0] == 2")
.build(),
Breakpoint.newBuilder()
.setLocation(Location.newBuilder().setLineNumber(3).setPath("/a/build/file/BUILD"))
.setExpression("x[0] == 1")
.build());
setBreakpoints(breakpoints);
Thread evaluationThread = execInWorkerThread(buildFile, null);
String threadName = evaluationThread.getName();
long threadId = evaluationThread.getId();
Breakpoint expectedBreakpoint = breakpoints.get(1);
DebugEvent event = client.waitForEvent(DebugEvent::hasThreadPaused, Duration.ofSeconds(5));
assertThat(event)
.isEqualTo(
DebugEventHelper.threadPausedEvent(
StarlarkDebuggingProtos.PausedThread.newBuilder()
.setName(threadName)
.setId(threadId)
.setLocation(expectedBreakpoint.getLocation().toBuilder().setColumnNumber(1))
.setPauseReason(PauseReason.HIT_BREAKPOINT)
.build()));
}
@Test
public void testPauseAtSatisfiedConditionalBreakpoint() throws Exception {
sendStartDebuggingRequest();
ParserInput buildFile = createInput("/a/build/file/BUILD", "x = [1,2,3]", "y = [2,3,4]");
Location location =
Location.newBuilder().setLineNumber(2).setPath("/a/build/file/BUILD").build();
Breakpoint breakpoint =
Breakpoint.newBuilder().setLocation(location).setExpression("x[0] == 1").build();
setBreakpoints(ImmutableList.of(breakpoint));
Thread evaluationThread = execInWorkerThread(buildFile, null);
String threadName = evaluationThread.getName();
long threadId = evaluationThread.getId();
// wait for breakpoint to be hit
DebugEvent event = client.waitForEvent(DebugEvent::hasThreadPaused, Duration.ofSeconds(5));
StarlarkDebuggingProtos.PausedThread expectedThreadState =
StarlarkDebuggingProtos.PausedThread.newBuilder()
.setName(threadName)
.setId(threadId)
.setPauseReason(PauseReason.HIT_BREAKPOINT)
.setLocation(location.toBuilder().setColumnNumber(1))
.build();
assertThat(event).isEqualTo(DebugEventHelper.threadPausedEvent(expectedThreadState));
}
@Test
public void testPauseAtInvalidConditionBreakpointWithError() throws Exception {
sendStartDebuggingRequest();
ParserInput buildFile = createInput("/a/build/file/BUILD", "x = [1,2,3]", "y = [2,3,4]");
Location location =
Location.newBuilder().setLineNumber(2).setPath("/a/build/file/BUILD").build();
Breakpoint breakpoint =
Breakpoint.newBuilder().setLocation(location).setExpression("z[0] == 1").build();
setBreakpoints(ImmutableList.of(breakpoint));
Thread evaluationThread = execInWorkerThread(buildFile, null);
String threadName = evaluationThread.getName();
long threadId = evaluationThread.getId();
// wait for breakpoint to be hit
DebugEvent event = client.waitForEvent(DebugEvent::hasThreadPaused, Duration.ofSeconds(5));
StarlarkDebuggingProtos.PausedThread expectedThreadState =
StarlarkDebuggingProtos.PausedThread.newBuilder()
.setName(threadName)
.setId(threadId)
.setPauseReason(PauseReason.CONDITIONAL_BREAKPOINT_ERROR)
.setLocation(location.toBuilder().setColumnNumber(1))
.setConditionalBreakpointError(
StarlarkDebuggingProtos.Error.newBuilder().setMessage("name \'z\' is not defined"))
.build();
assertThat(event).isEqualTo(DebugEventHelper.threadPausedEvent(expectedThreadState));
}
@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 paused");
}
@Test
public void testSimpleListFramesRequest() throws Exception {
sendStartDebuggingRequest();
ParserInput buildFile = createInput("/a/build/file/BUILD", "x = [1,2,3]", "y = [2,3,4]");
Location breakpoint =
Location.newBuilder().setLineNumber(2).setPath("/a/build/file/BUILD").build();
setBreakpoints(ImmutableList.of(breakpoint));
Thread evaluationThread = execInWorkerThread(buildFile, null);
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);
assertFramesEqualIgnoringValueIdentifiers(
frames.getFrame(0),
Frame.newBuilder()
.setFunctionName("<toplevel>")
.setLocation(breakpoint.toBuilder().setColumnNumber(1))
.addScope(
Scope.newBuilder()
.setName("global")
.addBinding(getValueProto("x", StarlarkList.of(/*mutability=*/ null, 1, 2, 3))))
.build());
}
@Test
public void testGetChildrenRequest() throws Exception {
sendStartDebuggingRequest();
ParserInput buildFile = createInput("/a/build/file/BUILD", "x = [1,2,3]", "y = [2,3,4]");
Location breakpoint =
Location.newBuilder().setLineNumber(2).setPath("/a/build/file/BUILD").build();
setBreakpoints(ImmutableList.of(breakpoint));
Thread evaluationThread = execInWorkerThread(buildFile, null);
long threadId = evaluationThread.getId();
// wait for breakpoint to be hit
client.waitForEvent(DebugEvent::hasThreadPaused, Duration.ofSeconds(5));
ListFramesResponse frames = listFrames(threadId);
Value xValue = frames.getFrame(0).getScope(0).getBinding(0);
assertValuesEqualIgnoringId(
xValue, getValueProto("x", StarlarkList.of(/*mutability=*/ null, 1, 2, 3)));
List<Value> children = getChildren(xValue);
assertThat(children)
.isEqualTo(
ImmutableList.of(
getValueProto("[0]", 1), getValueProto("[1]", 2), getValueProto("[2]", 3)));
}
@Test
public void testListFramesShadowedBinding() throws Exception {
sendStartDebuggingRequest();
ParserInput bzlFile =
createInput(
"/a/build/file/test.bzl",
"a = 1",
"c = 3",
"def fn():",
" a = 2",
" b = 1",
" b + 1",
"fn()");
Location breakpoint =
Location.newBuilder().setPath("/a/build/file/test.bzl").setLineNumber(6).build();
setBreakpoints(ImmutableList.of(breakpoint));
Module module = Module.create();
Thread evaluationThread = execInWorkerThread(bzlFile, module);
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);
assertFramesEqualIgnoringValueIdentifiers(
frames.getFrame(0),
Frame.newBuilder()
.setFunctionName("fn")
.setLocation(breakpoint.toBuilder().setColumnNumber(3))
.addScope(
Scope.newBuilder()
.setName("local")
.addBinding(getValueProto("a", 2))
.addBinding(getValueProto("b", 1)))
.addScope(
Scope.newBuilder()
.setName("global")
.addBinding(getValueProto("c", 3))
.addBinding(getValueProto("fn", module.getGlobal("fn"))))
.build());
assertFramesEqualIgnoringValueIdentifiers(
frames.getFrame(1),
Frame.newBuilder()
.setFunctionName("<toplevel>")
.setLocation(
Location.newBuilder()
.setPath("/a/build/file/test.bzl")
.setLineNumber(7)
.setColumnNumber(3))
.addScope(
Scope.newBuilder()
.setName("global")
.addBinding(getValueProto("a", 1))
.addBinding(getValueProto("c", 3))
.addBinding(getValueProto("fn", module.getGlobal("fn"))))
.build());
}
@Test
public void testEvaluateRequestWithExpression() throws Exception {
sendStartDebuggingRequest();
ParserInput buildFile = createInput("/a/build/file/BUILD", "x = [1,2,3]", "y = [2,3,4]");
Location breakpoint =
Location.newBuilder().setLineNumber(2).setPath("/a/build/file/BUILD").build();
setBreakpoints(ImmutableList.of(breakpoint));
Thread evaluationThread = execInWorkerThread(buildFile, null);
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).setStatement("x[1]").build())
.build());
assertThat(response.hasEvaluate()).isTrue();
assertThat(response.getEvaluate().getResult()).isEqualTo(getValueProto("Evaluation result", 2));
}
@Test
public void testEvaluateRequestWithAssignmentStatement() throws Exception {
sendStartDebuggingRequest();
ParserInput buildFile = createInput("/a/build/file/BUILD", "x = [1,2,3]", "y = [2,3,4]");
Location breakpoint =
Location.newBuilder().setLineNumber(2).setPath("/a/build/file/BUILD").build();
setBreakpoints(ImmutableList.of(breakpoint));
Thread evaluationThread = execInWorkerThread(buildFile, null);
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)
.setStatement("x = [5,6]")
.build())
.build());
assertThat(response.getEvaluate().getResult())
.isEqualTo(getValueProto("Evaluation result", Starlark.NONE));
ListFramesResponse frames = listFrames(threadId);
assertThat(frames.getFrame(0).getScope(0).getBindingList())
.contains(getValueProto("x", StarlarkList.of(/*mutability=*/ null, 5, 6)));
}
@Test
public void testEvaluateRequestWithExpressionStatementMutatingState() throws Exception {
sendStartDebuggingRequest();
ParserInput buildFile = createInput("/a/build/file/BUILD", "x = [1,2,3]", "y = [2,3,4]");
Location breakpoint =
Location.newBuilder().setLineNumber(2).setPath("/a/build/file/BUILD").build();
setBreakpoints(ImmutableList.of(breakpoint));
Thread evaluationThread = execInWorkerThread(buildFile, null);
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)
.setStatement("x.append(4)")
.build())
.build());
assertThat(response.getEvaluate().getResult())
.isEqualTo(getValueProto("Evaluation result", Starlark.NONE));
ListFramesResponse frames = listFrames(threadId);
assertThat(frames.getFrame(0).getScope(0).getBindingList())
.contains(getValueProto("x", StarlarkList.of(/*mutability=*/ null, 1, 2, 3, 4)));
}
@Test
public void testEvaluateRequestThrowingException() throws Exception {
sendStartDebuggingRequest();
ParserInput buildFile = createInput("/a/build/file/BUILD", "x = [1,2,3]", "y = [2,3,4]");
Location breakpoint =
Location.newBuilder().setLineNumber(2).setPath("/a/build/file/BUILD").build();
setBreakpoints(ImmutableList.of(breakpoint));
Thread evaluationThread = execInWorkerThread(buildFile, null);
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).setStatement("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();
ParserInput bzlFile =
createInput(
"/a/build/file/test.bzl",
"def fn():",
" a = 2",
" return a",
"x = fn()",
"y = [2,3,4]");
Location breakpoint =
Location.newBuilder().setLineNumber(4).setPath("/a/build/file/test.bzl").build();
setBreakpoints(ImmutableList.of(breakpoint));
Thread evaluationThread = execInWorkerThread(bzlFile, null);
long threadId = evaluationThread.getId();
// wait for breakpoint to be hit
DebugEvent event = client.waitForEvent(DebugEvent::hasThreadPaused, Duration.ofSeconds(5));
assertThat(event.getThreadPaused().getThread().getLocation().getLineNumber()).isEqualTo(4);
client.unnumberedEvents.clear();
client.sendRequestAndWaitForResponse(
DebugRequest.newBuilder()
.setSequenceNumber(2)
.setContinueExecution(
ContinueExecutionRequest.newBuilder()
.setThreadId(threadId)
.setStepping(Stepping.INTO)
.build())
.build());
event = client.waitForEvent(DebugEvent::hasThreadPaused, Duration.ofSeconds(5));
// check we're paused inside the function
assertThat(listFrames(threadId).getFrameCount()).isEqualTo(2);
// and verify the location and pause reason as well
Location expectedLocation = breakpoint.toBuilder().setLineNumber(2).setColumnNumber(3).build();
StarlarkDebuggingProtos.PausedThread pausedThread = event.getThreadPaused().getThread();
assertThat(pausedThread.getPauseReason()).isEqualTo(PauseReason.STEPPING);
assertThat(pausedThread.getLocation()).isEqualTo(expectedLocation);
}
@Test
public void testStepOverFunction() throws Exception {
sendStartDebuggingRequest();
ParserInput bzlFile =
createInput(
"/a/build/file/test.bzl",
"def fn():",
" a = 2",
" return a",
"x = fn()",
"y = [2,3,4]");
Location breakpoint =
Location.newBuilder().setLineNumber(4).setPath("/a/build/file/test.bzl").build();
setBreakpoints(ImmutableList.of(breakpoint));
Thread evaluationThread = execInWorkerThread(bzlFile, null);
long threadId = evaluationThread.getId();
// wait for breakpoint to be hit
DebugEvent event = client.waitForEvent(DebugEvent::hasThreadPaused, Duration.ofSeconds(5));
assertThat(event.getThreadPaused().getThread().getLocation().getLineNumber()).isEqualTo(4);
client.unnumberedEvents.clear();
client.sendRequestAndWaitForResponse(
DebugRequest.newBuilder()
.setSequenceNumber(2)
.setContinueExecution(
ContinueExecutionRequest.newBuilder()
.setThreadId(threadId)
.setStepping(Stepping.OVER)
.build())
.build());
event = client.waitForEvent(DebugEvent::hasThreadPaused, Duration.ofSeconds(5));
Location expectedLocation = breakpoint.toBuilder().setLineNumber(5).setColumnNumber(1).build();
PausedThread pausedThread = event.getThreadPaused().getThread();
assertThat(pausedThread.getPauseReason()).isEqualTo(PauseReason.STEPPING);
assertThat(pausedThread.getLocation()).isEqualTo(expectedLocation);
}
@Test
public void testStepOutOfFunction() throws Exception {
sendStartDebuggingRequest();
ParserInput bzlFile =
createInput(
"/a/build/file/test.bzl",
"def fn():",
" a = 2",
" return a",
"x = fn()",
"y = [2,3,4]");
Location breakpoint =
Location.newBuilder().setLineNumber(2).setPath("/a/build/file/test.bzl").build();
setBreakpoints(ImmutableList.of(breakpoint));
Thread evaluationThread = execInWorkerThread(bzlFile, null);
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());
DebugEvent event = client.waitForEvent(DebugEvent::hasThreadPaused, Duration.ofSeconds(5));
PausedThread pausedThread = event.getThreadPaused().getThread();
Location expectedLocation = breakpoint.toBuilder().setLineNumber(5).setColumnNumber(1).build();
assertThat(pausedThread.getPauseReason()).isEqualTo(PauseReason.STEPPING);
assertThat(pausedThread.getLocation()).isEqualTo(expectedLocation);
}
private void setBreakpoints(Collection<Location> locations) throws Exception {
setBreakpoints(
locations
.stream()
.map(l -> Breakpoint.newBuilder().setLocation(l).build())
.collect(Collectors.toList()));
}
private void setBreakpoints(Iterable<Breakpoint> breakpoints) throws Exception {
DebugEvent response =
client.sendRequestAndWaitForResponse(
DebugRequest.newBuilder()
.setSequenceNumber(10)
.setSetBreakpoints(SetBreakpointsRequest.newBuilder().addAllBreakpoint(breakpoints))
.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 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 ParserInput createInput(String filename, String... lines) {
return ParserInput.fromString(Joiner.on("\n").join(lines), filename);
}
/**
* Creates and starts a worker thread parsing, resolving, and executing the given Starlark file to
* populate the specified module, or if none is given, in a fresh module with a default
* environment.
*/
private static Thread execInWorkerThread(ParserInput input, @Nullable Module module) {
Thread javaThread =
new Thread(
() -> {
try (Mutability mu = Mutability.create("test")) {
StarlarkThread thread = new StarlarkThread(mu, StarlarkSemantics.DEFAULT);
EvalUtils.exec(
input, FileOptions.DEFAULT, module != null ? module : Module.create(), thread);
} catch (SyntaxError.Exception | EvalException | InterruptedException ex) {
throw new AssertionError(ex);
}
});
javaThread.start();
return javaThread;
}
/**
* Asserts that the given frames are equal after clearing the identifier from all {@link Value}s.
*/
private void assertFramesEqualIgnoringValueIdentifiers(Frame frame1, Frame frame2) {
assertThat(clearIds(frame1)).isEqualTo(clearIds(frame2));
}
private static Frame clearIds(Frame frame) {
Frame.Builder builder = frame.toBuilder();
for (int i = 0; i < frame.getScopeCount(); i++) {
builder.setScope(i, clearIds(builder.getScope(i)));
}
return builder.build();
}
private static Scope clearIds(Scope scope) {
Scope.Builder builder = scope.toBuilder();
for (int i = 0; i < scope.getBindingCount(); i++) {
builder.getBindingBuilder(i).clearId();
}
return builder.build();
}
private void assertValuesEqualIgnoringId(Value value1, Value value2) {
assertThat(clearId(value1)).isEqualTo(clearId(value2));
}
private static Value clearId(Value value) {
return value.toBuilder().clearId().build();
}
}