| // 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 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 net.starlark.java.eval.Debug; |
| import net.starlark.java.eval.EvalException; |
| import net.starlark.java.eval.Module; |
| import net.starlark.java.eval.Mutability; |
| import net.starlark.java.eval.Starlark; |
| import net.starlark.java.eval.StarlarkInt; |
| import net.starlark.java.eval.StarlarkList; |
| import net.starlark.java.eval.StarlarkSemantics; |
| import net.starlark.java.eval.StarlarkThread; |
| import net.starlark.java.syntax.FileOptions; |
| import net.starlark.java.syntax.ParserInput; |
| import net.starlark.java.syntax.SyntaxError; |
| 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.getDefaultInstance()) |
| .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( |
| net.starlark.java.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.getDefaultInstance()) |
| .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.immutableOf( |
| StarlarkInt.of(1), StarlarkInt.of(2), StarlarkInt.of(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.immutableOf(StarlarkInt.of(1), StarlarkInt.of(2), StarlarkInt.of(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", StarlarkInt.of(2))) |
| .addBinding(getValueProto("b", StarlarkInt.of(1)))) |
| .addScope( |
| Scope.newBuilder() |
| .setName("global") |
| .addBinding(getValueProto("c", StarlarkInt.of(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", StarlarkInt.of(1))) |
| .addBinding(getValueProto("c", StarlarkInt.of(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", StarlarkInt.of(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, StarlarkInt.of(5), StarlarkInt.of(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.immutableOf( |
| StarlarkInt.of(1), StarlarkInt.of(2), StarlarkInt.of(3), StarlarkInt.of(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.getDefaultInstance()) |
| .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); |
| Starlark.execFile( |
| 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(); |
| } |
| } |