blob: 403fbcec34b76e9e4efd12b9d4eb4b73aec82f52 [file] [log] [blame]
// Copyright 2015 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.syntax;
import static com.google.common.truth.Truth.assertThat;
import static com.google.devtools.build.lib.testutil.MoreAsserts.assertThrows;
import static java.util.stream.Collectors.joining;
import com.google.common.collect.ImmutableCollection;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.devtools.build.lib.analysis.test.AnalysisFailure;
import com.google.devtools.build.lib.analysis.test.AnalysisFailureInfo;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
import com.google.devtools.build.lib.events.Location;
import com.google.devtools.build.lib.packages.NativeInfo;
import com.google.devtools.build.lib.packages.NativeProvider;
import com.google.devtools.build.lib.packages.StructProvider;
import com.google.devtools.build.lib.skylarkinterface.Param;
import com.google.devtools.build.lib.skylarkinterface.ParamType;
import com.google.devtools.build.lib.skylarkinterface.SkylarkCallable;
import com.google.devtools.build.lib.skylarkinterface.SkylarkGlobalLibrary;
import com.google.devtools.build.lib.skylarkinterface.SkylarkModule;
import com.google.devtools.build.lib.syntax.util.EvaluationTestCase;
import java.util.List;
import java.util.Map;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/** Tests of Starlark evaluation. */
// There is no clear distinction between this and EvaluationTest.
// TODO(adonovan): reorganize.
@SkylarkGlobalLibrary // required for @SkylarkCallable-annotated methods
@RunWith(JUnit4.class)
public final class SkylarkEvaluationTest extends EvaluationTestCase {
@Immutable
static class Bad {
Bad () {
}
}
@SkylarkCallable(name = "foobar", documented = false)
public String foobar() {
return "foobar";
}
@SkylarkCallable(name = "interrupted_function", documented = false)
public NoneType interruptedFunction() throws InterruptedException {
throw new InterruptedException();
}
private static final NativeProvider<NativeInfoMock> CONSTRUCTOR =
new NativeProvider<NativeInfoMock>(NativeInfoMock.class, "native_info_mock") {};
@SkylarkModule(name = "Mock", doc = "")
class NativeInfoMock extends NativeInfo {
public NativeInfoMock() {
super(CONSTRUCTOR);
}
@SkylarkCallable(name = "callable_string", documented = false, structField = false)
public String callableString() {
return "a";
}
@SkylarkCallable(name = "struct_field_string", documented = false, structField = true)
public String structFieldString() {
return "a";
}
@SkylarkCallable(name = "struct_field_callable", documented = false, structField = true)
public BuiltinCallable structFieldCallable() {
return new BuiltinCallable(SkylarkEvaluationTest.this, "foobar");
}
@SkylarkCallable(
name = "struct_field_none",
documented = false,
structField = true,
allowReturnNones = true
)
public String structFieldNone() {
return null;
}
}
@SkylarkModule(name = "Mock", doc = "")
class Mock implements StarlarkValue {
@SkylarkCallable(
name = "MockFn",
selfCall = true,
documented = false,
parameters = {
@Param(name = "pos", positional = true, type = String.class),
})
public String selfCall(String myName) {
return "I'm a mock named " + myName;
}
@SkylarkCallable(
name = "value_of",
parameters = {@Param(name = "str", type = String.class)},
documented = false)
public Integer valueOf(String str) {
return Integer.valueOf(str);
}
@SkylarkCallable(name = "is_empty",
parameters = { @Param(name = "str", type = String.class) },
documented = false)
public Boolean isEmpty(String str) {
return str.isEmpty();
}
public void value() {}
@SkylarkCallable(name = "return_bad", documented = false)
public Bad returnBad() {
return new Bad(); // not a legal Starlark value
}
@SkylarkCallable(name = "struct_field", documented = false, structField = true)
public String structField() {
return "a";
}
@SkylarkCallable(
name = "struct_field_with_extra",
documented = false,
structField = true,
useStarlarkSemantics = true)
public String structFieldWithExtra(StarlarkSemantics sem) {
return "struct_field_with_extra("
+ (sem != null)
+ ")";
}
@SkylarkCallable(name = "struct_field_callable", documented = false, structField = true)
public Object structFieldCallable() {
return new BuiltinCallable(SkylarkEvaluationTest.this, "foobar");
}
@SkylarkCallable(name = "interrupted_struct_field", documented = false, structField = true)
public Object structFieldInterruptedCallable() throws InterruptedException {
throw new InterruptedException();
}
@SkylarkCallable(name = "function", documented = false, structField = false)
public String function() {
return "a";
}
@SuppressWarnings("unused")
@SkylarkCallable(
name = "nullfunc_failing",
parameters = {
@Param(name = "p1", type = String.class),
@Param(name = "p2", type = Integer.class),
},
documented = false,
allowReturnNones = false)
public StarlarkValue nullfuncFailing(String p1, Integer p2) {
return null;
}
@SkylarkCallable(name = "nullfunc_working", documented = false, allowReturnNones = true)
public StarlarkValue nullfuncWorking() {
return null;
}
@SkylarkCallable(name = "voidfunc", documented = false)
public void voidfunc() {}
@SkylarkCallable(name = "string_list", documented = false)
public ImmutableList<String> stringList() {
return ImmutableList.<String>of("a", "b");
}
@SkylarkCallable(name = "string", documented = false)
public String string() {
return "a";
}
@SkylarkCallable(name = "string_list_dict", documented = false)
public Map<String, List<String>> stringListDict() {
return ImmutableMap.of("a", ImmutableList.of("b", "c"));
}
@SkylarkCallable(
name = "legacy_method",
documented = false,
parameters = {
@Param(name = "pos", positional = true, type = Boolean.class),
@Param(name = "legacyNamed", type = Boolean.class, positional = true, named = false,
legacyNamed = true),
@Param(name = "named", type = Boolean.class, positional = false, named = true),
})
public String legacyMethod(Boolean pos, Boolean legacyNamed, Boolean named) {
return "legacy_method("
+ pos
+ ", "
+ legacyNamed
+ ", "
+ named
+ ")";
}
@SkylarkCallable(
name = "with_params",
documented = false,
parameters = {
@Param(name = "pos1"),
@Param(name = "pos2", defaultValue = "False", type = Boolean.class),
@Param(
name = "posOrNamed",
defaultValue = "False",
type = Boolean.class,
positional = true,
named = true),
@Param(name = "named", type = Boolean.class, positional = false, named = true),
@Param(
name = "optionalNamed",
type = Boolean.class,
defaultValue = "False",
positional = false,
named = true),
@Param(
name = "nonNoneable",
type = Object.class,
defaultValue = "\"a\"",
positional = false,
named = true),
@Param(
name = "noneable",
type = Integer.class,
defaultValue = "None",
noneable = true,
positional = false,
named = true),
@Param(
name = "multi",
allowedTypes = {
@ParamType(type = String.class),
@ParamType(type = Integer.class),
@ParamType(type = Sequence.class, generic1 = Integer.class),
},
defaultValue = "None",
noneable = true,
positional = false,
named = true)
})
public String withParams(
Integer pos1,
boolean pos2,
boolean posOrNamed,
boolean named,
boolean optionalNamed,
Object nonNoneable,
Object noneable,
Object multi) {
return "with_params("
+ pos1
+ ", "
+ pos2
+ ", "
+ posOrNamed
+ ", "
+ named
+ ", "
+ optionalNamed
+ ", "
+ nonNoneable
+ (noneable != Starlark.NONE ? ", " + noneable : "")
+ (multi != Starlark.NONE ? ", " + multi : "")
+ ")";
}
@SkylarkCallable(name = "with_extra", documented = false, useStarlarkThread = true)
public String withExtraInterpreterParams(StarlarkThread thread) {
return "with_extra(" + thread.getCallerLocation().line() + ")";
}
@SkylarkCallable(
name = "with_params_and_extra",
documented = false,
parameters = {
@Param(name = "pos1"),
@Param(name = "pos2", defaultValue = "False", type = Boolean.class),
@Param(
name = "posOrNamed",
defaultValue = "False",
type = Boolean.class,
positional = true,
named = true),
@Param(name = "named", type = Boolean.class, positional = false, named = true),
@Param(
name = "optionalNamed",
type = Boolean.class,
defaultValue = "False",
positional = false,
named = true),
@Param(
name = "nonNoneable",
type = Object.class,
defaultValue = "\"a\"",
positional = false,
named = true),
@Param(
name = "noneable",
type = Integer.class,
defaultValue = "None",
noneable = true,
positional = false,
named = true),
@Param(
name = "multi",
allowedTypes = {
@ParamType(type = String.class),
@ParamType(type = Integer.class),
@ParamType(type = Sequence.class, generic1 = Integer.class),
},
defaultValue = "None",
noneable = true,
positional = false,
named = true)
},
useStarlarkThread = true)
public String withParamsAndExtraInterpreterParams(
Integer pos1,
boolean pos2,
boolean posOrNamed,
boolean named,
boolean optionalNamed,
Object nonNoneable,
Object noneable,
Object multi,
StarlarkThread thread) {
return "with_params_and_extra("
+ pos1
+ ", "
+ pos2
+ ", "
+ posOrNamed
+ ", "
+ named
+ ", "
+ optionalNamed
+ ", "
+ nonNoneable
+ (noneable != Starlark.NONE ? ", " + noneable : "")
+ (multi != Starlark.NONE ? ", " + multi : "")
+ ", "
+ thread.getCallerLocation().line()
+ ")";
}
@SkylarkCallable(name = "proxy_methods_object",
doc = "Returns a struct containing all callable method objects of this mock",
allowReturnNones = true)
public ClassObject proxyMethodsObject() {
ImmutableMap.Builder<String, Object> builder = new ImmutableMap.Builder<>();
Starlark.addMethods(builder, this);
return StructProvider.STRUCT.create(builder.build(), "no native callable '%s'");
}
@SkylarkCallable(
name = "with_args_and_thread",
documented = false,
parameters = {
@Param(name = "pos1", type = Integer.class),
@Param(name = "pos2", defaultValue = "False", type = Boolean.class),
@Param(name = "named", type = Boolean.class, positional = false, named = true),
},
extraPositionals = @Param(name = "args"),
useStarlarkThread = true)
public String withArgsAndThread(
Integer pos1, boolean pos2, boolean named, Sequence<?> args, StarlarkThread thread) {
String argsString = debugPrintArgs(args);
return "with_args_and_thread("
+ pos1
+ ", "
+ pos2
+ ", "
+ named
+ ", "
+ argsString
+ ")";
}
@SkylarkCallable(
name = "with_kwargs",
documented = false,
parameters = {
@Param(name = "pos", defaultValue = "False", type = Boolean.class),
@Param(name = "named", type = Boolean.class, positional = false, named = true),
},
extraKeywords = @Param(name = "kwargs"))
public String withKwargs(boolean pos, boolean named, Dict<String, Object> kwargs) {
String kwargsString =
"kwargs("
+ kwargs
.entrySet()
.stream()
.map(entry -> entry.getKey() + "=" + entry.getValue())
.collect(joining(", "))
+ ")";
return "with_kwargs(" + pos + ", " + named + ", " + kwargsString + ")";
}
@SkylarkCallable(
name = "with_args_and_kwargs",
documented = false,
parameters = {
@Param(name = "foo", named = true, positional = true, type = String.class),
},
extraPositionals = @Param(name = "args"),
extraKeywords = @Param(name = "kwargs"))
public String withArgsAndKwargs(String foo, Tuple<Object> args, Dict<String, Object> kwargs) {
String argsString = debugPrintArgs(args);
String kwargsString =
"kwargs("
+ kwargs
.entrySet()
.stream()
.map(entry -> entry.getKey() + "=" + entry.getValue())
.collect(joining(", "))
+ ")";
return "with_args_and_kwargs(" + foo + ", " + argsString + ", " + kwargsString + ")";
}
@SkylarkCallable(name = "raise_unchecked_exception", documented = false)
public void raiseUncheckedException() {
throw new InternalError("buggy code");
}
@Override
public String toString() {
return "<mock>";
}
}
private static String debugPrintArgs(Iterable<?> args) {
Printer p = Printer.getPrinter();
p.append("args(");
String sep = "";
for (Object arg : args) {
p.append(sep).debugPrint(arg);
sep = ", ";
}
return p.append(")").toString();
}
@SkylarkModule(name = "MockInterface", doc = "")
static interface MockInterface extends StarlarkValue {
@SkylarkCallable(name = "is_empty_interface",
parameters = { @Param(name = "str", type = String.class) },
documented = false)
public Boolean isEmptyInterface(String str);
}
@SkylarkModule(name = "MockSubClass", doc = "")
final class MockSubClass extends Mock implements MockInterface {
@Override
public Boolean isEmpty(String str) {
return str.isEmpty();
}
@Override
public Boolean isEmptyInterface(String str) {
return str.isEmpty();
}
}
@SkylarkModule(name = "MockClassObject", documented = false, doc = "")
static final class MockClassObject implements ClassObject, StarlarkValue {
@Override
public Object getValue(String name) {
switch (name) {
case "field": return "a";
case "nset":
return NestedSetBuilder.stableOrder().build(); // not a legal Starlark value
default: return null;
}
}
@Override
public ImmutableCollection<String> getFieldNames() {
return ImmutableList.of("field", "nset");
}
@Override
public String getErrorMessageForUnknownField(String name) {
return null;
}
}
@SkylarkModule(name = "ParamterizedMock", doc = "")
static interface ParameterizedApi<ObjectT> extends StarlarkValue {
@SkylarkCallable(
name = "method",
documented = false,
parameters = {
@Param(name = "foo", named = true, positional = true, type = Object.class),
}
)
public ObjectT method(ObjectT o);
}
static final class ParameterizedMock implements ParameterizedApi<String> {
@Override
public String method(String o) {
return o;
}
}
// Verifies that a method implementation overriding a parameterized annotated interface method
// is still treated as skylark-callable. Concretely, method() below should be treated as
// callable even though its method signature isn't an *exact* match of the annotated method
// declaration, due to the interface's method declaration being generic.
@Test
public void testParameterizedMock() throws Exception {
new Scenario()
.update("mock", new ParameterizedMock())
.setUp("result = mock.method('bar')")
.testLookup("result", "bar");
}
@Test
public void testSimpleIf() throws Exception {
new Scenario()
.setUp("def foo():", " a = 0", " x = 0", " if x: a = 5", " return a", "a = foo()")
.testLookup("a", 0);
}
@Test
public void testIfPass() throws Exception {
new Scenario()
.setUp("def foo():", " a = 1", " x = True", " if x: pass", " return a", "a = foo()")
.testLookup("a", 1);
}
@Test
public void testNestedIf() throws Exception {
executeNestedIf(0, 0, 0);
executeNestedIf(1, 0, 3);
executeNestedIf(1, 1, 5);
}
private void executeNestedIf(int x, int y, int expected) throws Exception {
String fun = String.format("foo%s%s", x, y);
new Scenario()
.setUp(
"def " + fun + "():",
" x = " + x,
" y = " + y,
" a = 0",
" b = 0",
" if x:",
" if y:",
" a = 2",
" b = 3",
" return a + b",
"x = " + fun + "()")
.testLookup("x", expected);
}
@Test
public void testIfElse() throws Exception {
executeIfElse("foo", "something", 2);
executeIfElse("bar", "", 3);
}
private void executeIfElse(String fun, String y, int expected) throws Exception {
new Scenario()
.setUp(
"def " + fun + "():",
" y = '" + y + "'",
" x = 5",
" if x:",
" if y: a = 2",
" else: a = 3",
" return a",
"z = " + fun + "()")
.testLookup("z", expected);
}
@Test
public void testIfElifElse_IfExecutes() throws Exception {
execIfElifElse(1, 0, 1);
}
@Test
public void testIfElifElse_ElifExecutes() throws Exception {
execIfElifElse(0, 1, 2);
}
@Test
public void testIfElifElse_ElseExecutes() throws Exception {
execIfElifElse(0, 0, 3);
}
private void execIfElifElse(int x, int y, int v) throws Exception {
new Scenario()
.setUp(
"def foo():",
" x = " + x + "",
" y = " + y + "",
" if x:",
" return 1",
" elif y:",
" return 2",
" else:",
" return 3",
"v = foo()")
.testLookup("v", v);
}
@Test
public void testForOnList() throws Exception {
new Scenario()
.setUp(
"def foo():",
" s = ''",
" for i in ['hello', ' ', 'world']:",
" s = s + i",
" return s",
"s = foo()")
.testLookup("s", "hello world");
}
@Test
public void testForAssignmentList() throws Exception {
new Scenario()
.setUp(
"def foo():",
" d = ['a', 'b', 'c']",
" s = ''",
" for i in d:",
" s = s + i",
" d = ['d', 'e', 'f']", // check that we use the old list
" return s",
"s = foo()")
.testLookup("s", "abc");
}
@Test
public void testForAssignmentDict() throws Exception {
new Scenario()
.setUp(
"def func():",
" d = {'a' : 1, 'b' : 2, 'c' : 3}",
" s = ''",
" for i in d:",
" s = s + i",
" d = {'d' : 1, 'e' : 2, 'f' : 3}",
" return s",
"s = func()")
.testLookup("s", "abc");
}
@Test
public void testForUpdateList() throws Exception {
new Scenario()
.setUp(
"def foo():",
" xs = [1, 2, 3]",
" for x in xs:",
" if x == 1:",
" xs.append(10)")
.testIfErrorContains("trying to mutate a locked object", "foo()");
}
@Test
public void testForUpdateDict() throws Exception {
new Scenario()
.setUp("def foo():", " d = {'a': 1, 'b': 2, 'c': 3}", " for k in d:", " d[k] *= 2")
.testIfErrorContains("trying to mutate a locked object", "foo()");
}
@Test
public void testForUnlockedAfterBreak() throws Exception {
new Scenario()
.setUp(
"def foo():",
" xs = [1, 2]",
" for x in xs:",
" break",
" xs.append(3)",
" return xs")
.testEval("foo()", "[1, 2, 3]");
}
@Test
public void testForNestedOnSameListStillLocked() throws Exception {
new Scenario()
.setUp(
"def foo():",
" xs = [1, 2]",
" ys = []",
" for x1 in xs:",
" for x2 in xs:",
" ys.append(x1 * x2)",
" xs.append(4)",
" return ys")
.testIfErrorContains("trying to mutate a locked object", "foo()");
}
@Test
public void testForNestedOnSameListErrorMessage() throws Exception {
new Scenario()
.setUp(
"def foo():",
" xs = [1, 2]",
" ys = []",
" for x1 in xs:",
" for x2 in xs:",
" ys.append(x1 * x2)",
" xs.append(4)",
" return ys"
// No file name in message, due to how test is set up.
)
.testIfErrorContains("Object locked at the following location(s): :4:3, :5:5", "foo()");
}
@Test
public void testForNestedOnSameListUnlockedAtEnd() throws Exception {
new Scenario()
.setUp(
"def foo():",
" xs = [1, 2]",
" ys = []",
" for x1 in xs:",
" for x2 in xs:",
" ys.append(x1 * x2)",
" xs.append(4)",
" return ys")
.testEval("foo()", "[1, 2, 2, 4]");
}
@Test
public void testForNestedWithListCompGood() throws Exception {
new Scenario()
.setUp(
"def foo():",
" xs = [1, 2]",
" ys = []",
" for x in xs:",
" zs = [None for x in xs for y in (ys.append(x) or ys)]",
" return ys")
.testEval("foo()", "[1, 2, 1, 2]");
}
@Test
public void testForNestedWithListCompBad() throws Exception {
new Scenario()
.setUp(
"def foo():",
" xs = [1, 2, 3]",
" ys = []",
" for x in xs:",
" zs = [None for x in xs for y in (xs.append(x) or ys)]",
" return ys")
.testIfErrorContains("trying to mutate a locked object", "foo()");
}
@Test
public void testForDeepUpdate() throws Exception {
// Check that indirectly reachable values can still be manipulated as normal.
new Scenario()
.setUp(
"def foo():",
" xs = [['a'], ['b'], ['c']]",
" ys = []",
" for x in xs:",
" for y in x:",
" ys.append(y)",
" xs[2].append(x[0])",
" return ys",
"ys = foo()")
.testLookup("ys", StarlarkList.of(null, "a", "b", "c", "a", "b"));
}
@Test
public void testForNotIterable() throws Exception {
new Scenario()
.update("mock", new Mock())
.testIfErrorContains(
"type 'int' is not iterable",
"def func():",
" for i in mock.value_of('1'): a = i",
"func()\n");
}
@Test
public void testForStringNotIterable() throws Exception {
new Scenario()
.update("mock", new Mock())
.testIfErrorContains(
"type 'string' is not iterable", "def func():", " for i in 'abc': a = i", "func()\n");
}
@Test
public void testForOnDictionary() throws Exception {
new Scenario()
.setUp(
"def foo():",
" d = {1: 'a', 2: 'b', 3: 'c'}",
" s = ''",
" for i in d: s = s + d[i]",
" return s",
"s = foo()")
.testLookup("s", "abc");
}
@Test
public void testBadDictKey() throws Exception {
new Scenario().testIfErrorContains("unhashable type: 'list'", "{ [1, 2]: [3, 4] }");
}
@Test
public void testForLoopReuseVariable() throws Exception {
new Scenario()
.setUp(
"def foo():",
" s = ''",
" for i in ['a', 'b']:",
" for i in ['c', 'd']: s = s + i",
" return s",
"s = foo()")
.testLookup("s", "cdcd");
}
@Test
public void testForLoopMultipleVariables() throws Exception {
new Scenario()
.setUp(
"def foo():",
" s = ''",
" for [i, j] in [[1, 2], [3, 4]]:",
" s = s + str(i) + str(j) + '.'",
" return s",
"s = foo()")
.testLookup("s", "12.34.");
}
@Test
public void testForLoopBreak() throws Exception {
simpleFlowTest("break", 1);
}
@Test
public void testForLoopContinue() throws Exception {
simpleFlowTest("continue", 10);
}
@SuppressWarnings("unchecked")
private void simpleFlowTest(String statement, int expected) throws Exception {
exec(
"def foo():",
" s = 0",
" hit = 0",
" for i in range(0, 10):",
" s = s + 1",
" " + statement + "",
" hit = 1",
" return [s, hit]",
"x = foo()");
assertThat((Iterable<Object>) lookup("x")).containsExactly(expected, 0).inOrder();
}
@Test
public void testForLoopBreakFromDeeperBlock() throws Exception {
flowFromDeeperBlock("break", 1);
flowFromNestedBlocks("break", 29);
}
@Test
public void testForLoopContinueFromDeeperBlock() throws Exception {
flowFromDeeperBlock("continue", 5);
flowFromNestedBlocks("continue", 39);
}
private void flowFromDeeperBlock(String statement, int expected) throws Exception {
exec(
"def foo():",
" s = 0",
" for i in range(0, 10):",
" if i % 2 != 0:",
" " + statement + "",
" s = s + 1",
" return s",
"x = foo()");
assertThat(lookup("x")).isEqualTo(expected);
}
private void flowFromNestedBlocks(String statement, int expected) throws Exception {
exec(
"def foo2():",
" s = 0",
" for i in range(1, 41):",
" if i % 2 == 0:",
" if i % 3 == 0:",
" if i % 5 == 0:",
" " + statement + "",
" s = s + 1",
" return s",
"y = foo2()");
assertThat(lookup("y")).isEqualTo(expected);
}
@Test
public void testNestedForLoopsMultipleBreaks() throws Exception {
nestedLoopsTest("break", 2, 6, 6);
}
@Test
public void testNestedForLoopsMultipleContinues() throws Exception {
nestedLoopsTest("continue", 4, 20, 20);
}
@SuppressWarnings("unchecked")
private void nestedLoopsTest(String statement, Integer outerExpected, int firstExpected,
int secondExpected) throws Exception {
exec(
"def foo():",
" outer = 0",
" first = 0",
" second = 0",
" for i in range(0, 5):",
" for j in range(0, 5):",
" if j == 2:",
" " + statement + "",
" first = first + 1",
" for k in range(0, 5):",
" if k == 2:",
" " + statement + "",
" second = second + 1",
" if i == 2:",
" " + statement + "",
" outer = outer + 1",
" return [outer, first, second]",
"x = foo()");
assertThat((Iterable<Object>) lookup("x"))
.containsExactly(outerExpected, firstExpected, secondExpected).inOrder();
}
@Test
public void testForLoopBreakError() throws Exception {
flowStatementInsideFunction("break");
flowStatementAfterLoop("break");
}
@Test
public void testForLoopContinueError() throws Exception {
flowStatementInsideFunction("continue");
flowStatementAfterLoop("continue");
}
// TODO(adonovan): move this and all tests that use it to Validation tests.
private void assertValidationError(String expectedError, final String... lines) throws Exception {
SyntaxError error = assertThrows(SyntaxError.class, () -> exec(lines));
assertThat(error).hasMessageThat().contains(expectedError);
}
private void flowStatementInsideFunction(String statement) throws Exception {
assertValidationError(
statement + " statement must be inside a for loop",
//
"def foo():",
" " + statement,
"x = foo()");
}
private void flowStatementAfterLoop(String statement) throws Exception {
assertValidationError(
statement + " statement must be inside a for loop",
//
"def foo2():",
" for i in range(0, 3):",
" pass",
" " + statement,
"y = foo2()");
}
@Test
public void testStructMembersAreImmutable() throws Exception {
assertValidationError("cannot assign to 's.x'", "s = struct(x = 'a')", "s.x = 'b'\n");
}
@Test
public void testNoneAssignment() throws Exception {
new Scenario()
.setUp("def foo(x=None):", " x = 1", " x = None", " return 2", "s = foo()")
.testLookup("s", 2);
}
@Test
public void testReassignment() throws Exception {
exec(
"def foo(x=None):", //
" x = 1",
" x = [1, 2]",
" x = 'str'",
" return x",
"s = foo()");
assertThat(lookup("s")).isEqualTo("str");
}
@Test
public void testJavaCalls() throws Exception {
new Scenario()
.update("mock", new Mock())
.setUp("b = mock.is_empty('a')")
.testLookup("b", Boolean.FALSE);
}
@Test
public void testJavaCallsOnSubClass() throws Exception {
new Scenario()
.update("mock", new MockSubClass())
.setUp("b = mock.is_empty('a')")
.testLookup("b", Boolean.FALSE);
}
@Test
public void testJavaCallsOnInterface() throws Exception {
new Scenario()
.update("mock", new MockSubClass())
.setUp("b = mock.is_empty_interface('a')")
.testLookup("b", Boolean.FALSE);
}
@Test
public void testJavaCallsNotSkylarkCallable() throws Exception {
new Scenario()
.update("mock", new Mock())
.testIfExactError("'Mock' value has no field or method 'value'", "mock.value()");
}
@Test
public void testNoOperatorIndex() throws Exception {
new Scenario()
.update("mock", new Mock())
.testIfExactError("type 'Mock' has no operator [](int)", "mock[2]");
}
@Test
public void testJavaCallsNoMethod() throws Exception {
new Scenario()
.update("mock", new Mock())
.testIfExactError("'Mock' value has no field or method 'bad'", "mock.bad()");
}
@Test
public void testJavaCallsNoMethodErrorMsg() throws Exception {
new Scenario()
.testIfExactError("'int' value has no field or method 'bad'", "s = 3.bad('a', 'b', 'c')");
}
@Test
public void testJavaCallWithKwargs() throws Exception {
new Scenario()
.update("mock", new Mock())
.testIfExactError(
"'Mock' value has no field or method 'isEmpty'", "mock.isEmpty(str='abc')");
}
@Test
public void testStringListDictValues() throws Exception {
new Scenario()
.update("mock", new Mock())
.setUp(
"def func(mock):",
" for i, v in mock.string_list_dict().items():",
" modified_list = v + ['extra_string']",
" return modified_list",
"m = func(mock)")
.testLookup("m", StarlarkList.of(null, "b", "c", "extra_string"));
}
@Test
public void testProxyMethodsObject() throws Exception {
new Scenario()
.update("mock", new Mock())
.setUp("m = mock.proxy_methods_object()", "b = m.with_params(1, True, named=True)")
.testLookup("b", "with_params(1, true, false, true, false, a)");
}
@Test
public void testLegacyNamed() throws Exception {
new Scenario("--incompatible_restrict_named_params=false")
.update("mock", new Mock())
.setUp("b = mock.legacy_method(True, legacyNamed=True, named=True)")
.testLookup("b", "legacy_method(true, true, true)");
new Scenario("--incompatible_restrict_named_params=false")
.update("mock", new Mock())
.setUp("b = mock.legacy_method(True, True, named=True)")
.testLookup("b", "legacy_method(true, true, true)");
// Verify legacyNamed also works with proxy method objects.
new Scenario("--incompatible_restrict_named_params=false")
.update("mock", new Mock())
.setUp(
"m = mock.proxy_methods_object()",
"b = m.legacy_method(True, legacyNamed=True, named=True)")
.testLookup("b", "legacy_method(true, true, true)");
new Scenario("--incompatible_restrict_named_params=false")
.update("mock", new Mock())
.setUp("m = mock.proxy_methods_object()", "b = m.legacy_method(True, True, named=True)")
.testLookup("b", "legacy_method(true, true, true)");
}
/**
* This test verifies an error is raised when a method parameter is set both positionally and
* by name.
*/
@Test
public void testArgSpecifiedBothByNameAndPosition() throws Exception {
// in with_params, 'posOrNamed' is positional parameter index 2. So by specifying both
// posOrNamed by name and three positional parameters, there is a conflict.
new Scenario()
.update("mock", new Mock())
.testIfErrorContains(
"with_params() got multiple values for argument 'posOrNamed'",
"mock.with_params(1, True, True, posOrNamed=True, named=True)");
}
@Test
public void testTooManyPositionalArgs() throws Exception {
new Scenario()
.update("mock", new Mock())
.testIfErrorContains(
"with_params() accepts no more than 3 positional arguments but got 4",
"mock.with_params(1, True, True, 'toomany', named=True)");
new Scenario()
.update("mock", new Mock())
.testIfErrorContains(
"with_params() accepts no more than 3 positional arguments but got 5",
"mock.with_params(1, True, True, 'toomany', 'alsotoomany', named=True)");
new Scenario()
.update("mock", new Mock())
.testIfErrorContains(
"is_empty() accepts no more than 1 positional argument but got 2",
"mock.is_empty('a', 'b')");
}
@Test
public void testJavaCallWithPositionalAndKwargs() throws Exception {
new Scenario()
.update("mock", new Mock())
.setUp("b = mock.with_params(1, True, named=True)")
.testLookup("b", "with_params(1, true, false, true, false, a)");
new Scenario()
.update("mock", new Mock())
.setUp("b = mock.with_params(1, True, named=True, multi=1)")
.testLookup("b", "with_params(1, true, false, true, false, a, 1)");
new Scenario()
.update("mock", new Mock())
.setUp("b = mock.with_params(1, True, named=True, multi='abc')")
.testLookup("b", "with_params(1, true, false, true, false, a, abc)");
new Scenario()
.update("mock", new Mock())
.setUp("b = mock.with_params(1, True, named=True, multi=[1,2,3])")
.testLookup("b", "with_params(1, true, false, true, false, a, [1, 2, 3])");
new Scenario()
.update("mock", new Mock())
.setUp("")
.testIfExactError(
"with_params() missing 1 required named argument: named", "mock.with_params(1, True)");
new Scenario()
.update("mock", new Mock())
.setUp("")
.testIfExactError(
"with_params() missing 1 required named argument: named",
"mock.with_params(1, True, True)");
new Scenario()
.update("mock", new Mock())
.setUp("b = mock.with_params(1, True, True, named=True)")
.testLookup("b", "with_params(1, true, true, true, false, a)");
new Scenario()
.update("mock", new Mock())
.setUp("b = mock.with_params(1, True, named=True, posOrNamed=True)")
.testLookup("b", "with_params(1, true, true, true, false, a)");
new Scenario()
.update("mock", new Mock())
.setUp("b = mock.with_params(1, True, named=True, posOrNamed=True, optionalNamed=True)")
.testLookup("b", "with_params(1, true, true, true, true, a)");
new Scenario()
.update("mock", new Mock())
.setUp("")
.testIfExactError(
"with_params() got unexpected keyword argument 'posornamed' (did you mean"
+ " 'posOrNamed'?)",
"mock.with_params(1, True, named=True, posornamed=True)");
new Scenario()
.update("mock", new Mock())
.setUp("")
.testIfExactError(
"with_params() got unexpected keyword argument 'n'",
"mock.with_params(1, True, named=True, posOrNamed=True, n=2)");
new Scenario()
.update("mock", new Mock())
.setUp("")
.testIfExactError(
"in call to with_params(), parameter 'nonNoneable' cannot be None",
"mock.with_params(1, True, True, named=True, optionalNamed=False, nonNoneable=None)");
new Scenario()
.update("mock", new Mock())
.setUp("")
.testIfExactError(
"in call to with_params(), parameter 'multi' got value of type 'bool', want 'string or"
+ " int or sequence of ints or NoneType'",
"mock.with_params(1, True, named=True, multi=False)");
// We do not enforce list item parameter type constraints.
// Test for this behavior.
new Scenario()
.update("mock", new Mock())
.setUp("b = mock.with_params(1, True, named=True, multi=['a', 'b'])")
.testLookup("b", "with_params(1, true, false, true, false, a, [\"a\", \"b\"])");
}
@Test
public void testNoJavaCallsWithoutSkylark() throws Exception {
new Scenario()
.testIfExactError("'int' value has no field or method 'to_string'", "s = 3.to_string()");
}
@Test
public void testStructAccess() throws Exception {
new Scenario().update("mock", new Mock()).setUp("v = mock.struct_field").testLookup("v", "a");
}
@Test
public void testStructAccessAsFuncallNonCallable() throws Exception {
new Scenario()
.update("mock", new Mock())
.testIfExactError("'string' object is not callable", "v = mock.struct_field()");
}
@Test
public void testSelfCall() throws Exception {
new Scenario()
.update("mock", new Mock())
.setUp("v = mock('bestmock')")
.testLookup("v", "I'm a mock named bestmock");
new Scenario()
.update("mock", new Mock())
.setUp("mockfunction = mock", "v = mockfunction('bestmock')")
.testLookup("v", "I'm a mock named bestmock");
new Scenario()
.update("mock", new Mock())
.testIfErrorContains(
"in call to MockFn(), parameter 'pos' got value of type 'int', want 'string'",
"v = mock(1)");
}
@Test
public void testStructAccessAsFuncall() throws Exception {
new Scenario()
.update("mock", new Mock())
.setUp("v = mock.struct_field_callable()")
.testLookup("v", "foobar");
}
@Test
public void testCallingInterruptedStructField() throws Exception {
update("mock", new Mock());
assertThrows(InterruptedException.class, () -> eval("mock.interrupted_struct_field()"));
}
@Test
public void testCallingInterruptedFunction() throws Exception {
update("interrupted_function", new BuiltinCallable(this, "interrupted_function"));
assertThrows(InterruptedException.class, () -> eval("interrupted_function()"));
}
@Test
public void testCallingMethodThatRaisesUncheckedException() throws Exception {
update("mock", new Mock());
assertThrows(InternalError.class, () -> eval("mock.raise_unchecked_exception()"));
}
@Test
public void testJavaFunctionWithExtraInterpreterParams() throws Exception {
new Scenario()
.update("mock", new Mock())
.setUp("v = mock.with_extra()")
.testLookup("v", "with_extra(1)");
}
@Test
public void testStructFieldWithExtraInterpreterParams() throws Exception {
new Scenario()
.update("mock", new Mock())
.setUp("v = mock.struct_field_with_extra")
.testLookup("v", "struct_field_with_extra(true)");
}
@Test
public void testJavaFunctionWithParamsAndExtraInterpreterParams() throws Exception {
new Scenario()
.update("mock", new Mock())
.setUp("b = mock.with_params_and_extra(1, True, named=True)")
.testLookup("b", "with_params_and_extra(1, true, false, true, false, a, 1)");
}
@Test
public void testJavaFunctionWithExtraArgsAndThread() throws Exception {
new Scenario()
.update("mock", new Mock())
.setUp("b = mock.with_args_and_thread(1, True, 'extraArg1', 'extraArg2', named=True)")
.testLookup("b", "with_args_and_thread(1, true, true, args(extraArg1, extraArg2))");
// Use an args list.
new Scenario()
.update("mock", new Mock())
.setUp(
"myargs = ['extraArg2']",
"b = mock.with_args_and_thread(1, True, 'extraArg1', named=True, *myargs)")
.testLookup("b", "with_args_and_thread(1, true, true, args(extraArg1, extraArg2))");
}
@Test
public void testJavaFunctionWithExtraKwargs() throws Exception {
new Scenario()
.update("mock", new Mock())
.setUp("b = mock.with_kwargs(True, extraKey1=True, named=True, extraKey2='x')")
.testLookup("b", "with_kwargs(true, true, kwargs(extraKey1=true, extraKey2=x))");
// Use a kwargs dict.
new Scenario()
.update("mock", new Mock())
.setUp(
"mykwargs = {'extraKey2':'x', 'named':True}",
"b = mock.with_kwargs(True, extraKey1=True, **mykwargs)")
.testLookup("b", "with_kwargs(true, true, kwargs(extraKey1=true, extraKey2=x))");
}
@Test
public void testJavaFunctionWithArgsAndKwargs() throws Exception {
// Foo is used positionally
new Scenario()
.update("mock", new Mock())
.setUp("b = mock.with_args_and_kwargs('foo', 'bar', 'baz', extraKey1=True, extraKey2='x')")
.testLookup(
"b", "with_args_and_kwargs(foo, args(bar, baz), kwargs(extraKey1=true, extraKey2=x))");
// Use an args list and a kwargs dict
new Scenario()
.update("mock", new Mock())
.setUp(
"mykwargs = {'extraKey1':True}",
"myargs = ['baz']",
"b = mock.with_args_and_kwargs('foo', 'bar', extraKey2='x', *myargs, **mykwargs)")
.testLookup(
"b", "with_args_and_kwargs(foo, args(bar, baz), kwargs(extraKey2=x, extraKey1=true))");
// Foo is used by name
new Scenario()
.update("mock", new Mock())
.setUp("b = mock.with_args_and_kwargs(foo='foo', extraKey1=True)")
.testLookup("b", "with_args_and_kwargs(foo, args(), kwargs(extraKey1=true))");
// Empty args and kwargs.
new Scenario()
.update("mock", new Mock())
.setUp("b = mock.with_args_and_kwargs('foo')")
.testLookup("b", "with_args_and_kwargs(foo, args(), kwargs())");
}
@Test
public void testProxyMethodsObjectWithArgsAndKwargs() throws Exception {
// Foo is used positionally
new Scenario()
.update("mock", new Mock())
.setUp(
"m = mock.proxy_methods_object()",
"b = m.with_args_and_kwargs('foo', 'bar', 'baz', extraKey1=True, extraKey2='x')")
.testLookup(
"b", "with_args_and_kwargs(foo, args(bar, baz), kwargs(extraKey1=true, extraKey2=x))");
// Use an args list and a kwargs dict
new Scenario()
.update("mock", new Mock())
.setUp(
"mykwargs = {'extraKey1':True}",
"myargs = ['baz']",
"m = mock.proxy_methods_object()",
"b = m.with_args_and_kwargs('foo', 'bar', extraKey2='x', *myargs, **mykwargs)")
.testLookup(
"b", "with_args_and_kwargs(foo, args(bar, baz), kwargs(extraKey2=x, extraKey1=true))");
// Foo is used by name
new Scenario()
.update("mock", new Mock())
.setUp(
"m = mock.proxy_methods_object()",
"b = m.with_args_and_kwargs(foo='foo', extraKey1=True)")
.testLookup("b", "with_args_and_kwargs(foo, args(), kwargs(extraKey1=true))");
// Empty args and kwargs.
new Scenario()
.update("mock", new Mock())
.setUp("m = mock.proxy_methods_object()", "b = m.with_args_and_kwargs('foo')")
.testLookup("b", "with_args_and_kwargs(foo, args(), kwargs())");
}
@Test
public void testStructAccessOfMethod() throws Exception {
new Scenario().update("mock", new Mock()).testExpression("type(mock.function)", "function");
new Scenario().update("mock", new Mock()).testExpression("mock.function()", "a");
}
@Test
public void testStructAccessTypo() throws Exception {
new Scenario()
.update("mock", new MockClassObject())
.testIfExactError(
"'MockClassObject' value has no field or method 'fild' (did you mean 'field'?)",
"mock.fild");
}
@Test
public void testStructAccessType_nonClassObject() throws Exception {
new Scenario()
.update("mock", new Mock())
.testIfExactError(
"'Mock' value has no field or method 'sturct_field' (did you mean 'struct_field'?)",
"v = mock.sturct_field");
}
@Test
public void testJavaFunctionReturnsIllegalValue() throws Exception {
update("mock", new Mock());
IllegalArgumentException e =
assertThrows(IllegalArgumentException.class, () -> eval("mock.return_bad()"));
assertThat(e)
.hasMessageThat()
.contains(
"cannot expose internal type to Starlark: class"
+ " com.google.devtools.build.lib.syntax.SkylarkEvaluationTest$Bad");
}
@Test
public void testJavaFunctionReturnsNullFails() throws Exception {
update("mock", new Mock());
IllegalStateException e =
assertThrows(IllegalStateException.class, () -> eval("mock.nullfunc_failing('abc', 1)"));
assertThat(e).hasMessageThat().contains("method invocation returned None");
}
@Test
public void testClassObjectAccess() throws Exception {
new Scenario()
.update("mock", new MockClassObject())
.setUp("v = mock.field")
.testLookup("v", "a");
}
@Test
public void testUnionSet() throws Exception {
new Scenario("--incompatible_depset_union=false")
.testExpression("str(depset([1, 3]) | depset([1, 2]))", "depset([1, 2, 3])")
.testExpression("str(depset([1, 2]) | [1, 3])", "depset([1, 2, 3])")
.testIfExactError("unsupported binary operation: int | bool", "2 | False");
}
@Test
public void testSetIsNotIterable() throws Exception {
new Scenario()
.testIfErrorContains("not iterable", "list(depset(['a', 'b']))")
.testIfErrorContains("not iterable", "max(depset([1, 2, 3]))")
.testIfErrorContains(
"unsupported binary operation: int in depset", "1 in depset([1, 2, 3])")
.testIfErrorContains("not iterable", "sorted(depset(['a', 'b']))")
.testIfErrorContains("not iterable", "tuple(depset(['a', 'b']))")
.testIfErrorContains("not iterable", "[x for x in depset()]")
.testIfErrorContains("not iterable", "len(depset(['a']))");
}
@Test
public void testFieldReturnsNestedSet() throws Exception {
update("mock", new MockClassObject());
IllegalArgumentException e =
assertThrows(IllegalArgumentException.class, () -> eval("mock.nset"));
assertThat(e)
.hasMessageThat()
.contains(
"invalid Starlark value: class"
+ " com.google.devtools.build.lib.collect.nestedset.NestedSet");
}
@Test
public void testJavaFunctionReturnsNone() throws Exception {
new Scenario()
.update("mock", new Mock())
.setUp("v = mock.nullfunc_working()")
.testLookup("v", Starlark.NONE);
}
@Test
public void testVoidJavaFunctionReturnsNone() throws Exception {
new Scenario()
.update("mock", new Mock())
.setUp("v = mock.voidfunc()")
.testLookup("v", Starlark.NONE);
}
@Test
public void testAugmentedAssignment() throws Exception {
new Scenario()
.setUp("def f1(x):", " x += 1", " return x", "", "foo = f1(41)")
.testLookup("foo", 42);
}
@Test
public void testAugmentedAssignmentHasNoSideEffects() throws Exception {
// Check object position.
new Scenario()
.setUp(
"counter = [0]",
"value = [1, 2]",
"",
"def f():",
" counter[0] = counter[0] + 1",
" return value",
"",
"f()[1] += 1") // `f()` should be called only once here
.testLookup("counter", StarlarkList.of(null, 1));
// Check key position.
new Scenario()
.setUp(
"counter = [0]",
"value = [1, 2]",
"",
"def f():",
" counter[0] = counter[0] + 1",
" return 1",
"",
"value[f()] += 1") // `f()` should be called only once here
.testLookup("counter", StarlarkList.of(null, 1));
}
@Test
public void testInvalidAugmentedAssignment_ListExpression() throws Exception {
assertValidationError(
"cannot perform augmented assignment on a list or tuple expression",
//
"def f(a, b):",
" [a, b] += []",
"f(1, 2)");
}
@Test
public void testInvalidAugmentedAssignment_NotAnLValue() throws Exception {
assertValidationError(
"cannot assign to 'x + 1'",
//
"x + 1 += 2");
}
@Test
public void testAssignmentEvaluationOrder() throws Exception {
new Scenario()
.setUp(
"ordinary = []",
"augmented = []",
"value = [1, 2]",
"",
"def f(record):",
" record.append('f')",
" return value",
"",
"def g(record):",
" record.append('g')",
" return value",
"",
"f(ordinary)[0] = g(ordinary)[1]",
"f(augmented)[0] += g(augmented)[1]")
.testLookup("ordinary", StarlarkList.of(null, "g", "f")) // This order is consistent
.testLookup("augmented", StarlarkList.of(null, "f", "g")); // with Python
}
@Test
public void testDictComprehensions_IterationOrder() throws Exception {
new Scenario()
.setUp(
"def foo():",
" d = {x : x for x in ['c', 'a', 'b']}",
" s = ''",
" for a in d:",
" s += a",
" return s",
"s = foo()")
.testLookup("s", "cab");
}
@Test
public void testDotExpressionOnNonStructObject() throws Exception {
new Scenario()
.testIfExactError("'string' value has no field or method 'field'", "x = 'a'.field");
}
@Test
public void testPlusEqualsOnListMutating() throws Exception {
new Scenario()
.setUp(
"def func():",
" l1 = [1, 2]",
" l2 = l1",
" l2 += [3, 4]",
" return l1, l2",
"lists = str(func())")
.testLookup("lists", "([1, 2, 3, 4], [1, 2, 3, 4])");
// The same but with += after an IndexExpression
new Scenario()
.setUp(
"def func():",
" l = [1, 2]",
" d = {0: l}",
" d[0] += [3, 4]",
" return l, d[0]",
"lists = str(func())")
.testLookup("lists", "([1, 2, 3, 4], [1, 2, 3, 4])");
}
@Test
public void testPlusEqualsOnTuple() throws Exception {
new Scenario()
.setUp(
"def func():",
" t1 = (1, 2)",
" t2 = t1",
" t2 += (3, 4)",
" return t1, t2",
"tuples = func()")
.testLookup("tuples", Tuple.of(Tuple.of(1, 2), Tuple.of(1, 2, 3, 4)));
}
@Test
public void testPlusOnDictDeprecated() throws Exception {
new Scenario()
.testIfErrorContains("unsupported binary operation: dict + dict", "{1: 2} + {3: 4}");
new Scenario()
.testIfErrorContains(
"unsupported binary operation: dict + dict",
"def func():",
" d = {1: 2}",
" d += {3: 4}",
"func()");
}
@Test
public void testDictAssignmentAsLValue() throws Exception {
new Scenario()
.setUp("def func():", " d = {'a' : 1}", " d['b'] = 2", " return d", "d = func()")
.testLookup("d", ImmutableMap.of("a", 1, "b", 2));
}
@Test
public void testNestedDictAssignmentAsLValue() throws Exception {
new Scenario()
.setUp(
"def func():",
" d = {'a' : 1}",
" e = {'d': d}",
" e['d']['b'] = 2",
" return e",
"e = func()")
.testLookup("e", ImmutableMap.of("d", ImmutableMap.of("a", 1, "b", 2)));
}
@Test
public void testListAssignmentAsLValue() throws Exception {
new Scenario()
.setUp(
"def func():",
" a = [1, 2]",
" a[1] = 3",
" a[-2] = 4",
" return a",
"a = str(func())")
.testLookup("a", "[4, 3]");
}
@Test
public void testNestedListAssignmentAsLValue() throws Exception {
new Scenario()
.setUp(
"def func():",
" d = [1, 2]",
" e = [3, d]",
" e[1][1] = 4",
" return e",
"e = str(func())")
.testLookup("e", "[3, [1, 4]]");
}
@Test
public void testDictTupleAssignmentAsLValue() throws Exception {
new Scenario()
.setUp(
"def func():", " d = {'a' : 1}", " d['b'], d['c'] = 2, 3", " return d", "d = func()")
.testLookup("d", ImmutableMap.of("a", 1, "b", 2, "c", 3));
}
@Test
public void testDictItemPlusEqual() throws Exception {
new Scenario()
.setUp("def func():", " d = {'a' : 2}", " d['a'] += 3", " return d", "d = func()")
.testLookup("d", ImmutableMap.of("a", 5));
}
@Test
public void testDictAssignmentAsLValueSideEffects() throws Exception {
new Scenario()
.setUp("def func(d):", " d['b'] = 2", "d = {'a' : 1}", "func(d)")
.testLookup("d", Dict.of((Mutability) null, "a", 1, "b", 2));
}
@Test
public void testAssignmentToListInDictSideEffect() throws Exception {
new Scenario()
.setUp("l = [1, 2]", "d = {0: l}", "d[0].append(3)")
.testLookup("l", StarlarkList.of(null, 1, 2, 3));
}
@Test
public void testUserFunctionKeywordArgs() throws Exception {
new Scenario()
.setUp("def foo(a, b, c):", " return a + b + c", "s = foo(1, c=2, b=3)")
.testLookup("s", 6);
}
@Test
public void testFunctionCallOrdering() throws Exception {
new Scenario()
.setUp("def func(): return foo() * 2", "def foo(): return 2", "x = func()")
.testLookup("x", 4);
}
@Test
public void testFunctionCallBadOrdering() throws Exception {
new Scenario()
.testIfErrorContains(
"global variable 'foo' is referenced before assignment.",
"def func(): return foo() * 2",
"x = func()",
"def foo(): return 2");
}
@Test
public void testLocalVariableDefinedBelow() throws Exception {
new Scenario()
.setUp(
"def beforeEven(li):", // returns the value before the first even number
" for i in li:",
" if i % 2 == 0:",
" return a",
" else:",
" a = i",
"res = beforeEven([1, 3, 4, 5])")
.testLookup("res", 3);
}
@Test
public void testShadowisNotInitialized() throws Exception {
new Scenario()
.testIfErrorContains(
/* error message */ "local variable 'gl' is referenced before assignment",
"gl = 5",
"def foo():",
" if False: gl = 2",
" return gl",
"res = foo()");
}
@Test
public void testShadowBuiltin() throws Exception {
new Scenario()
.testIfErrorContains(
"global variable 'len' is referenced before assignment",
"x = len('abc')",
"len = 2",
"y = x + len");
}
@Test
public void testFunctionCallRecursion() throws Exception {
new Scenario()
.testIfErrorContains(
"function 'f' called recursively",
"def main():",
" f(5)",
"def f(n):",
" if n > 0: g(n - 1)",
"def g(n):",
" if n > 0: f(n - 1)",
"main()");
}
@Test
// TODO(adonovan): move to Validation tests.
public void testTypo() throws Exception {
assertValidationError(
"name 'my_variable' is not defined (did you mean 'myVariable'?)",
//
"myVariable = 2",
"x = my_variable + 1");
}
@Test
public void testNoneTrueFalseInSkylark() throws Exception {
new Scenario()
.setUp("a = None", "b = True", "c = False")
.testLookup("a", Starlark.NONE)
.testLookup("b", Boolean.TRUE)
.testLookup("c", Boolean.FALSE);
}
@Test
public void testHasattrMethods() throws Exception {
new Scenario()
.update("mock", new Mock())
.setUp(
"a = hasattr(mock, 'struct_field')",
"b = hasattr(mock, 'function')",
"c = hasattr(mock, 'is_empty')",
"d = hasattr('str', 'replace')",
"e = hasattr(mock, 'other')\n")
.testLookup("a", Boolean.TRUE)
.testLookup("b", Boolean.TRUE)
.testLookup("c", Boolean.TRUE)
.testLookup("d", Boolean.TRUE)
.testLookup("e", Boolean.FALSE);
}
@Test
public void testGetattrMethods() throws Exception {
new Scenario()
.update("mock", new Mock())
.setUp(
"a = str(getattr(mock, 'struct_field', 'no'))",
"b = str(getattr(mock, 'function', 'no'))",
"c = str(getattr(mock, 'is_empty', 'no'))",
"d = str(getattr('str', 'replace', 'no'))",
"e = str(getattr(mock, 'other', 'no'))\n")
.testLookup("a", "a")
.testLookup("b", "<built-in function function>")
.testLookup("c", "<built-in function is_empty>")
.testLookup("d", "<built-in function replace>")
.testLookup("e", "no");
}
@Test
public void testListAnTupleConcatenationDoesNotWorkInSkylark() throws Exception {
new Scenario()
.testIfExactError("unsupported binary operation: list + tuple", "[1, 2] + (3, 4)");
}
@Test
public void testCannotCreateMixedListInSkylark() throws Exception {
new Scenario().testExactOrder("['a', 'b', 1, 2]", "a", "b", 1, 2);
}
@Test
public void testCannotConcatListInSkylarkWithDifferentGenericTypes() throws Exception {
new Scenario().testExactOrder("[1, 2] + ['a', 'b']", 1, 2, "a", "b");
}
@Test
public void testConcatEmptyListWithNonEmptyWorks() throws Exception {
new Scenario().testExactOrder("[] + ['a', 'b']", "a", "b");
}
@Test
public void testFormatStringWithTuple() throws Exception {
new Scenario().setUp("v = '%s%s' % ('a', 1)").testLookup("v", "a1");
}
@Test
public void testSingletonTuple() throws Exception {
new Scenario().testExactOrder("(1,)", 1);
}
@Test
public void testDirFindsClassObjectFields() throws Exception {
new Scenario()
.update("mock", new MockClassObject())
.testExactOrder("dir(mock)", "field", "nset");
}
@Test
public void testDirFindsJavaObjectStructFieldsAndMethods() throws Exception {
new Scenario()
.update("mock", new Mock())
.testExactOrder(
"dir(mock)",
"function",
"interrupted_struct_field",
"is_empty",
"legacy_method",
"nullfunc_failing",
"nullfunc_working",
"proxy_methods_object",
"raise_unchecked_exception",
"return_bad",
"string",
"string_list",
"string_list_dict",
"struct_field",
"struct_field_callable",
"struct_field_with_extra",
"value_of",
"voidfunc",
"with_args_and_kwargs",
"with_args_and_thread",
"with_extra",
"with_kwargs",
"with_params",
"with_params_and_extra");
}
@Test
public void testStrNativeInfo() throws Exception {
new Scenario()
.update("mock", new NativeInfoMock())
.testEval(
"str(mock)",
"'struct(struct_field_callable = <built-in function foobar>, struct_field_none = None, "
+ "struct_field_string = \"a\")'");
}
@Test
public void testNativeInfoAttrs() throws Exception {
new Scenario()
.update("mock", new NativeInfoMock())
.testEval(
"dir(mock)",
"['callable_string', 'struct_field_callable', 'struct_field_none', "
+ "'struct_field_string', 'to_json', 'to_proto']")
.testExpression("str(mock.to_json)", "<built-in function to_json>")
.testExpression("str(getattr(mock, 'to_json'))", "<built-in function to_json>");
}
@Test
public void testPrint() throws Exception {
// TODO(fwe): cannot be handled by current testing suite
setFailFast(false);
exec("print('hello')");
assertContainsDebug("hello");
exec("print('a', 'b')");
assertContainsDebug("a b");
exec("print('a', 'b', sep='x')");
assertContainsDebug("axb");
}
@Test
public void testPrintBadKwargs() throws Exception {
new Scenario()
.testIfErrorContains(
"print() got unexpected keyword argument 'end'", "print(end='x', other='y')");
}
@Test
public void testConditionalExpressionAtToplevel() throws Exception {
new Scenario().setUp("x = 1 if 2 else 3").testLookup("x", 1);
}
@Test
public void testConditionalExpressionInFunction() throws Exception {
new Scenario()
.setUp("def foo(a, b, c): return a+b if c else a-b\n")
.testExpression("foo(23, 5, 0)", 18);
}
// This class extends NativeInfo (which provides @SkylarkCallable-annotated fields)
// with additional fields from a map. The only production code that currently
// does that is ToolchainInfo and its subclasses.
@SkylarkModule(name = "SkylarkClassObjectWithSkylarkCallables", doc = "")
private static final class SkylarkClassObjectWithSkylarkCallables extends NativeInfo {
static final NativeProvider<SkylarkClassObjectWithSkylarkCallables> CONSTRUCTOR =
new NativeProvider<SkylarkClassObjectWithSkylarkCallables>(
SkylarkClassObjectWithSkylarkCallables.class, "struct_with_skylark_callables") {};
// A function that returns "fromValues".
Object returnFromValues =
new StarlarkCallable() {
@Override
public String getName() {
return "returnFromValues";
}
@Override
public Object fastcall(StarlarkThread thread, Object[] positional, Object[] named) {
return "fromValues";
}
};
final Map<String, Object> fields =
ImmutableMap.of(
"values_only_field",
"fromValues",
"values_only_method",
returnFromValues,
"collision_field",
"fromValues",
"collision_method",
returnFromValues);
SkylarkClassObjectWithSkylarkCallables() {
super(CONSTRUCTOR, Location.BUILTIN);
}
@Override
public Object getValue(String name) throws EvalException {
Object x = fields.get(name);
return x != null ? x : super.getValue(name);
}
@Override
public ImmutableCollection<String> getFieldNames() {
return ImmutableSet.<String>builder()
.addAll(fields.keySet())
.addAll(super.getFieldNames())
.build();
}
@SkylarkCallable(name = "callable_only_field", documented = false, structField = true)
public String getCallableOnlyField() {
return "fromSkylarkCallable";
}
@SkylarkCallable(name = "callable_only_method", documented = false, structField = false)
public String getCallableOnlyMethod() {
return "fromSkylarkCallable";
}
@SkylarkCallable(name = "collision_field", documented = false, structField = true)
public String getCollisionField() {
return "fromSkylarkCallable";
}
@SkylarkCallable(name = "collision_method", documented = false, structField = false)
public String getCollisionMethod() {
return "fromSkylarkCallable";
}
}
@Test
public void testStructFieldDefinedOnlyInValues() throws Exception {
new Scenario()
.update("val", new SkylarkClassObjectWithSkylarkCallables())
.setUp("v = val.values_only_field")
.testLookup("v", "fromValues");
}
@Test
public void testStructMethodDefinedOnlyInValues() throws Exception {
new Scenario()
.update("val", new SkylarkClassObjectWithSkylarkCallables())
.setUp("v = val.values_only_method()")
.testLookup("v", "fromValues");
}
@Test
public void testStructFieldDefinedOnlyInSkylarkCallable() throws Exception {
new Scenario()
.update("val", new SkylarkClassObjectWithSkylarkCallables())
.setUp("v = val.callable_only_field")
.testLookup("v", "fromSkylarkCallable");
}
@Test
public void testStructMethodDefinedOnlyInSkylarkCallable() throws Exception {
new Scenario()
.update("val", new SkylarkClassObjectWithSkylarkCallables())
.setUp("v = val.callable_only_method()")
.testLookup("v", "fromSkylarkCallable");
}
@Test
public void testStructMethodDefinedInValuesAndSkylarkCallable() throws Exception {
// This test exercises the resolution of ambiguity between @SkylarkCallable-annotated
// fields and those reported by ClassObject.getValue.
new Scenario()
.update("val", new SkylarkClassObjectWithSkylarkCallables())
.setUp("v = val.collision_method()")
.testLookup("v", "fromSkylarkCallable");
}
@Test
public void testStructFieldNotDefined() throws Exception {
new Scenario()
.update("val", new SkylarkClassObjectWithSkylarkCallables())
.testIfExactError(
// TODO(bazel-team): This should probably list callable_only_method as well.
"'struct_with_skylark_callables' value has no field or method 'nonexistent_field'\n"
+ "Available attributes: callable_only_field, collision_field, collision_method, "
+ "values_only_field, values_only_method",
"v = val.nonexistent_field");
}
@Test
public void testStructMethodNotDefined() throws Exception {
new Scenario()
.update("val", new SkylarkClassObjectWithSkylarkCallables())
.testIfExactError(
"'struct_with_skylark_callables' value has no field or method 'nonexistent_method'\n"
+ "Available attributes: callable_only_field, collision_field, collision_method, "
+ "values_only_field, values_only_method",
"v = val.nonexistent_method()");
}
@Test
public void testListComprehensionsShadowGlobalVariable() throws Exception {
exec(
"a = 18", //
"def foo():",
" b = [a for a in range(3)]",
" return a",
"x = foo()");
assertThat(lookup("x")).isEqualTo(18);
}
@Test
public void testComprehensionsAreLocal() throws Exception {
// Regression test for https://github.com/bazelbuild/starlark/issues/92.
exec(
"x = 1", // this global binding is not affected (even temporarily) by the comprehension
"def f():",
" return x",
"y = [f() for x in [2]][0]");
assertThat(lookup("y")).isEqualTo(1);
}
@Test
public void testAnalysisFailureInfo() throws Exception {
AnalysisFailure cause = new AnalysisFailure(Label.create("test", "test"), "ErrorMessage");
AnalysisFailureInfo info = AnalysisFailureInfo.forAnalysisFailures(ImmutableList.of(cause));
new Scenario()
.update("val", info)
.setUp(
"causes = val.causes",
"label = causes.to_list()[0].label",
"message = causes.to_list()[0].message")
.testLookup("label", Label.create("test", "test"))
.testLookup("message", "ErrorMessage");
}
@Test
public void testFunctionEvaluatedBeforeArguments() throws Exception {
// ''.nonesuch must be evaluated (and fail) before f().
new Scenario()
.testIfErrorContains(
"'string' value has no field or method 'nonesuch'",
"def f(): x = 1//0",
"''.nonesuch(f())");
}
}