blob: c5715536184ac5545dfa907c30582af1d9e2d211 [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.skylarkdebug.server;
import static com.google.common.truth.Truth.assertThat;
import com.google.common.collect.ImmutableList;
import com.google.devtools.build.lib.events.Event;
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.Location;
import com.google.devtools.build.lib.skylarkdebugging.SkylarkDebuggingProtos.PauseReason;
import com.google.devtools.build.lib.skylarkdebugging.SkylarkDebuggingProtos.PausedThread;
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.skylarkdebugging.SkylarkDebuggingProtos.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.Mutability;
import com.google.devtools.build.lib.syntax.ParserInput;
import com.google.devtools.build.lib.syntax.Starlark;
import com.google.devtools.build.lib.syntax.StarlarkFile;
import com.google.devtools.build.lib.syntax.StarlarkList;
import com.google.devtools.build.lib.syntax.StarlarkThread;
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.BindException;
import java.net.InetAddress;
import java.net.ServerSocket;
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 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 final ThreadObjectMap dummyObjectMap = new ThreadObjectMap();
private MockDebugClient client;
private SkylarkDebugServer 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 (BindException e) {
return new ServerSocket(0, 1, InetAddress.getByName("127.0.0.1"));
}
}
@Before
public void setUpServerAndClient() throws Exception {
ServerSocket serverSocket = getServerSocket();
Future<SkylarkDebugServer> future =
executor.submit(
() ->
SkylarkDebugServer.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 {
StarlarkFile buildFile = parseFile("/a/build/file/BUILD", "x = [1,2,3]");
StarlarkThread thread = newStarlarkThread();
Thread evaluationThread = execInWorkerThread(buildFile, thread);
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(buildFile.getStatements().get(0).getStartLocation());
assertThat(event)
.isEqualTo(
DebugEventHelper.threadPausedEvent(
SkylarkDebuggingProtos.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();
StarlarkFile buildFile = parseFile("/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, newStarlarkThread());
execInWorkerThread(buildFile, newStarlarkThread());
// 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();
StarlarkFile buildFile = parseFile("/a/build/file/BUILD", "x = [1,2,3]", "y = [2,3,4]");
StarlarkThread thread = newStarlarkThread();
Location breakpoint =
Location.newBuilder().setLineNumber(2).setPath("/a/build/file/BUILD").build();
setBreakpoints(ImmutableList.of(breakpoint));
Thread evaluationThread = execInWorkerThread(buildFile, thread);
String threadName = evaluationThread.getName();
long threadId = evaluationThread.getId();
// wait for breakpoint to be hit
DebugEvent event = client.waitForEvent(DebugEvent::hasThreadPaused, Duration.ofSeconds(5));
SkylarkDebuggingProtos.PausedThread expectedThreadState =
SkylarkDebuggingProtos.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();
StarlarkFile buildFile =
parseFile("/a/build/file/BUILD", "x = [1,2,3]", "y = [2,3,4]", "z = 1");
StarlarkThread thread = newStarlarkThread();
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, thread);
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(
SkylarkDebuggingProtos.PausedThread.newBuilder()
.setName(threadName)
.setId(threadId)
.setLocation(expectedBreakpoint.getLocation().toBuilder().setColumnNumber(1))
.setPauseReason(PauseReason.HIT_BREAKPOINT)
.build()));
}
@Test
public void testPauseAtSatisfiedConditionalBreakpoint() throws Exception {
sendStartDebuggingRequest();
StarlarkFile buildFile = parseFile("/a/build/file/BUILD", "x = [1,2,3]", "y = [2,3,4]");
StarlarkThread thread = newStarlarkThread();
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, thread);
String threadName = evaluationThread.getName();
long threadId = evaluationThread.getId();
// wait for breakpoint to be hit
DebugEvent event = client.waitForEvent(DebugEvent::hasThreadPaused, Duration.ofSeconds(5));
SkylarkDebuggingProtos.PausedThread expectedThreadState =
SkylarkDebuggingProtos.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();
StarlarkFile buildFile = parseFile("/a/build/file/BUILD", "x = [1,2,3]", "y = [2,3,4]");
StarlarkThread thread = newStarlarkThread();
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, thread);
String threadName = evaluationThread.getName();
long threadId = evaluationThread.getId();
// wait for breakpoint to be hit
DebugEvent event = client.waitForEvent(DebugEvent::hasThreadPaused, Duration.ofSeconds(5));
SkylarkDebuggingProtos.PausedThread expectedThreadState =
SkylarkDebuggingProtos.PausedThread.newBuilder()
.setName(threadName)
.setId(threadId)
.setPauseReason(PauseReason.CONDITIONAL_BREAKPOINT_ERROR)
.setLocation(location.toBuilder().setColumnNumber(1))
.setConditionalBreakpointError(
SkylarkDebuggingProtos.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();
StarlarkFile buildFile = parseFile("/a/build/file/BUILD", "x = [1,2,3]", "y = [2,3,4]");
StarlarkThread thread = newStarlarkThread();
Location breakpoint =
Location.newBuilder().setLineNumber(2).setPath("/a/build/file/BUILD").build();
setBreakpoints(ImmutableList.of(breakpoint));
Thread evaluationThread = execInWorkerThread(buildFile, thread);
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();
StarlarkFile buildFile = parseFile("/a/build/file/BUILD", "x = [1,2,3]", "y = [2,3,4]");
StarlarkThread thread = newStarlarkThread();
Location breakpoint =
Location.newBuilder().setLineNumber(2).setPath("/a/build/file/BUILD").build();
setBreakpoints(ImmutableList.of(breakpoint));
Thread evaluationThread = execInWorkerThread(buildFile, thread);
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();
StarlarkFile bzlFile =
parseFile(
"/a/build/file/test.bzl",
"a = 1",
"c = 3",
"def fn():",
" a = 2",
" b = 1",
" b + 1",
"fn()");
StarlarkThread thread = newStarlarkThread();
Location breakpoint =
Location.newBuilder().setPath("/a/build/file/test.bzl").setLineNumber(6).build();
setBreakpoints(ImmutableList.of(breakpoint));
Thread evaluationThread = execInWorkerThread(bzlFile, thread);
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", thread.getGlobals().lookup("fn"))))
.build());
assertFramesEqualIgnoringValueIdentifiers(
frames.getFrame(1),
Frame.newBuilder()
.setFunctionName("<toplevel>")
.setLocation(
Location.newBuilder()
.setPath("/a/build/file/test.bzl")
.setLineNumber(7)
.setColumnNumber(1))
.addScope(
Scope.newBuilder()
.setName("global")
.addBinding(getValueProto("a", 1))
.addBinding(getValueProto("c", 3))
.addBinding(getValueProto("fn", thread.getGlobals().lookup("fn"))))
.build());
}
@Test
public void testEvaluateRequestWithExpression() throws Exception {
sendStartDebuggingRequest();
StarlarkFile buildFile = parseFile("/a/build/file/BUILD", "x = [1,2,3]", "y = [2,3,4]");
StarlarkThread thread = newStarlarkThread();
Location breakpoint =
Location.newBuilder().setLineNumber(2).setPath("/a/build/file/BUILD").build();
setBreakpoints(ImmutableList.of(breakpoint));
Thread evaluationThread = execInWorkerThread(buildFile, thread);
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();
StarlarkFile buildFile = parseFile("/a/build/file/BUILD", "x = [1,2,3]", "y = [2,3,4]");
StarlarkThread thread = newStarlarkThread();
Location breakpoint =
Location.newBuilder().setLineNumber(2).setPath("/a/build/file/BUILD").build();
setBreakpoints(ImmutableList.of(breakpoint));
Thread evaluationThread = execInWorkerThread(buildFile, thread);
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();
StarlarkFile buildFile = parseFile("/a/build/file/BUILD", "x = [1,2,3]", "y = [2,3,4]");
StarlarkThread thread = newStarlarkThread();
Location breakpoint =
Location.newBuilder().setLineNumber(2).setPath("/a/build/file/BUILD").build();
setBreakpoints(ImmutableList.of(breakpoint));
Thread evaluationThread = execInWorkerThread(buildFile, thread);
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();
StarlarkFile buildFile = parseFile("/a/build/file/BUILD", "x = [1,2,3]", "y = [2,3,4]");
StarlarkThread thread = newStarlarkThread();
Location breakpoint =
Location.newBuilder().setLineNumber(2).setPath("/a/build/file/BUILD").build();
setBreakpoints(ImmutableList.of(breakpoint));
Thread evaluationThread = execInWorkerThread(buildFile, thread);
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();
StarlarkFile bzlFile =
parseFile(
"/a/build/file/test.bzl",
"def fn():",
" a = 2",
" return a",
"x = fn()",
"y = [2,3,4]");
StarlarkThread thread = newStarlarkThread();
Location breakpoint =
Location.newBuilder().setLineNumber(4).setPath("/a/build/file/test.bzl").build();
setBreakpoints(ImmutableList.of(breakpoint));
Thread evaluationThread = execInWorkerThread(bzlFile, thread);
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();
SkylarkDebuggingProtos.PausedThread pausedThread = event.getThreadPaused().getThread();
assertThat(pausedThread.getPauseReason()).isEqualTo(PauseReason.STEPPING);
assertThat(pausedThread.getLocation()).isEqualTo(expectedLocation);
}
@Test
public void testStepOverFunction() throws Exception {
sendStartDebuggingRequest();
StarlarkFile bzlFile =
parseFile(
"/a/build/file/test.bzl",
"def fn():",
" a = 2",
" return a",
"x = fn()",
"y = [2,3,4]");
StarlarkThread thread = newStarlarkThread();
Location breakpoint =
Location.newBuilder().setLineNumber(4).setPath("/a/build/file/test.bzl").build();
setBreakpoints(ImmutableList.of(breakpoint));
Thread evaluationThread = execInWorkerThread(bzlFile, thread);
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();
StarlarkFile bzlFile =
parseFile(
"/a/build/file/test.bzl",
"def fn():",
" a = 2",
" return a",
"x = fn()",
"y = [2,3,4]");
StarlarkThread thread = newStarlarkThread();
Location breakpoint =
Location.newBuilder().setLineNumber(2).setPath("/a/build/file/test.bzl").build();
setBreakpoints(ImmutableList.of(breakpoint));
Thread evaluationThread = execInWorkerThread(bzlFile, thread);
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 StarlarkThread newStarlarkThread() {
Mutability mutability = Mutability.create("test");
return StarlarkThread.builder(mutability).useDefaultSemantics().build();
}
private StarlarkFile parseFile(String filename, String... lines) throws IOException {
Path path = scratch.file(filename, lines);
byte[] bytes = FileSystemUtils.readWithKnownFileSize(path, path.getFileSize());
ParserInput input = ParserInput.create(bytes, filename);
StarlarkFile file = StarlarkFile.parse(input);
Event.replayEventsOn(events.reporter(), file.errors());
return file;
}
/**
* Creates and starts a worker thread executing the given {@link StarlarkFile} in the given
* environment.
*/
private static Thread execInWorkerThread(StarlarkFile file, StarlarkThread thread) {
Thread javaThread =
new Thread(
() -> {
try {
EvalUtils.exec(file, thread.getGlobals(), thread);
} catch (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();
}
}