blob: 4c01400e6b268b4db66d644383877d825d05766e [file]
// 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 net.starlark.java.eval;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import javax.annotation.Nullable;
import net.starlark.java.eval.Debug.ReadyToPause;
import net.starlark.java.eval.Debug.Stepping;
import net.starlark.java.syntax.FileOptions;
import net.starlark.java.syntax.Location;
import net.starlark.java.syntax.ParserInput;
import net.starlark.java.syntax.SyntaxError;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/** Tests of debugging features of StarlarkThread. */
@RunWith(JUnit4.class)
public class StarlarkThreadDebuggingTest {
// TODO(adonovan): rewrite these tests at a higher level.
private static StarlarkThread newThread() {
return StarlarkThread.createTransient(Mutability.create("test"), StarlarkSemantics.DEFAULT);
}
// Executes the definition of a trivial function f and returns the function value.
private static StarlarkFunction defineFunc() throws Exception {
return (StarlarkFunction)
Starlark.execFile(
ParserInput.fromLines("def f(): pass\nf"),
FileOptions.DEFAULT,
Module.create(),
newThread());
}
@Test
public void testListFramesEmptyStack() {
StarlarkThread thread = newThread();
assertThat(Debug.getCallStack(thread)).isEmpty();
assertThat(thread.getCallStack()).isEmpty();
}
/**
* A callable which captures the Starlark call stack at the time of the last call to it.
*
* <p>In Starlark, returns the first positional arg if supplied, or None otherwise.
*/
private static final class StackTracer implements StarlarkCallable {
private final String name;
// Debug.Frame values are mutable (and are expected to mutate during the execution of a thread),
// so we capture their formatted string form instead. (The string form also makes test failures
// more informative.)
@Nullable private ImmutableList<String> debugStack;
@Nullable private ImmutableList<StarlarkThread.CallStackEntry> liteStack;
private StackTracer(String name) {
this.name = name;
}
@Nullable
public ImmutableList<String> getDebugStack() {
return debugStack;
}
@Nullable
public String getCallerDebugFrame() {
return debugStack != null ? debugStack.get(debugStack.size() - 2) : null;
}
@Nullable
public ImmutableList<StarlarkThread.CallStackEntry> getLiteStack() {
return liteStack;
}
@Override
public String getName() {
return name;
}
@Override
public Object call(StarlarkThread thread, Tuple args, Dict<String, Object> kwargs) {
debugStack =
Debug.getCallStack(thread).stream()
.map(this::formatDebugFrame)
.collect(toImmutableList());
liteStack = thread.getCallStack();
return Iterables.getFirst(args, Starlark.NONE);
}
private String formatDebugFrame(Debug.Frame fr) {
return String.format(
"%s @ %s local=%s", fr.getFunction().getName(), fr.getLocation(), fr.getLocals());
}
@Override
public Location getLocation() {
return Location.BUILTIN;
}
@Override
public String toString() {
return "<stack tracer>";
}
}
@Test
public void testListFramesFromBuiltin() throws Exception {
// f is a built-in that captures the stack using the Debugger API.
StackTracer f = new StackTracer("f");
// Set up global environment.
Module module =
Module.withPredeclared(StarlarkSemantics.DEFAULT, ImmutableMap.of("a", 1, "b", 2, "f", f));
// Execute a small file that calls f.
ParserInput input =
ParserInput.fromString(
"""
def g(a, y, z): # shadows global a
f()
g(4, 5, 6)
""",
"main.star");
Starlark.execFile(input, FileOptions.DEFAULT, module, newThread());
assertThat(f.getDebugStack())
.containsExactly(
// location is paren of g(4, 5, 6) call:
"<toplevel> @ main.star:4:2 local={}",
// location is paren of "f()" call:
"g @ main.star:2:6 local={a=4, y=5, z=6}",
// location is "current PC" in f.
"f @ <builtin> local={}")
.inOrder();
// Same, with "lite" stack API.
assertThat(f.getLiteStack().toString()) // an ImmutableList<StarlarkThread.CallStackEntry>
.isEqualTo("[<toplevel>@main.star:4:2, g@main.star:2:6, f@<builtin>]");
// TODO(adonovan): more tests:
// - a stack containing functions defined in different modules.
// - changing environment at various program points within a function.
}
@Test
public void comprehensionVariables() throws Exception {
// Tracers for capturing the stack using the Debugger API.
StackTracer f = new StackTracer("f");
StackTracer g = new StackTracer("g");
StackTracer h = new StackTracer("h");
StackTracer i = new StackTracer("i");
StackTracer j = new StackTracer("j");
StackTracer k = new StackTracer("k");
Module module =
Module.withPredeclared(
StarlarkSemantics.DEFAULT,
ImmutableMap.of("f", f, "g", g, "h", h, "i", i, "j", j, "k", k));
ParserInput input =
ParserInput.fromString(
"""
def foo(x):
x += [[j(x) for x in i(x)] + h(x) for x in f(x) if g(x)]
return k(x)
foo([[1]])
""",
"main.star");
Starlark.execFile(input, FileOptions.DEFAULT, module, newThread());
// f is in the outer comprehension's first for clause, and sees foo's local x
assertThat(f.getCallerDebugFrame()).isEqualTo("foo @ main.star:2:49 local={x=[[1]]}");
// g and h see the outer comprehension's x
assertThat(g.getCallerDebugFrame()).isEqualTo("foo @ main.star:2:57 local={x=[1]}");
assertThat(h.getCallerDebugFrame()).isEqualTo("foo @ main.star:2:35 local={x=[1]}");
// i is in the inner comprehension's first for clause, and so sees the outer comprehension's x
assertThat(i.getCallerDebugFrame()).isEqualTo("foo @ main.star:2:27 local={x=[1]}");
// j sees the inner comprehension's x
assertThat(j.getCallerDebugFrame()).isEqualTo("foo @ main.star:2:13 local={x=1}");
// k is outside the comprehensions' scope, and sees the final value of foo's local x
assertThat(k.getCallerDebugFrame()).isEqualTo("foo @ main.star:3:13 local={x=[[1], [1, 1]]}");
}
@Test
public void testStepIntoFunction() throws Exception {
StarlarkThread thread = newThread();
ReadyToPause predicate = Debug.stepControl(thread, Stepping.INTO);
thread.push(defineFunc());
assertThat(predicate.test(thread)).isTrue();
}
@Test
public void testStepIntoFallsBackToStepOver() {
// test that when stepping into, we'll fall back to stopping at the next statement in the
// current frame
StarlarkThread thread = newThread();
ReadyToPause predicate = Debug.stepControl(thread, Stepping.INTO);
assertThat(predicate.test(thread)).isTrue();
}
@Test
public void testStepIntoFallsBackToStepOut() throws Exception {
// test that when stepping into, we'll fall back to stopping when exiting the current frame
StarlarkThread thread = newThread();
thread.push(defineFunc());
ReadyToPause predicate = Debug.stepControl(thread, Stepping.INTO);
thread.pop();
assertThat(predicate.test(thread)).isTrue();
}
@Test
public void testStepOverFunction() throws Exception {
StarlarkThread thread = newThread();
ReadyToPause predicate = Debug.stepControl(thread, Stepping.OVER);
thread.push(defineFunc());
assertThat(predicate.test(thread)).isFalse();
thread.pop();
assertThat(predicate.test(thread)).isTrue();
}
@Test
public void testStepOverFallsBackToStepOut() throws Exception {
// test that when stepping over, we'll fall back to stopping when exiting the current frame
StarlarkThread thread = newThread();
thread.push(defineFunc());
ReadyToPause predicate = Debug.stepControl(thread, Stepping.OVER);
thread.pop();
assertThat(predicate.test(thread)).isTrue();
}
@Test
public void testStepOutOfInnerFrame() throws Exception {
StarlarkThread thread = newThread();
thread.push(defineFunc());
ReadyToPause predicate = Debug.stepControl(thread, Stepping.OUT);
assertThat(predicate.test(thread)).isFalse();
thread.pop();
assertThat(predicate.test(thread)).isTrue();
}
@Test
public void testStepOutOfOutermostFrame() {
StarlarkThread thread = newThread();
assertThat(Debug.stepControl(thread, Stepping.OUT)).isNull();
}
@Test
public void testStepControlWithNoSteppingReturnsNull() {
StarlarkThread thread = newThread();
assertThat(Debug.stepControl(thread, Stepping.NONE)).isNull();
}
@Test
public void testEvaluateVariableInScope() throws Exception {
Module module =
Module.withPredeclared(StarlarkSemantics.DEFAULT, ImmutableMap.of("a", StarlarkInt.of(1)));
StarlarkThread thread = newThread();
Object a = Starlark.execFile(ParserInput.fromLines("a"), FileOptions.DEFAULT, module, thread);
assertThat(a).isEqualTo(StarlarkInt.of(1));
}
@Test
public void testEvaluateVariableNotInScopeFails() {
Module module = Module.create();
SyntaxError.Exception e =
assertThrows(
SyntaxError.Exception.class,
() ->
Starlark.execFile(
ParserInput.fromLines("b"), FileOptions.DEFAULT, module, newThread()));
assertThat(e).hasMessageThat().isEqualTo("name 'b' is not defined");
}
@Test
public void testEvaluateExpressionOnVariableInScope() throws Exception {
StarlarkThread thread = newThread();
Module module =
Module.withPredeclared(
StarlarkSemantics.DEFAULT, /*predeclared=*/ ImmutableMap.of("a", "string"));
assertThat(
Starlark.execFile(
ParserInput.fromLines("a.startswith('str')"), FileOptions.DEFAULT, module, thread))
.isEqualTo(true);
Starlark.execFile(ParserInput.fromLines("a = 1"), FileOptions.DEFAULT, module, thread);
assertThat(Starlark.execFile(ParserInput.fromLines("a"), FileOptions.DEFAULT, module, thread))
.isEqualTo(StarlarkInt.of(1));
}
}