aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/test/java/com/google/devtools
diff options
context:
space:
mode:
authorGravatar Ulf Adams <ulfjack@google.com>2015-02-26 13:39:28 +0000
committerGravatar Han-Wen Nienhuys <hanwen@google.com>2015-02-26 13:39:28 +0000
commit89f012dd8b5c75573668c0a5a984d814da31c46f (patch)
tree74d4c305f67ab2be73d18e22eb7597e8da6ec588 /src/test/java/com/google/devtools
parent5a4f28664237fd5d53273c791f5f2decbf27d45b (diff)
Open source all the tests under lib/syntax/.
-- MOS_MIGRATED_REVID=87244284
Diffstat (limited to 'src/test/java/com/google/devtools')
-rw-r--r--src/test/java/com/google/devtools/build/lib/syntax/ASTNodeTest.java64
-rw-r--r--src/test/java/com/google/devtools/build/lib/syntax/AbstractEvaluationTestCase.java52
-rw-r--r--src/test/java/com/google/devtools/build/lib/syntax/AbstractParserTestCase.java100
-rw-r--r--src/test/java/com/google/devtools/build/lib/syntax/BuildFileASTTest.java300
-rw-r--r--src/test/java/com/google/devtools/build/lib/syntax/EnvironmentTest.java133
-rw-r--r--src/test/java/com/google/devtools/build/lib/syntax/EvalUtilsTest.java221
-rw-r--r--src/test/java/com/google/devtools/build/lib/syntax/EvaluationTest.java492
-rw-r--r--src/test/java/com/google/devtools/build/lib/syntax/FunctionTest.java424
-rw-r--r--src/test/java/com/google/devtools/build/lib/syntax/GlobCriteriaTest.java169
-rw-r--r--src/test/java/com/google/devtools/build/lib/syntax/GlobListTest.java103
-rw-r--r--src/test/java/com/google/devtools/build/lib/syntax/LabelTest.java364
-rw-r--r--src/test/java/com/google/devtools/build/lib/syntax/LexerTest.java399
-rw-r--r--src/test/java/com/google/devtools/build/lib/syntax/LineNumberTableTest.java113
-rw-r--r--src/test/java/com/google/devtools/build/lib/syntax/MixedModeFunctionTest.java130
-rw-r--r--src/test/java/com/google/devtools/build/lib/syntax/ParserInputSourceTest.java116
-rw-r--r--src/test/java/com/google/devtools/build/lib/syntax/ParserTest.java876
-rw-r--r--src/test/java/com/google/devtools/build/lib/syntax/SkylarkEvaluationTest.java799
-rw-r--r--src/test/java/com/google/devtools/build/lib/syntax/SkylarkListTest.java139
-rw-r--r--src/test/java/com/google/devtools/build/lib/syntax/SkylarkNestedSetTest.java170
-rw-r--r--src/test/java/com/google/devtools/build/lib/syntax/SkylarkShell.java94
-rw-r--r--src/test/java/com/google/devtools/build/lib/syntax/ValidationTests.java576
21 files changed, 5834 insertions, 0 deletions
diff --git a/src/test/java/com/google/devtools/build/lib/syntax/ASTNodeTest.java b/src/test/java/com/google/devtools/build/lib/syntax/ASTNodeTest.java
new file mode 100644
index 0000000000..c607d904f1
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/syntax/ASTNodeTest.java
@@ -0,0 +1,64 @@
+// Copyright 2015 Google Inc. 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 org.junit.Assert.fail;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests {@link ASTNode}.
+ */
+@RunWith(JUnit4.class)
+public class ASTNodeTest {
+
+ private ASTNode node;
+
+ @Before
+ public void setUp() throws Exception {
+ node = new ASTNode() {
+ @Override
+ public String toString() {
+ return null;
+ }
+ @Override
+ public void accept(SyntaxTreeVisitor visitor) {
+ }
+ };
+ }
+
+ @Test
+ public void testHashCodeNotSupported() {
+ try {
+ node.hashCode();
+ fail();
+ } catch (UnsupportedOperationException e) {
+ // yes!
+ }
+ }
+
+ @Test
+ public void testEqualsNotSupported() {
+ try {
+ node.equals(this);
+ fail();
+ } catch (UnsupportedOperationException e) {
+ // yes!
+ }
+ }
+
+}
diff --git a/src/test/java/com/google/devtools/build/lib/syntax/AbstractEvaluationTestCase.java b/src/test/java/com/google/devtools/build/lib/syntax/AbstractEvaluationTestCase.java
new file mode 100644
index 0000000000..7937786161
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/syntax/AbstractEvaluationTestCase.java
@@ -0,0 +1,52 @@
+// Copyright 2006 Google Inc. 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 java.util.List;
+
+/**
+ * Base class for test cases that use eval services.
+ */
+public abstract class AbstractEvaluationTestCase extends AbstractParserTestCase {
+
+ public Object eval(String input) throws Exception {
+ return eval(parseExpr(input));
+ }
+
+ public Object eval(String input, Environment env) throws Exception {
+ return eval(parseExpr(input), env);
+ }
+
+ public static Object eval(Expression e) throws Exception {
+ return eval(e, new Environment());
+ }
+
+ public static Object eval(Expression e, Environment env) throws Exception {
+ return e.eval(env);
+ }
+
+ public void exec(String input, Environment env) throws Exception {
+ exec(parseStmt(input), env);
+ }
+
+ public void exec(Statement s, Environment env) throws Exception {
+ s.exec(env);
+ }
+
+ public static void exec(List<Statement> li, Environment env) throws Exception {
+ for (Statement stmt : li) {
+ stmt.exec(env);
+ }
+ }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/syntax/AbstractParserTestCase.java b/src/test/java/com/google/devtools/build/lib/syntax/AbstractParserTestCase.java
new file mode 100644
index 0000000000..ba59864ffe
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/syntax/AbstractParserTestCase.java
@@ -0,0 +1,100 @@
+// Copyright 2006 Google Inc. 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 com.google.common.collect.ImmutableMap;
+import com.google.devtools.build.lib.events.util.EventCollectionApparatus;
+import com.google.devtools.build.lib.packages.CachingPackageLocator;
+import com.google.devtools.build.lib.rules.SkylarkModules;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.util.FsApparatus;
+
+import junit.framework.TestCase;
+
+import java.util.List;
+
+/**
+ * Base class for test cases that use parsing services.
+ */
+public abstract class AbstractParserTestCase extends TestCase {
+ public static final class EmptyPackageLocator implements CachingPackageLocator {
+ @Override
+ public Path getBuildFileForPackage(String packageName) {
+ return null;
+ }
+ }
+
+ protected EventCollectionApparatus syntaxEvents = new EventCollectionApparatus();
+ private FsApparatus scratch = FsApparatus.newInMemory();
+ private CachingPackageLocator locator = new EmptyPackageLocator();
+
+ private static Lexer createLexer(String input,
+ EventCollectionApparatus syntaxEvents, FsApparatus scratch) {
+ Path someFile = scratch.path("/some/file.txt");
+ ParserInputSource inputSource = ParserInputSource.create(input, someFile);
+ return new Lexer(inputSource, syntaxEvents.reporter());
+ }
+
+ protected Lexer createLexer(String input) {
+ return createLexer(input, syntaxEvents, scratch);
+ }
+
+ protected List<Statement> parseFile(String input) {
+ return Parser.parseFile(createLexer(input), syntaxEvents.reporter(), locator, false)
+ .statements;
+ }
+
+ protected List<Statement> parseFile(String input, boolean parsePython) {
+ return Parser.parseFile(createLexer(input), syntaxEvents.reporter(), locator, parsePython)
+ .statements;
+ }
+
+ protected List<Statement> parseFileForSkylark(String input) {
+ return Parser.parseFileForSkylark(createLexer(input), syntaxEvents.reporter(), locator,
+ SkylarkModules.getValidationEnvironment()).statements;
+ }
+
+ protected List<Statement> parseFileForSkylark(
+ String input, ImmutableMap<String, SkylarkType> extraObject) {
+ return Parser.parseFileForSkylark(createLexer(input), syntaxEvents.reporter(), locator,
+ SkylarkModules.getValidationEnvironment(extraObject)).statements;
+ }
+
+ protected Parser.ParseResult parseFileWithComments(String input) {
+ return Parser.parseFile(createLexer(input), syntaxEvents.reporter(), locator, false);
+ }
+
+ protected Statement parseStmt(String input) {
+ return Parser.parseStatement(createLexer(input), syntaxEvents.reporter());
+ }
+
+ protected Expression parseExpr(String input) {
+ return Parser.parseExpression(createLexer(input), syntaxEvents.reporter());
+ }
+
+ public static List<Statement> parseFileForSkylark(
+ EventCollectionApparatus syntaxEvents, FsApparatus scratch, String input) {
+ return Parser.parseFileForSkylark(createLexer(input, syntaxEvents, scratch),
+ syntaxEvents.reporter(), null,
+ SkylarkModules.getValidationEnvironment()).statements;
+ }
+
+ public static List<Statement> parseFileForSkylark(
+ EventCollectionApparatus syntaxEvents, FsApparatus scratch, String input,
+ ImmutableMap<String, SkylarkType> extraObject) {
+ return Parser.parseFileForSkylark(createLexer(input, syntaxEvents, scratch),
+ syntaxEvents.reporter(), null,
+ SkylarkModules.getValidationEnvironment(extraObject)).statements;
+ }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/syntax/BuildFileASTTest.java b/src/test/java/com/google/devtools/build/lib/syntax/BuildFileASTTest.java
new file mode 100644
index 0000000000..eff5c92e73
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/syntax/BuildFileASTTest.java
@@ -0,0 +1,300 @@
+// Copyright 2006 Google Inc. 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 com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.EventCollector;
+import com.google.devtools.build.lib.events.EventKind;
+import com.google.devtools.build.lib.events.Reporter;
+import com.google.devtools.build.lib.events.util.EventCollectionApparatus;
+import com.google.devtools.build.lib.packages.CachingPackageLocator;
+import com.google.devtools.build.lib.testutil.JunitTestUtils;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.util.FsApparatus;
+
+import junit.framework.TestCase;
+
+import java.io.IOException;
+import java.util.Arrays;
+
+public class BuildFileASTTest extends TestCase {
+
+ private FsApparatus scratch = FsApparatus.newInMemory();
+
+ private EventCollectionApparatus events = new EventCollectionApparatus(EventKind.ALL_EVENTS);
+
+ private class ScratchPathPackageLocator implements CachingPackageLocator {
+ @Override
+ public Path getBuildFileForPackage(String packageName) {
+ return scratch.path(packageName).getRelative("BUILD");
+ }
+ }
+
+ private CachingPackageLocator locator = new ScratchPathPackageLocator();
+
+ /**
+ * Parses the contents of the specified string (using DUMMY_PATH as the fake
+ * filename) and returns the AST. Resets the error handler beforehand.
+ */
+ private BuildFileAST parseBuildFile(String... lines) throws IOException {
+ Path file = scratch.file("/a/build/file/BUILD", lines);
+ return BuildFileAST.parseBuildFile(file, events.reporter(), locator, false);
+ }
+
+ public void testParseBuildFileOK() throws Exception {
+ Path buildFile = scratch.file("/BUILD",
+ "# a file in the build language",
+ "",
+ "x = [1,2,'foo',4] + [1,2, \"%s%d\" % ('foo', 1)]");
+
+ Environment env = new Environment();
+ Reporter reporter = new Reporter();
+ BuildFileAST buildfile = BuildFileAST.parseBuildFile(buildFile, reporter, null, false);
+
+ assertTrue(buildfile.exec(env, reporter));
+
+ // Test final environment is correctly modified:
+ //
+ // input1.BUILD contains:
+ // x = [1,2,'foo',4] + [1,2, "%s%d" % ('foo', 1)]
+ assertEquals(Arrays.<Object>asList(1, 2, "foo", 4, 1, 2, "foo1"),
+ env.lookup("x"));
+ }
+
+ public void testEvalException() throws Exception {
+ Path buildFile = scratch.file("/input1.BUILD",
+ "x = 1",
+ "y = [2,3]",
+ "",
+ "z = x + y");
+
+ Environment env = new Environment();
+ Reporter reporter = new Reporter();
+ EventCollector collector = new EventCollector(EventKind.ALL_EVENTS);
+ reporter.addHandler(collector);
+ BuildFileAST buildfile = BuildFileAST.parseBuildFile(buildFile, reporter, null, false);
+
+ assertFalse(buildfile.exec(env, reporter));
+ Event e = JunitTestUtils.assertContainsEvent(collector,
+ "unsupported operand type(s) for +: 'int' and 'list'");
+ assertEquals(4, e.getLocation().getStartLineAndColumn().getLine());
+ }
+
+ public void testParsesFineWithNewlines() throws Exception {
+ BuildFileAST buildFileAST = parseBuildFile("foo()\n"
+ + "bar()\n"
+ + "something = baz()\n"
+ + "bar()");
+ assertEquals(4, buildFileAST.getStatements().size());
+ }
+
+ public void testFailsIfNewlinesAreMissing() throws Exception {
+ events.setFailFast(false);
+
+ BuildFileAST buildFileAST =
+ parseBuildFile("foo() bar() something = baz() bar()");
+
+ Event event = events.collector().iterator().next();
+ assertEquals("syntax error at \'bar\'", event.getMessage());
+ assertEquals("/a/build/file/BUILD",
+ event.getLocation().getPath().toString());
+ assertEquals(1, event.getLocation().getStartLineAndColumn().getLine());
+ assertTrue(buildFileAST.containsErrors());
+ }
+
+ public void testImplicitStringConcatenationFails() throws Exception {
+ events.setFailFast(false);
+ BuildFileAST buildFileAST = parseBuildFile("a = 'foo' 'bar'");
+ Event event = events.collector().iterator().next();
+ assertEquals("Implicit string concatenation is forbidden, use the + operator",
+ event.getMessage());
+ assertEquals("/a/build/file/BUILD",
+ event.getLocation().getPath().toString());
+ assertEquals(1, event.getLocation().getStartLineAndColumn().getLine());
+ assertEquals(10, event.getLocation().getStartLineAndColumn().getColumn());
+ assertTrue(buildFileAST.containsErrors());
+ }
+
+ public void testImplicitStringConcatenationAcrossLinesIsIllegal() throws Exception {
+ events.setFailFast(false);
+ BuildFileAST buildFileAST = parseBuildFile("a = 'foo'\n 'bar'");
+
+ Event event = events.collector().iterator().next();
+ assertEquals("indentation error", event.getMessage());
+ assertEquals("/a/build/file/BUILD",
+ event.getLocation().getPath().toString());
+ assertEquals(2, event.getLocation().getStartLineAndColumn().getLine());
+ assertEquals(2, event.getLocation().getStartLineAndColumn().getColumn());
+ assertTrue(buildFileAST.containsErrors());
+ }
+
+ /**
+ * If the specified EventCollector does contain an event which has
+ * 'expectedEvent' as a substring, the matching event is
+ * returned. Otherwise this will return null.
+ */
+ public static Event findEvent(EventCollector eventCollector,
+ String expectedEvent) {
+ for (Event event : eventCollector) {
+ if (event.getMessage().contains(expectedEvent)) {
+ return event;
+ }
+ }
+ return null;
+ }
+
+ public void testWithSyntaxErrorsDoesNotPrintDollarError() throws Exception {
+ events.setFailFast(false);
+ BuildFileAST buildFile = parseBuildFile(
+ "abi = cxx_abi + '-glibc-' + glibc_version + '-' + "
+ + "generic_cpu + '-' + sysname",
+ "libs = [abi + opt_level + '/lib/libcc.a']",
+ "shlibs = [abi + opt_level + '/lib/libcc.so']",
+ "+* shlibs", // syntax error at '+'
+ "cc_library(name = 'cc',",
+ " srcs = libs,",
+ " includes = [ abi + opt_level + '/include' ])");
+ assertTrue(buildFile.containsErrors());
+ Event event = events.collector().iterator().next();
+ assertEquals("syntax error at '+'", event.getMessage());
+ Environment env = new Environment();
+ assertFalse(buildFile.exec(env, events.reporter()));
+ assertNull(findEvent(events.collector(), "$error$"));
+ // This message should not be printed anymore.
+ Event event2 = findEvent(events.collector(), "contains syntax error(s)");
+ assertNull(event2);
+ }
+
+ public void testInclude() throws Exception {
+ scratch.file("/foo/bar/BUILD",
+ "c = 4\n"
+ + "d = 5\n");
+ Path buildFile = scratch.file("/BUILD",
+ "a = 2\n"
+ + "include(\"//foo/bar:BUILD\")\n"
+ + "b = 4\n");
+
+ BuildFileAST buildFileAST = BuildFileAST.parseBuildFile(buildFile, events.reporter(),
+ locator, false);
+
+ assertFalse(buildFileAST.containsErrors());
+ assertEquals(5, buildFileAST.getStatements().size());
+ }
+
+ public void testInclude2() throws Exception {
+ scratch.file("/foo/bar/defs",
+ "a = 1\n");
+ Path buildFile = scratch.file("/BUILD",
+ "include(\"//foo/bar:defs\")\n"
+ + "b = a + 1\n");
+
+ BuildFileAST buildFileAST = BuildFileAST.parseBuildFile(buildFile, events.reporter(),
+ locator, false);
+
+ assertFalse(buildFileAST.containsErrors());
+ assertEquals(3, buildFileAST.getStatements().size());
+
+ Environment env = new Environment();
+ Reporter reporter = new Reporter();
+ assertFalse(buildFileAST.exec(env, reporter));
+ assertEquals(2, env.lookup("b"));
+ }
+
+ public void testMultipleIncludes() throws Exception {
+ String fileA =
+ "include(\"//foo:fileB\")\n"
+ + "include(\"//foo:fileC\")\n";
+ scratch.file("/foo/fileB",
+ "b = 3\n"
+ + "include(\"//foo:fileD\")\n");
+ scratch.file("/foo/fileC",
+ "include(\"//foo:fileD\")\n"
+ + "c = b + 2\n");
+ scratch.file("/foo/fileD",
+ "b = b + 1\n"); // this code is included twice
+
+ BuildFileAST buildFileAST = parseBuildFile(fileA);
+ assertFalse(buildFileAST.containsErrors());
+ assertEquals(8, buildFileAST.getStatements().size());
+
+ Environment env = new Environment();
+ Reporter reporter = new Reporter();
+ assertFalse(buildFileAST.exec(env, reporter));
+ assertEquals(5, env.lookup("b"));
+ assertEquals(7, env.lookup("c"));
+ }
+
+ public void testFailInclude() throws Exception {
+ events.setFailFast(false);
+ BuildFileAST buildFileAST = parseBuildFile("include(\"//nonexistent\")");
+ assertEquals(1, buildFileAST.getStatements().size());
+ events.assertContainsEvent("Include of '//nonexistent' failed");
+ }
+
+
+ private class EmptyPackageLocator implements CachingPackageLocator {
+ @Override
+ public Path getBuildFileForPackage(String packageName) {
+ return null;
+ }
+ }
+
+ private CachingPackageLocator emptyLocator = new EmptyPackageLocator();
+
+ public void testFailInclude2() throws Exception {
+ events.setFailFast(false);
+ Path buildFile = scratch.file("/foo/bar/BUILD",
+ "include(\"//nonexistent:foo\")\n");
+ BuildFileAST buildFileAST = BuildFileAST.parseBuildFile(buildFile, events.reporter(),
+ emptyLocator, false);
+ assertEquals(1, buildFileAST.getStatements().size());
+ events.assertContainsEvent("Package 'nonexistent' not found");
+ }
+
+ public void testInvalidInclude() throws Exception {
+ events.setFailFast(false);
+ BuildFileAST buildFileAST = parseBuildFile("include(2)");
+ assertEquals(0, buildFileAST.getStatements().size());
+ events.assertContainsEvent("syntax error at '2'");
+ }
+
+ public void testRecursiveInclude() throws Exception {
+ events.setFailFast(false);
+ Path buildFile = scratch.file("/foo/bar/BUILD",
+ "include(\"//foo/bar:BUILD\")\n");
+
+ BuildFileAST.parseBuildFile(buildFile, events.reporter(), locator, false);
+ events.assertContainsEvent("Recursive inclusion");
+ }
+
+ public void testParseErrorInclude() throws Exception {
+ events.setFailFast(false);
+
+ scratch.file("/foo/bar/file",
+ "a = 2 + % 3\n"); // parse error
+
+ parseBuildFile("include(\"//foo/bar:file\")");
+
+ // Check the location is properly reported
+ Event event = events.collector().iterator().next();
+ assertEquals("/foo/bar/file:1:9", event.getLocation().print());
+ assertEquals("syntax error at '%'", event.getMessage());
+ }
+
+ public void testNonExistentIncludeReported() throws Exception {
+ events.setFailFast(false);
+ BuildFileAST buildFileAST = parseBuildFile("include('//foo:bar')");
+ assertEquals(1, buildFileAST.getStatements().size());
+ }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/syntax/EnvironmentTest.java b/src/test/java/com/google/devtools/build/lib/syntax/EnvironmentTest.java
new file mode 100644
index 0000000000..045291364c
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/syntax/EnvironmentTest.java
@@ -0,0 +1,133 @@
+// Copyright 2006 Google Inc. 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 com.google.common.collect.Sets;
+
+/**
+ * Tests of Environment.
+ */
+public class EnvironmentTest extends AbstractEvaluationTestCase {
+
+ // Test the API directly
+ public void testLookupAndUpdate() throws Exception {
+ Environment env = new Environment();
+
+ try {
+ env.lookup("foo");
+ fail();
+ } catch (Environment.NoSuchVariableException e) {
+ assertEquals("no such variable: foo", e.getMessage());
+ }
+
+ env.update("foo", "bar");
+
+ assertEquals("bar", env.lookup("foo"));
+ }
+
+ public void testLookupWithDefault() throws Exception {
+ Environment env = new Environment();
+ assertEquals(21, env.lookup("VERSION", 21));
+ env.update("VERSION", 42);
+ assertEquals(42, env.lookup("VERSION", 21));
+ }
+
+ public void testDoubleUpdateSucceeds() throws Exception {
+ Environment env = new Environment();
+ env.update("VERSION", 42);
+ assertEquals(42, env.lookup("VERSION"));
+ env.update("VERSION", 43);
+ assertEquals(43, env.lookup("VERSION"));
+ }
+
+ // Test assign through interpreter, lookup through API:
+ public void testAssign() throws Exception {
+ Environment env = new Environment();
+
+ try {
+ env.lookup("foo");
+ fail();
+ } catch (Environment.NoSuchVariableException e) {
+ assertEquals("no such variable: foo", e.getMessage());
+ }
+
+ exec(parseStmt("foo = 'bar'"), env);
+
+ assertEquals("bar", env.lookup("foo"));
+ }
+
+ // Test update through API, reference through interpreter:
+ public void testReference() throws Exception {
+ Environment env = new Environment();
+
+ try {
+ eval(parseExpr("foo"), env);
+ fail();
+ } catch (EvalException e) {
+ assertEquals("name 'foo' is not defined", e.getMessage());
+ }
+
+ env.update("foo", "bar");
+
+ assertEquals("bar", eval(parseExpr("foo"), env));
+ }
+
+ // Test assign and reference through interpreter:
+ public void testAssignAndReference() throws Exception {
+ Environment env = new Environment();
+
+ try {
+ eval(parseExpr("foo"), env);
+ fail();
+ } catch (EvalException e) {
+ assertEquals("name 'foo' is not defined", e.getMessage());
+ }
+
+ exec(parseStmt("foo = 'bar'"), env);
+
+ assertEquals("bar", eval(parseExpr("foo"), env));
+ }
+
+ public void testGetVariableNames() throws Exception {
+ Environment env = new Environment();
+ env.update("foo", "bar");
+ env.update("wiz", 3);
+
+ Environment nestedEnv = new Environment(env);
+ nestedEnv.update("foo", "bat");
+ nestedEnv.update("quux", 42);
+
+ assertEquals(Sets.newHashSet("True", "False", "None", "foo", "wiz"), env.getVariableNames());
+ assertEquals(Sets.newHashSet("True", "False", "None", "foo", "wiz", "quux"),
+ nestedEnv.getVariableNames());
+ }
+
+ public void testToString() throws Exception {
+ Environment env = new Environment();
+ env.update("subject", new StringLiteral("Hello, 'world'.", '\''));
+ env.update("from", new StringLiteral("Java", '"'));
+ assertEquals("Environment{False -> false, None -> None, True -> true, from -> \"Java\", "
+ + "subject -> 'Hello, \\'world\\'.', }", env.toString());
+ }
+
+ public void testBindToNullThrowsException() throws Exception {
+ try {
+ new Environment().update("some_name", null);
+ fail();
+ } catch (NullPointerException e) {
+ assertEquals("update(value == null)", e.getMessage());
+ }
+ }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/syntax/EvalUtilsTest.java b/src/test/java/com/google/devtools/build/lib/syntax/EvalUtilsTest.java
new file mode 100644
index 0000000000..595055b042
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/syntax/EvalUtilsTest.java
@@ -0,0 +1,221 @@
+// Copyright 2006 Google Inc. 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 com.google.common.collect.Lists;
+
+import junit.framework.TestCase;
+
+import java.util.Arrays;
+import java.util.IllegalFormatException;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Test properties of the evaluator's datatypes and utility functions
+ * without actually creating any parse trees.
+ */
+public class EvalUtilsTest extends TestCase {
+
+ private static List<?> makeList(Object ...args) {
+ return EvalUtils.makeSequence(Arrays.<Object>asList(args), false);
+ }
+ private static List<?> makeTuple(Object ...args) {
+ return EvalUtils.makeSequence(Arrays.<Object>asList(args), true);
+ }
+ private static Map<Object, Object> makeDict() {
+ return new LinkedHashMap<>();
+ }
+ private static FilesetEntry makeFilesetEntry() {
+ try {
+ return new FilesetEntry(Label.parseAbsolute("//foo:bar"),
+ Lists.<Label>newArrayList(), Lists.newArrayList("xyz"), "",
+ FilesetEntry.SymlinkBehavior.COPY, ".");
+ } catch (Label.SyntaxException e) {
+ throw new RuntimeException("Bad label: ", e);
+ }
+ }
+
+ public void testDataTypeNames() throws Exception {
+ assertEquals("string", EvalUtils.getDataTypeName("foo"));
+ assertEquals("int", EvalUtils.getDataTypeName(3));
+ assertEquals("tuple", EvalUtils.getDataTypeName(makeTuple(1, 2, 3)));
+ assertEquals("list", EvalUtils.getDataTypeName(makeList(1, 2, 3)));
+ assertEquals("dict", EvalUtils.getDataTypeName(makeDict()));
+ assertEquals("FilesetEntry", EvalUtils.getDataTypeName(makeFilesetEntry()));
+ assertEquals("None", EvalUtils.getDataTypeName(Environment.NONE));
+ }
+
+ public void testDatatypeMutability() throws Exception {
+ assertTrue(EvalUtils.isImmutable("foo"));
+ assertTrue(EvalUtils.isImmutable(3));
+ assertTrue(EvalUtils.isImmutable(makeTuple(1, 2, 3)));
+ assertFalse(EvalUtils.isImmutable(makeList(1, 2, 3)));
+ assertFalse(EvalUtils.isImmutable(makeDict()));
+ assertFalse(EvalUtils.isImmutable(makeFilesetEntry()));
+ }
+
+ public void testPrintValue() throws Exception {
+ // Note that prettyPrintValue and printValue only differ on behaviour of
+ // labels and strings at toplevel.
+ assertEquals("foo\nbar", EvalUtils.printValue("foo\nbar"));
+ assertEquals("\"foo\\nbar\"", EvalUtils.prettyPrintValue("foo\nbar"));
+ assertEquals("'", EvalUtils.printValue("'"));
+ assertEquals("\"'\"", EvalUtils.prettyPrintValue("'"));
+ assertEquals("\"", EvalUtils.printValue("\""));
+ assertEquals("\"\\\"\"", EvalUtils.prettyPrintValue("\""));
+ assertEquals("3", EvalUtils.printValue(3));
+ assertEquals("3", EvalUtils.prettyPrintValue(3));
+ assertEquals("None", EvalUtils.prettyPrintValue(Environment.NONE));
+
+ assertEquals("//x:x", EvalUtils.printValue(Label.parseAbsolute("//x")));
+ assertEquals("\"//x:x\"", EvalUtils.prettyPrintValue(Label.parseAbsolute("//x")));
+
+ List<?> list = makeList("foo", "bar");
+ List<?> tuple = makeTuple("foo", "bar");
+
+ assertEquals("(1, [\"foo\", \"bar\"], 3)",
+ EvalUtils.printValue(makeTuple(1, list, 3)));
+ assertEquals("(1, [\"foo\", \"bar\"], 3)",
+ EvalUtils.prettyPrintValue(makeTuple(1, list, 3)));
+ assertEquals("[1, (\"foo\", \"bar\"), 3]",
+ EvalUtils.printValue(makeList(1, tuple, 3)));
+ assertEquals("[1, (\"foo\", \"bar\"), 3]",
+ EvalUtils.prettyPrintValue(makeList(1, tuple, 3)));
+
+ Map<Object, Object> dict = makeDict();
+ dict.put(1, tuple);
+ dict.put(2, list);
+ dict.put("foo", makeList());
+ assertEquals("{1: (\"foo\", \"bar\"), 2: [\"foo\", \"bar\"], \"foo\": []}",
+ EvalUtils.printValue(dict));
+ assertEquals("{1: (\"foo\", \"bar\"), 2: [\"foo\", \"bar\"], \"foo\": []}",
+ EvalUtils.prettyPrintValue(dict));
+ assertEquals("FilesetEntry(srcdir = \"//foo:bar\", files = [], "
+ + "excludes = [\"xyz\"], destdir = \"\", "
+ + "strip_prefix = \".\", symlinks = \"copy\")",
+ EvalUtils.prettyPrintValue(makeFilesetEntry()));
+ }
+
+ private void checkFormatPositionalFails(String format, List<?> tuple,
+ String errorMessage) {
+ try {
+ EvalUtils.formatString(format, tuple);
+ fail();
+ } catch (IllegalFormatException e) {
+ assertEquals(errorMessage, e.getMessage());
+ }
+ }
+
+ public void testFormatPositional() throws Exception {
+ assertEquals("foo 3", EvalUtils.formatString("%s %d", makeTuple("foo", 3)));
+
+ // Note: formatString doesn't perform scalar x -> (x) conversion;
+ // The %-operator is responsible for that.
+ assertEquals("", EvalUtils.formatString("", makeTuple()));
+ assertEquals("foo", EvalUtils.formatString("%s", makeTuple("foo")));
+ assertEquals("3.14159", EvalUtils.formatString("%s", makeTuple(3.14159)));
+ checkFormatPositionalFails("%s", makeTuple(1, 2, 3),
+ "not all arguments converted during string formatting");
+ assertEquals("%foo", EvalUtils.formatString("%%%s", makeTuple("foo")));
+ checkFormatPositionalFails("%%s", makeTuple("foo"),
+ "not all arguments converted during string formatting");
+ checkFormatPositionalFails("% %s", makeTuple("foo"),
+ "invalid arguments for format string");
+ assertEquals("[1, 2, 3]", EvalUtils.formatString("%s", makeTuple(makeList(1, 2, 3))));
+ assertEquals("(1, 2, 3)", EvalUtils.formatString("%s", makeTuple(makeTuple(1, 2, 3))));
+ assertEquals("[]", EvalUtils.formatString("%s", makeTuple(makeList())));
+ assertEquals("()", EvalUtils.formatString("%s", makeTuple(makeTuple())));
+
+ checkFormatPositionalFails("%.3g", makeTuple(), "invalid arguments for format string");
+ checkFormatPositionalFails("%.3g", makeTuple(1, 2), "invalid arguments for format string");
+ checkFormatPositionalFails("%.s", makeTuple(), "invalid arguments for format string");
+ }
+
+ private String createExpectedFilesetEntryString(FilesetEntry.SymlinkBehavior symlinkBehavior) {
+ return "FilesetEntry(srcdir = \"//x:x\","
+ + " files = [\"//x:x\"],"
+ + " excludes = [],"
+ + " destdir = \"\","
+ + " strip_prefix = \".\","
+ + " symlinks = \"" + symlinkBehavior.toString().toLowerCase() + "\")";
+ }
+
+ private FilesetEntry createTestFilesetEntry(FilesetEntry.SymlinkBehavior symlinkBehavior)
+ throws Exception {
+ Label label = Label.parseAbsolute("//x");
+ return new FilesetEntry(label,
+ Arrays.asList(label),
+ Arrays.<String>asList(),
+ "",
+ symlinkBehavior,
+ ".");
+ }
+
+ public void testFilesetEntrySymlinkAttr() throws Exception {
+ FilesetEntry entryDereference =
+ createTestFilesetEntry(FilesetEntry.SymlinkBehavior.DEREFERENCE);
+
+ assertEquals(createExpectedFilesetEntryString(FilesetEntry.SymlinkBehavior.DEREFERENCE),
+ EvalUtils.prettyPrintValue(entryDereference));
+ }
+
+ private FilesetEntry createStripPrefixFilesetEntry(String stripPrefix) throws Exception {
+ Label label = Label.parseAbsolute("//x");
+ return new FilesetEntry(
+ label,
+ Arrays.asList(label),
+ Arrays.<String>asList(),
+ "",
+ FilesetEntry.SymlinkBehavior.DEREFERENCE,
+ stripPrefix);
+ }
+
+ public void testFilesetEntryStripPrefixAttr() throws Exception {
+ FilesetEntry withoutStripPrefix = createStripPrefixFilesetEntry(".");
+ FilesetEntry withStripPrefix = createStripPrefixFilesetEntry("orange");
+
+ String prettyWithout = EvalUtils.prettyPrintValue(withoutStripPrefix);
+ String prettyWith = EvalUtils.prettyPrintValue(withStripPrefix);
+
+ assertTrue(prettyWithout.contains("strip_prefix = \".\""));
+ assertTrue(prettyWith.contains("strip_prefix = \"orange\""));
+ }
+
+ public void testRegressionCrashInPrettyPrintValue() throws Exception {
+ // Would cause crash in code such as this:
+ // Fileset(name='x', entries=[], out=[FilesetEntry(files=['a'])])
+ // While formatting the "expected x, got y" message for the 'out'
+ // attribute, prettyPrintValue(FilesetEntry) would be recursively called
+ // with a List<Label> even though this isn't a valid datatype in the
+ // interpreter.
+ // Fileset isn't part of bazel, even though FilesetEntry is.
+ Label label = Label.parseAbsolute("//x");
+ assertEquals("FilesetEntry(srcdir = \"//x:x\","
+ + " files = [\"//x:x\"],"
+ + " excludes = [],"
+ + " destdir = \"\","
+ + " strip_prefix = \".\","
+ + " symlinks = \"copy\")",
+ EvalUtils.prettyPrintValue(
+ new FilesetEntry(label,
+ Arrays.asList(label),
+ Arrays.<String>asList(),
+ "",
+ FilesetEntry.SymlinkBehavior.COPY,
+ ".")));
+ }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/syntax/EvaluationTest.java b/src/test/java/com/google/devtools/build/lib/syntax/EvaluationTest.java
new file mode 100644
index 0000000000..f7ed359dab
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/syntax/EvaluationTest.java
@@ -0,0 +1,492 @@
+// Copyright 2014 Google Inc. 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 com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.devtools.build.lib.packages.PackageFactory;
+import com.google.devtools.build.lib.testutil.TestRuleClassProvider;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Test of evaluation behavior. (Implicitly uses lexer + parser.)
+ */
+public class EvaluationTest extends AbstractEvaluationTestCase {
+
+ protected Environment env;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ PackageFactory factory = new PackageFactory(TestRuleClassProvider.getRuleClassProvider());
+ env = factory.getEnvironment();
+ }
+
+ public Environment singletonEnv(String id, Object value) {
+ Environment env = new Environment();
+ env.update(id, value);
+ return env;
+ }
+
+ @Override
+ public Object eval(String input) throws Exception {
+ return eval(parseExpr(input), env);
+ }
+
+ public void testExprs() throws Exception {
+ assertEquals("fooxbar",
+ eval("'%sx' % 'foo' + 'bar'"));
+ assertEquals("fooxbar",
+ eval("('%sx' % 'foo') + 'bar'"));
+ assertEquals("foobarx",
+ eval("'%sx' % ('foo' + 'bar')"));
+ assertEquals(579,
+ eval("123 + 456"));
+ assertEquals(333,
+ eval("456 - 123"));
+ assertEquals(2,
+ eval("8 % 3"));
+
+ checkEvalError("3 % 'foo'", "unsupported operand type(s) for %: 'int' and 'string'");
+ }
+
+ public void testListExprs() throws Exception {
+ assertEquals(Arrays.asList(1, 2, 3),
+ eval("[1, 2, 3]"));
+ assertEquals(Arrays.asList(1, 2, 3),
+ eval("(1, 2, 3)"));
+ }
+
+ public void testStringFormatMultipleArgs() throws Exception {
+ assertEquals("XYZ", eval("'%sY%s' % ('X', 'Z')"));
+ }
+
+ public void testAndOr() throws Exception {
+ assertEquals(8, eval("8 or 9"));
+ assertEquals(8, eval("8 or foo")); // check that 'foo' is not evaluated
+ assertEquals(9, eval("0 or 9"));
+ assertEquals(9, eval("8 and 9"));
+ assertEquals(0, eval("0 and 9"));
+ assertEquals(0, eval("0 and foo")); // check that 'foo' is not evaluated
+
+ assertEquals(2, eval("1 and 2 or 3"));
+ assertEquals(3, eval("0 and 2 or 3"));
+ assertEquals(3, eval("1 and 0 or 3"));
+
+ assertEquals(1, eval("1 or 2 and 3"));
+ assertEquals(3, eval("0 or 2 and 3"));
+ assertEquals(0, eval("0 or 0 and 3"));
+ assertEquals(1, eval("1 or 0 and 3"));
+ assertEquals(1, eval("1 or 0 and 3"));
+
+ assertEquals(9, eval("\"\" or 9"));
+ assertEquals("abc", eval("\"abc\" or 9"));
+ assertEquals(Environment.NONE, eval("None and 1"));
+ }
+
+ public void testNot() throws Exception {
+ assertEquals(false, eval("not 1"));
+ assertEquals(true, eval("not ''"));
+ }
+
+ public void testNotWithLogicOperators() throws Exception {
+ assertEquals(0, eval("0 and not 0"));
+ assertEquals(0, eval("not 0 and 0"));
+
+ assertEquals(true, eval("1 and not 0"));
+ assertEquals(true, eval("not 0 or 0"));
+
+ assertEquals(0, eval("not 1 or 0"));
+ assertEquals(1, eval("not 1 or 1"));
+
+ assertEquals(true, eval("not (0 and 0)"));
+ assertEquals(false, eval("not (1 or 0)"));
+ }
+
+ public void testNotWithArithmeticOperators() throws Exception {
+ assertEquals(true, eval("not 0 + 0"));
+ assertEquals(false, eval("not 2 - 1"));
+ }
+
+ public void testNotWithCollections() throws Exception {
+ assertEquals(true, eval("not []"));
+ assertEquals(false, eval("not {'a' : 1}"));
+ }
+
+ public void testEquality() throws Exception {
+ assertEquals(true, eval("1 == 1"));
+ assertEquals(false, eval("1 == 2"));
+ assertEquals(true, eval("'hello' == 'hel' + 'lo'"));
+ assertEquals(false, eval("'hello' == 'bye'"));
+ assertEquals(true, eval("[1, 2] == [1, 2]"));
+ assertEquals(false, eval("[1, 2] == [2, 1]"));
+ assertEquals(true, eval("None == None"));
+ }
+
+ public void testInequality() throws Exception {
+ assertEquals(false, eval("1 != 1"));
+ assertEquals(true, eval("1 != 2"));
+ assertEquals(false, eval("'hello' != 'hel' + 'lo'"));
+ assertEquals(true, eval("'hello' != 'bye'"));
+ assertEquals(false, eval("[1, 2] != [1, 2]"));
+ assertEquals(true, eval("[1, 2] != [2, 1]"));
+ }
+
+ public void testEqualityPrecedence() throws Exception {
+ assertEquals(true, eval("1 + 3 == 2 + 2"));
+ assertEquals(true, eval("not 1 == 2"));
+ assertEquals(false, eval("not 1 != 2"));
+ assertEquals(true, eval("2 and 3 == 3 or 1"));
+ assertEquals(2, eval("2 or 3 == 3 and 1"));
+ }
+
+ public void testLessThan() throws Exception {
+ assertEquals(true, eval("1 <= 1"));
+ assertEquals(false, eval("1 < 1"));
+ assertEquals(true, eval("'a' <= 'b'"));
+ assertEquals(false, eval("'c' < 'a'"));
+ }
+
+ public void testGreaterThan() throws Exception {
+ assertEquals(true, eval("1 >= 1"));
+ assertEquals(false, eval("1 > 1"));
+ assertEquals(false, eval("'a' >= 'b'"));
+ assertEquals(true, eval("'c' > 'a'"));
+ }
+
+ public void testCompareStringInt() throws Exception {
+ checkEvalError("'a' >= 1", "Cannot compare string with int");
+ }
+
+ public void testNotComparable() throws Exception {
+ checkEvalError("[1, 2] < [1, 3]", "[1, 2] is not comparable");
+ }
+
+ public void testSumFunction() throws Exception {
+ Function sum = new AbstractFunction("sum") {
+ @Override
+ public Object call(List<Object> args, Map<String, Object> kwargs,
+ FuncallExpression ast, Environment env) {
+ int sum = 0;
+ for (Object arg : args) {
+ sum += (Integer) arg;
+ }
+ return sum;
+ }
+ };
+
+ Environment env = singletonEnv(sum.getName(), sum);
+
+ String callExpr = "sum(1, 2, 3, 4, 5, 6)";
+ assertEquals(21, eval(callExpr, env));
+
+ assertEquals(sum, eval("sum", env));
+
+ assertEquals(0, eval("sum(a=1, b=2)", env));
+
+ // rebind 'sum' in a new environment:
+ env = new Environment();
+ exec(parseStmt("sum = 123456"), env);
+
+ assertEquals(123456, env.lookup("sum"));
+
+ // now we can't call it any more:
+ checkEvalError(callExpr, env, "'int' object is not callable");
+
+ assertEquals(123456, eval("sum", env));
+ }
+
+ public void testKeywordArgs() throws Exception {
+
+ // This function returns the list of keyword-argument keys or values,
+ // depending on whether its first (integer) parameter is zero.
+ Function keyval = new AbstractFunction("keyval") {
+ @Override
+ public Object call(List<Object> args,
+ final Map<String, Object> kwargs,
+ FuncallExpression ast,
+ Environment env) {
+ ArrayList<String> keys = new ArrayList<>(kwargs.keySet());
+ Collections.sort(keys);
+ if ((Integer) args.get(0) == 0) {
+ return keys;
+ } else {
+ return Lists.transform(keys, new com.google.common.base.Function<String, Object> () {
+ @Override public Object apply (String s) {
+ return kwargs.get(s);
+ }});
+ }
+ }
+ };
+
+ Environment env = singletonEnv(keyval.getName(), keyval);
+
+ assertEquals(eval("['bar', 'foo', 'wiz']"),
+ eval("keyval(0, foo=1, bar='bar', wiz=[1,2,3])", env));
+
+ assertEquals(eval("['bar', 1, [1,2,3]]"),
+ eval("keyval(1, foo=1, bar='bar', wiz=[1,2,3])", env));
+ }
+
+ public void testMult() throws Exception {
+ assertEquals(42, eval("6 * 7"));
+
+ assertEquals("ababab", eval("3 * 'ab'"));
+ assertEquals("", eval("0 * 'ab'"));
+ assertEquals("100000", eval("'1' + '0' * 5"));
+ }
+
+ public void testConcatStrings() throws Exception {
+ assertEquals("foobar", eval("'foo' + 'bar'"));
+ }
+
+ public void testConcatLists() throws Exception {
+ // list
+ Object x = eval("[1,2] + [3,4]");
+ assertEquals(Arrays.asList(1, 2, 3, 4), x);
+ assertFalse(EvalUtils.isImmutable(x));
+
+ // tuple
+ x = eval("(1,2) + (3,4)");
+ assertEquals(Arrays.asList(1, 2, 3, 4), x);
+ assertTrue(EvalUtils.isImmutable(x));
+
+ checkEvalError("(1,2) + [3,4]", // list + tuple
+ "can only concatenate list (not \"tuple\") to list");
+ }
+
+ @SuppressWarnings("unchecked")
+ public void testListComprehensions() throws Exception {
+ Iterable<Object> eval = (Iterable<Object>) eval(
+ "['foo/%s.java' % x for x in []]");
+ assertThat(eval).isEmpty();
+
+ eval = (Iterable<Object>) eval(
+ "['foo/%s.java' % x for x in ['bar', 'wiz', 'quux']]");
+ assertThat(eval).containsExactly("foo/bar.java", "foo/wiz.java", "foo/quux.java").inOrder();
+
+ eval = (Iterable<Object>) eval(
+ "['%s/%s.java' % (x, y) "
+ + "for x in ['foo', 'bar'] "
+ + "for y in ['baz', 'wiz', 'quux']]");
+ assertThat(eval).containsExactly("foo/baz.java", "foo/wiz.java", "foo/quux.java",
+ "bar/baz.java", "bar/wiz.java", "bar/quux.java").inOrder();
+
+ eval = (Iterable<Object>) eval(
+ "['%s/%s.java' % (x, x) "
+ + "for x in ['foo', 'bar'] "
+ + "for x in ['baz', 'wiz', 'quux']]");
+ assertThat(eval).containsExactly("baz/baz.java", "wiz/wiz.java", "quux/quux.java",
+ "baz/baz.java", "wiz/wiz.java", "quux/quux.java").inOrder();
+
+ eval = (Iterable<Object>) eval(
+ "['%s/%s.%s' % (x, y, z) "
+ + "for x in ['foo', 'bar'] "
+ + "for y in ['baz', 'wiz', 'quux'] "
+ + "for z in ['java', 'cc']]");
+ assertThat(eval).containsExactly("foo/baz.java", "foo/baz.cc", "foo/wiz.java", "foo/wiz.cc",
+ "foo/quux.java", "foo/quux.cc", "bar/baz.java", "bar/baz.cc", "bar/wiz.java", "bar/wiz.cc",
+ "bar/quux.java", "bar/quux.cc").inOrder();
+ }
+
+ // TODO(bazel-team): should this test work in Skylark?
+ @SuppressWarnings("unchecked")
+ public void testListComprehensionModifiesGlobalEnv() throws Exception {
+ Environment env = singletonEnv("x", 42);
+ assertThat((Iterable<Object>) eval(parseExpr("[x + 1 for x in [1,2,3]]"), env))
+ .containsExactly(2, 3, 4).inOrder();
+ assertEquals(3, env.lookup("x")); // (x is global)
+ }
+
+ public void testDictComprehensions() throws Exception {
+ assertEquals(Collections.emptyMap(), eval("{x : x for x in []}"));
+ assertEquals(ImmutableMap.of(1, 1, 2, 2), eval("{x : x for x in [1, 2]}"));
+ assertEquals(ImmutableMap.of("a", "v_a", "b", "v_b"),
+ eval("{x : 'v_' + x for x in ['a', 'b']}"));
+ assertEquals(ImmutableMap.of("k_a", "a", "k_b", "b"),
+ eval("{'k_' + x : x for x in ['a', 'b']}"));
+ assertEquals(ImmutableMap.of("k_a", "v_a", "k_b", "v_b"),
+ eval("{'k_' + x : 'v_' + x for x in ['a', 'b']}"));
+ }
+
+ public void testDictComprehensions_MultipleKey() throws Exception {
+ assertEquals(ImmutableMap.of(1, 1, 2, 2), eval("{x : x for x in [1, 2, 1]}"));
+ assertEquals(ImmutableMap.of("ab", "ab", "c", "c"),
+ eval("{x : x for x in ['ab', 'c', 'a' + 'b']}"));
+ }
+
+ public void testDictComprehensions_ToString() throws Exception {
+ assertEquals("{x: x for x in [1, 2]}", parseExpr("{x : x for x in [1, 2]}").toString());
+ assertEquals("{x + 'a': x for x in [1, 2]}",
+ parseExpr("{x + 'a' : x for x in [1, 2]}").toString());
+ }
+
+ public void testListConcatenation() throws Exception {
+ assertEquals(Arrays.asList(1, 2, 3, 4), eval("[1, 2] + [3, 4]", env));
+ assertEquals(ImmutableList.of(1, 2, 3, 4), eval("(1, 2) + (3, 4)", env));
+ checkEvalError("[1, 2] + (3, 4)", "can only concatenate tuple (not \"list\") to tuple");
+ checkEvalError("(1, 2) + [3, 4]", "can only concatenate list (not \"tuple\") to list");
+ }
+
+ public void testListComprehensionFailsOnNonSequence() throws Exception {
+ checkEvalError("[x + 1 for x in 123]", "type 'int' is not an iterable");
+ }
+
+ @SuppressWarnings("unchecked")
+ public void testListComprehensionOnString() throws Exception {
+ assertThat((Iterable<Object>) eval("[x for x in 'abc']")).containsExactly("a", "b", "c")
+ .inOrder();
+ }
+
+ public void testInvalidAssignment() throws Exception {
+ Environment env = singletonEnv("x", 1);
+ checkEvalError(parseStmt("x + 1 = 2"), env, "can only assign to variables, not to 'x + 1'");
+ }
+
+ public void testListComprehensionOnDictionary() throws Exception {
+ List<Statement> input = parseFile("val = ['var_' + n for n in {'a':1,'b':2}]");
+ exec(input, env);
+ Iterable<?> result = (Iterable<?>) env.lookup("val");
+ assertThat(result).hasSize(2);
+ assertEquals("var_a", Iterables.get(result, 0));
+ assertEquals("var_b", Iterables.get(result, 1));
+ }
+
+ public void testListComprehensionOnDictionaryCompositeExpression() throws Exception {
+ exec(parseFile("d = {1:'a',2:'b'}\n"
+ + "l = [d[x] for x in d]"), env);
+ assertEquals("[a, b]", env.lookup("l").toString());
+ }
+
+ public void testInOnListContains() throws Exception {
+ assertEquals(Boolean.TRUE, eval("'b' in ['a', 'b']"));
+ }
+
+ public void testInOnListDoesNotContain() throws Exception {
+ assertEquals(Boolean.FALSE, eval("'c' in ['a', 'b']"));
+ }
+
+ public void testInOnTupleContains() throws Exception {
+ assertEquals(Boolean.TRUE, eval("'b' in ('a', 'b')"));
+ }
+
+ public void testInOnTupleDoesNotContain() throws Exception {
+ assertEquals(Boolean.FALSE, eval("'c' in ('a', 'b')"));
+ }
+
+ public void testInOnDictContains() throws Exception {
+ assertEquals(Boolean.TRUE, eval("'b' in {'a' : 1, 'b' : 2}"));
+ }
+
+ public void testInOnDictDoesNotContainKey() throws Exception {
+ assertEquals(Boolean.FALSE, eval("'c' in {'a' : 1, 'b' : 2}"));
+ }
+
+ public void testInOnDictDoesNotContainVal() throws Exception {
+ assertEquals(Boolean.FALSE, eval("1 in {'a' : 1, 'b' : 2}"));
+ }
+
+ public void testInOnStringContains() throws Exception {
+ assertEquals(Boolean.TRUE, eval("'b' in 'abc'"));
+ }
+
+ public void testInOnStringDoesNotContain() throws Exception {
+ assertEquals(Boolean.FALSE, eval("'d' in 'abc'"));
+ }
+
+ public void testInOnStringLeftNotString() throws Exception {
+ checkEvalError("1 in '123'",
+ "in operator only works on strings if the left operand is also a string");
+ }
+
+ public void testInFailsOnNonIterable() throws Exception {
+ checkEvalError("'a' in 1",
+ "in operator only works on lists, tuples, dictionaries and strings");
+ }
+
+ public void testInCompositeForPrecedence() throws Exception {
+ assertEquals(0, eval("not 'a' in ['a'] or 0"));
+ }
+
+ private Object createObjWithStr() {
+ return new Object() {
+ @Override
+ public String toString() {
+ return "str marker";
+ }
+ };
+ }
+
+ public void testPercOnObject() throws Exception {
+ env.update("obj", createObjWithStr());
+ assertEquals("str marker", eval("'%s' % obj", env));
+ }
+
+ public void testPercOnObjectList() throws Exception {
+ env.update("obj", createObjWithStr());
+ assertEquals("str marker str marker", eval("'%s %s' % (obj, obj)", env));
+ }
+
+ public void testPercOnObjectInvalidFormat() throws Exception {
+ env.update("obj", createObjWithStr());
+ checkEvalError("'%d' % obj", env, "invalid arguments for format string");
+ }
+
+ @SuppressWarnings("unchecked")
+ public void testDictKeys() throws Exception {
+ exec("v = {'a': 1}.keys() + ['b', 'c']", env);
+ assertThat((Iterable<Object>) env.lookup("v")).containsExactly("a", "b", "c").inOrder();
+ }
+
+ public void testDictKeysTooManyArgs() throws Exception {
+ checkEvalError("{'a': 1}.keys('abc')", env, "Invalid number of arguments (expected 0)");
+ checkEvalError("{'a': 1}.keys(arg='abc')", env, "Invalid number of arguments (expected 0)");
+ }
+
+ protected void checkEvalError(String input, String msg) throws Exception {
+ checkEvalError(input, env, msg);
+ }
+
+ protected void checkEvalError(String input, Environment env, String msg) throws Exception {
+ try {
+ eval(input, env);
+ fail();
+ } catch (EvalException e) {
+ assertEquals(msg, e.getMessage());
+ }
+ }
+
+ protected void checkEvalError(Statement input, Environment env, String msg) throws Exception {
+ checkEvalError(ImmutableList.of(input), env, msg);
+ }
+
+ protected void checkEvalError(List<Statement> input, Environment env, String msg)
+ throws Exception {
+ try {
+ exec(input, env);
+ fail();
+ } catch (EvalException e) {
+ assertEquals(msg, e.getMessage());
+ }
+ }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/syntax/FunctionTest.java b/src/test/java/com/google/devtools/build/lib/syntax/FunctionTest.java
new file mode 100644
index 0000000000..fc14922e2e
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/syntax/FunctionTest.java
@@ -0,0 +1,424 @@
+// Copyright 2014 Google Inc. 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 com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableMap;
+import com.google.devtools.build.lib.packages.MethodLibrary;
+import com.google.devtools.build.lib.syntax.SkylarkType.SkylarkFunctionType;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A test class for functions and scoping.
+ */
+public class FunctionTest extends AbstractEvaluationTestCase {
+
+ private Environment env;
+
+ private static final ImmutableMap<String, SkylarkType> OUTER_FUNC_TYPES =
+ ImmutableMap.<String, SkylarkType>of(
+ "outer_func", SkylarkFunctionType.of("outer_func", SkylarkType.NONE));
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ env = new SkylarkEnvironment(syntaxEvents.collector());
+ }
+
+ public void testFunctionDef() throws Exception {
+ List<Statement> input = parseFileForSkylark(
+ "def func(a,b,c):\n"
+ + " a = 1\n"
+ + " b = a\n");
+
+ exec(input, env);
+ UserDefinedFunction stmt = (UserDefinedFunction) env.lookup("func");
+ assertNotNull(stmt);
+ assertEquals("func", stmt.getName());
+ assertEquals(3, stmt.getFunctionSignature().getSignature().getShape().getMandatoryPositionals());
+ assertThat(stmt.getStatements()).hasSize(2);
+ }
+
+ public void testFunctionDefDuplicateArguments() throws Exception {
+ syntaxEvents.setFailFast(false);
+ parseFileForSkylark(
+ "def func(a,b,a):\n"
+ + " a = 1\n");
+ syntaxEvents.assertContainsEvent("duplicate parameter name in function definition");
+ }
+
+ public void testFunctionDefCallOuterFunc() throws Exception {
+ final List<Object> params = new ArrayList<>();
+ List<Statement> input = parseFileForSkylark(
+ "def func(a):\n"
+ + " outer_func(a)\n"
+ + "func(1)\n"
+ + "func(2)",
+ OUTER_FUNC_TYPES);
+ createOuterFunction(env, params);
+ exec(input, env);
+ assertThat(params).containsExactly(1, 2).inOrder();
+ }
+
+ private void createOuterFunction(Environment env, final List<Object> params) {
+ Function outerFunc = new AbstractFunction("outer_func") {
+
+ @Override
+ public Object call(List<Object> args, Map<String, Object> kwargs, FuncallExpression ast,
+ Environment env) throws EvalException, InterruptedException {
+ params.addAll(args);
+ return Environment.NONE;
+ }
+ };
+ env.update("outer_func", outerFunc);
+ }
+
+ public void testFunctionDefNoEffectOutsideScope() throws Exception {
+ List<Statement> input = parseFileForSkylark(
+ "def func():\n"
+ + " a = 2\n"
+ + "func()\n");
+ env.update("a", 1);
+ exec(input, env);
+ assertEquals(1, env.lookup("a"));
+ }
+
+ public void testFunctionDefGlobalVaribleReadInFunction() throws Exception {
+ List<Statement> input = parseFileForSkylark(
+ "a = 1\n"
+ + "def func():\n"
+ + " b = a\n"
+ + " return b\n"
+ + "c = func()\n");
+ exec(input, env);
+ assertEquals(1, env.lookup("c"));
+ }
+
+ public void testFunctionDefLocalGlobalScope() throws Exception {
+ List<Statement> input = parseFileForSkylark(
+ "a = 1\n"
+ + "def func():\n"
+ + " a = 2\n"
+ + " b = a\n"
+ + " return b\n"
+ + "c = func()\n");
+ exec(input, env);
+ assertEquals(2, env.lookup("c"));
+ }
+
+ public void testFunctionDefLocalVariableReferencedBeforeAssignment() throws Exception {
+ List<Statement> input = parseFileForSkylark(
+ "a = 1\n"
+ + "def func():\n"
+ + " b = a\n"
+ + " a = 2\n"
+ + " return b\n"
+ + "c = func()\n");
+ try {
+ exec(input, env);
+ fail();
+ } catch (EvalException e) {
+ assertThat(e.getMessage()).contains("Variable 'a' is referenced before assignment.");
+ }
+ }
+
+ public void testFunctionDefLocalVariableReferencedAfterAssignment() throws Exception {
+ List<Statement> input = parseFileForSkylark(
+ "a = 1\n"
+ + "def func():\n"
+ + " a = 2\n"
+ + " b = a\n"
+ + " a = 3\n"
+ + " return b\n"
+ + "c = func()\n");
+ exec(input, env);
+ assertEquals(2, env.lookup("c"));
+ }
+
+ @SuppressWarnings("unchecked")
+ public void testSkylarkGlobalComprehensionIsAllowed() throws Exception {
+ List<Statement> input = parseFileForSkylark(
+ "a = [i for i in [1, 2, 3]]\n");
+ exec(input, env);
+ assertThat((Iterable<Object>) env.lookup("a")).containsExactly(1, 2, 3).inOrder();
+ }
+
+ public void testFunctionReturn() throws Exception {
+ List<Statement> input = parseFileForSkylark(
+ "def func():\n"
+ + " return 2\n"
+ + "b = func()\n");
+ exec(input, env);
+ assertEquals(2, env.lookup("b"));
+ }
+
+ public void testFunctionReturnFromALoop() throws Exception {
+ List<Statement> input = parseFileForSkylark(
+ "def func():\n"
+ + " for i in [1, 2, 3, 4, 5]:\n"
+ + " return i\n"
+ + "b = func()\n");
+ exec(input, env);
+ assertEquals(1, env.lookup("b"));
+ }
+
+ public void testFunctionExecutesProperly() throws Exception {
+ List<Statement> input = parseFileForSkylark(
+ "def func(a):\n"
+ + " b = 1\n"
+ + " if a:\n"
+ + " b = 2\n"
+ + " return b\n"
+ + "c = func(0)\n"
+ + "d = func(1)\n");
+ exec(input, env);
+ assertEquals(1, env.lookup("c"));
+ assertEquals(2, env.lookup("d"));
+ }
+
+ public void testFunctionCallFromFunction() throws Exception {
+ final List<Object> params = new ArrayList<>();
+ List<Statement> input = parseFileForSkylark(
+ "def func2(a):\n"
+ + " outer_func(a)\n"
+ + "def func1(b):\n"
+ + " func2(b)\n"
+ + "func1(1)\n"
+ + "func1(2)\n",
+ OUTER_FUNC_TYPES);
+ createOuterFunction(env, params);
+ exec(input, env);
+ assertThat(params).containsExactly(1, 2).inOrder();
+ }
+
+ public void testFunctionCallFromFunctionReadGlobalVar() throws Exception {
+ List<Statement> input = parseFileForSkylark(
+ "a = 1\n"
+ + "def func2():\n"
+ + " return a\n"
+ + "def func1():\n"
+ + " return func2()\n"
+ + "b = func1()\n");
+ exec(input, env);
+ assertEquals(1, env.lookup("b"));
+ }
+
+ public void testSingleLineFunction() throws Exception {
+ List<Statement> input = parseFileForSkylark(
+ "def func(): return 'a'\n"
+ + "s = func()\n");
+ exec(input, env);
+ assertEquals("a", env.lookup("s"));
+ }
+
+ public void testFunctionReturnsDictionary() throws Exception {
+ MethodLibrary.setupMethodEnvironment(env);
+ List<Statement> input = parseFileForSkylark(
+ "def func(): return {'a' : 1}\n"
+ + "d = func()\n"
+ + "a = d['a']\n");
+ exec(input, env);
+ assertEquals(1, env.lookup("a"));
+ }
+
+ public void testFunctionReturnsList() throws Exception {
+ MethodLibrary.setupMethodEnvironment(env);
+ List<Statement> input = parseFileForSkylark(
+ "def func(): return [1, 2, 3]\n"
+ + "d = func()\n"
+ + "a = d[1]\n");
+ exec(input, env);
+ assertEquals(2, env.lookup("a"));
+ }
+
+ @SuppressWarnings("unchecked")
+ public void testFunctionListArgumentsAreImmutable() throws Exception {
+ MethodLibrary.setupMethodEnvironment(env);
+ List<Statement> input = parseFileForSkylark(
+ "l = [1]\n"
+ + "def func(l):\n"
+ + " l += [2]\n"
+ + "func(l)");
+ exec(input, env);
+ assertThat((Iterable<Object>) env.lookup("l")).containsExactly(1);
+ }
+
+ public void testFunctionDictArgumentsAreImmutable() throws Exception {
+ MethodLibrary.setupMethodEnvironment(env);
+ List<Statement> input = parseFileForSkylark(
+ "d = {'a' : 1}\n"
+ + "def func(d):\n"
+ + " d += {'a' : 2}\n"
+ + "func(d)");
+ exec(input, env);
+ assertEquals(ImmutableMap.of("a", 1), env.lookup("d"));
+ }
+
+ public void testFunctionNameAliasing() throws Exception {
+ List<Statement> input = parseFileForSkylark(
+ "def func(a):\n"
+ + " return a + 1\n"
+ + "alias = func\n"
+ + "r = alias(1)");
+ exec(input, env);
+ assertEquals(2, env.lookup("r"));
+ }
+
+ public void testCallingFunctionsWithMixedModeArgs() throws Exception {
+ List<Statement> input = parseFileForSkylark(
+ "def func(a, b, c):\n"
+ + " return a + b + c\n"
+ + "v = func(1, c = 2, b = 3)");
+ exec(input, env);
+ assertEquals(6, env.lookup("v"));
+ }
+
+ private String functionWithOptionalArgs() {
+ return "def func(a, b = None, c = None):\n"
+ + " r = a + 'a'\n"
+ + " if b:\n"
+ + " r += 'b'\n"
+ + " if c:\n"
+ + " r += 'c'\n"
+ + " return r\n";
+ }
+
+ public void testWhichOptionalArgsAreDefinedForFunctions() throws Exception {
+ List<Statement> input = parseFileForSkylark(
+ functionWithOptionalArgs()
+ + "v1 = func('1', 1, 1)\n"
+ + "v2 = func(b = 2, a = '2', c = 2)\n"
+ + "v3 = func('3')\n"
+ + "v4 = func('4', c = 1)\n");
+ exec(input, env);
+ assertEquals("1abc", env.lookup("v1"));
+ assertEquals("2abc", env.lookup("v2"));
+ assertEquals("3a", env.lookup("v3"));
+ assertEquals("4ac", env.lookup("v4"));
+ }
+
+ public void testDefaultArguments() throws Exception {
+ List<Statement> input = parseFileForSkylark(
+ "def func(a, b = 'b', c = 'c'):\n"
+ + " return a + b + c\n"
+ + "v1 = func('a', 'x', 'y')\n"
+ + "v2 = func(b = 'x', a = 'a', c = 'y')\n"
+ + "v3 = func('a')\n"
+ + "v4 = func('a', c = 'y')\n");
+ exec(input, env);
+ assertEquals("axy", env.lookup("v1"));
+ assertEquals("axy", env.lookup("v2"));
+ assertEquals("abc", env.lookup("v3"));
+ assertEquals("aby", env.lookup("v4"));
+ }
+
+ public void testDefaultArgumentsInsufficientArgNum() throws Exception {
+ checkError("func(a, b = null, c = null) received insufficient arguments",
+ "def func(a, b = 'b', c = 'c'):",
+ " return a + b + c",
+ "func()");
+ }
+
+ public void testKwargs() throws Exception {
+ List<Statement> input = parseFileForSkylark(
+ "def foo(a, b = 'b', c = 'c'):\n"
+ + " return a + b + c\n"
+ + "args = {'a': 'x', 'c': 'z'}\n"
+ + "v1 = foo(**args)\n"
+ + "v2 = foo('x', **{'b': 'y'})\n"
+ + "v3 = foo(c = 'z', a = 'x', **{'b': 'y'})");
+ exec(input, env);
+ assertEquals("xbz", env.lookup("v1"));
+ assertEquals("xyc", env.lookup("v2"));
+ assertEquals("xyz", env.lookup("v3"));
+ }
+
+ public void testKwargsBadKey() throws Exception {
+ checkError("Keywords must be strings, not int",
+ "def func(a, b):",
+ " return a + b",
+ "func('a', **{3: 1})");
+ }
+
+ public void testKwargsIsNotDict() throws Exception {
+ checkError("Argument after ** must be a dictionary, not int",
+ "def func(a, b):",
+ " return a + b",
+ "func('a', **42)");
+ }
+
+ public void testKwargsCollision() throws Exception {
+ checkError("func(a, b) got multiple values for keyword argument 'b'",
+ "def func(a, b):",
+ " return a + b",
+ "func('a', 'b', **{'b': 'foo'})");
+ }
+
+ public void testKwargsCollisionWithNamed() throws Exception {
+ checkError("duplicate keyword 'b' in call to func",
+ "def func(a, b):",
+ " return a + b",
+ "func('a', b = 'b', **{'b': 'foo'})");
+ }
+
+ public void testDefaultArguments2() throws Exception {
+ List<Statement> input = parseFileForSkylark(
+ "a = 2\n"
+ + "def foo(x=a): return x\n"
+ + "def bar():\n"
+ + " a = 3\n"
+ + " return foo()\n"
+ + "v = bar()\n");
+ exec(input, env);
+ assertEquals(2, env.lookup("v"));
+ }
+
+ public void testMixingPositionalOptional() throws Exception {
+ List<Statement> input = parseFileForSkylark(
+ "def f(name, value = '', optional = ''): return value\n"
+ + "v = f('name', 'value')\n");
+ exec(input, env);
+ assertEquals("value", env.lookup("v"));
+ }
+
+ public void testStarArg() throws Exception {
+ List<Statement> input = parseFileForSkylark(
+ "def f(name, value = '1', optional = '2'): return name + value + optional\n"
+ + "v1 = f(*['name', 'value'])\n"
+ + "v2 = f('0', *['name', 'value'])\n"
+ + "v3 = f('0', *['b'], optional = '3')\n"
+ + "v4 = f(*[],name='a')\n");
+ exec(input, env);
+ assertEquals("namevalue2", env.lookup("v1"));
+ assertEquals("0namevalue", env.lookup("v2"));
+ assertEquals("0b3", env.lookup("v3"));
+ assertEquals("a12", env.lookup("v4"));
+ }
+
+ private void checkError(String msg, String... lines)
+ throws Exception {
+ try {
+ List<Statement> input = parseFileForSkylark(Joiner.on("\n").join(lines));
+ exec(input, env);
+ fail();
+ } catch (EvalException e) {
+ assertEquals(msg, e.getMessage());
+ }
+ }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/syntax/GlobCriteriaTest.java b/src/test/java/com/google/devtools/build/lib/syntax/GlobCriteriaTest.java
new file mode 100644
index 0000000000..75fed7d4e2
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/syntax/GlobCriteriaTest.java
@@ -0,0 +1,169 @@
+// Copyright 2009 Google Inc. 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 com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.testutil.Suite;
+import com.google.devtools.build.lib.testutil.TestSpec;
+
+import junit.framework.TestCase;
+
+/**
+ * Links for {@link GlobCriteria}
+ */
+@TestSpec(size = Suite.SMALL_TESTS)
+public class GlobCriteriaTest extends TestCase {
+
+ public void testParse_EmptyList() throws Exception {
+ GlobCriteria gc = GlobCriteria.parse("[]");
+ assertFalse(gc.isGlob());
+ assertTrue(gc.getIncludePatterns().isEmpty());
+ assertTrue(gc.getExcludePatterns().isEmpty());
+ }
+
+ public void testParse_SingleList() throws Exception {
+ GlobCriteria gc = GlobCriteria.parse("['abc']");
+ assertFalse(gc.isGlob());
+ assertEquals(ImmutableList.of("abc"), gc.getIncludePatterns());
+ assertTrue(gc.getExcludePatterns().isEmpty());
+ }
+
+ public void testParse_MultipleList() throws Exception {
+ GlobCriteria gc = GlobCriteria.parse("['abc', 'def', 'ghi']");
+ assertFalse(gc.isGlob());
+ assertEquals(ImmutableList.of("abc", "def", "ghi"), gc.getIncludePatterns());
+ assertTrue(gc.getExcludePatterns().isEmpty());
+ }
+
+ public void testParse_EmptyGlob() throws Exception {
+ GlobCriteria gc = GlobCriteria.parse("glob([])");
+ assertTrue(gc.isGlob());
+ assertTrue(gc.getIncludePatterns().isEmpty());
+ assertTrue(gc.getExcludePatterns().isEmpty());
+ }
+
+ public void testParse_SingleGlob() throws Exception {
+ GlobCriteria gc = GlobCriteria.parse("glob(['abc'])");
+ assertTrue(gc.isGlob());
+ assertEquals(ImmutableList.of("abc"), gc.getIncludePatterns());
+ assertTrue(gc.getExcludePatterns().isEmpty());
+ }
+
+ public void testParse_MultipleGlob() throws Exception {
+ GlobCriteria gc = GlobCriteria.parse("glob(['abc', 'def', 'ghi'])");
+ assertTrue(gc.isGlob());
+ assertEquals(ImmutableList.of("abc", "def", "ghi"), gc.getIncludePatterns());
+ assertTrue(gc.getExcludePatterns().isEmpty());
+ }
+
+ public void testParse_EmptyGlobWithExclude() throws Exception {
+ GlobCriteria gc = GlobCriteria.parse("glob([], exclude=['xyz'])");
+ assertTrue(gc.isGlob());
+ assertTrue(gc.getIncludePatterns().isEmpty());
+ assertEquals(ImmutableList.of("xyz"), gc.getExcludePatterns());
+ }
+
+ public void testParse_SingleGlobWithExclude() throws Exception {
+ GlobCriteria gc = GlobCriteria.parse("glob(['abc'], exclude=['xyz'])");
+ assertTrue(gc.isGlob());
+ assertEquals(ImmutableList.of("abc"), gc.getIncludePatterns());
+ assertEquals(ImmutableList.of("xyz"), gc.getExcludePatterns());
+ }
+
+ public void testParse_MultipleGlobWithExclude() throws Exception {
+ GlobCriteria gc = GlobCriteria.parse("glob(['abc', 'def', 'ghi'], exclude=['xyz'])");
+ assertTrue(gc.isGlob());
+ assertEquals(ImmutableList.of("abc", "def", "ghi"), gc.getIncludePatterns());
+ assertEquals(ImmutableList.of("xyz"), gc.getExcludePatterns());
+ }
+
+ public void testParse_MultipleGlobWithMultipleExclude() throws Exception {
+ GlobCriteria gc = GlobCriteria.parse(
+ "glob(['abc', 'def', 'ghi'], exclude=['rst', 'uvw', 'xyz'])");
+ assertTrue(gc.isGlob());
+ assertEquals(ImmutableList.of("abc", "def", "ghi"), gc.getIncludePatterns());
+ assertEquals(ImmutableList.of("rst", "uvw", "xyz"), gc.getExcludePatterns());
+ }
+
+ public void testParse_GlobWithSlashesAndWildcards() throws Exception {
+ GlobCriteria gc = GlobCriteria.parse("glob(['java/src/net/jsunit/*.java'])");
+ assertTrue(gc.isGlob());
+ assertEquals(ImmutableList.of("java/src/net/jsunit/*.java"), gc.getIncludePatterns());
+ assertTrue(gc.getExcludePatterns().isEmpty());
+ }
+
+ public void testParse_ExcludeWithInvalidLabel() throws Exception {
+ GlobCriteria gc = GlobCriteria.parse("glob(['abc', 'def', 'ghi'], exclude=['xyz~'])");
+ assertTrue(gc.isGlob());
+ assertEquals(ImmutableList.of("abc", "def", "ghi"), gc.getIncludePatterns());
+ assertEquals(ImmutableList.of("xyz~"), gc.getExcludePatterns());
+ }
+
+ public void testParse_InvalidFormat_TooManySpacesList() throws Exception {
+ try {
+ GlobCriteria.parse("glob(['abc, 'def', 'ghi'], exclude=['xyz~'])");
+ fail();
+ } catch (IllegalArgumentException e) {
+ // expected
+ }
+ }
+
+ public void testParse_InvalidFormat_MissingQuote() throws Exception {
+ try {
+ GlobCriteria.parse("glob(['abc, 'def', 'ghi'], exclude=['xyz~'])");
+ fail();
+ } catch (IllegalArgumentException e) {
+ // expected
+ }
+ }
+
+ public void testParse_InvalidFormat_TooManySpacesExclude() throws Exception {
+ try {
+ GlobCriteria.parse("glob(['abc', 'def', 'ghi'], exclude=['xyz~'])");
+ fail();
+ } catch (IllegalArgumentException e) {
+ // expected
+ }
+ }
+
+ public void testParse_InvalidFormat_MissingQuoteExclude() throws Exception {
+ try {
+ GlobCriteria.parse("glob(['abc, 'def', 'ghi'], exclude=['xyz~])");
+ fail();
+ } catch (IllegalArgumentException e) {
+ // expected
+ }
+ }
+
+ public void testParse_InvalidFormat_ExcludeWithList() throws Exception {
+ try {
+ GlobCriteria.parse("['abc, 'def', 'ghi'], exclude=['xyz~']");
+ fail();
+ } catch (IllegalArgumentException e) {
+ // expected
+ }
+ }
+
+ public void testParse_veryLongString() throws Exception {
+ StringBuilder builder = new StringBuilder();
+ builder.append("['File0.java'");
+ for (int i = 1; i < 5000; ++i) {
+ builder.append(", 'File").append(i).append(".java'");
+ }
+ builder.append("]");
+ String s = builder.toString();
+ GlobCriteria gc = GlobCriteria.parse(s);
+ assertEquals(s, gc.toString());
+ }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/syntax/GlobListTest.java b/src/test/java/com/google/devtools/build/lib/syntax/GlobListTest.java
new file mode 100644
index 0000000000..94e066782b
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/syntax/GlobListTest.java
@@ -0,0 +1,103 @@
+// Copyright 2009 Google Inc. 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 org.junit.Assert.assertEquals;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.testutil.Suite;
+import com.google.devtools.build.lib.testutil.TestSpec;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.List;
+
+/**
+ * Tests for {@link GlobList}
+ */
+@TestSpec(size = Suite.SMALL_TESTS)
+@RunWith(JUnit4.class)
+public class GlobListTest {
+
+ @Test
+ public void testParse_glob() throws Exception {
+ String expression = "glob(['abc'])";
+ assertEquals(expression, GlobList.parse(expression).toExpression());
+ }
+
+ @Test
+ public void testParse_multipleGlobs() throws Exception {
+ String expression = "glob(['abc']) + glob(['def']) + glob(['ghi'])";
+ assertEquals(expression, GlobList.parse(expression).toExpression());
+ }
+
+ @Test
+ public void testParse_multipleLists() throws Exception {
+ String expression = "['abc'] + ['def'] + ['ghi']";
+ assertEquals(expression, GlobList.parse(expression).toExpression());
+ }
+
+ @Test
+ public void testParse_complexExpression() throws Exception {
+ String expression = "glob(['abc', 'def', 'ghi'], "
+ + "exclude=['rst', 'uvw', 'xyz']) "
+ + "+ glob(['abc', 'def', 'ghi'], exclude=['rst', 'uvw', 'xyz'])";
+ assertEquals(expression, GlobList.parse(expression).toExpression());
+ }
+
+ @Test
+ public void testConcat_GlobToGlob() throws Exception {
+ GlobList<String> glob1 = GlobList.parse(
+ "glob(['abc'], exclude=['def']) + glob(['xyz'])");
+ GlobList<String> glob2 = GlobList.parse(
+ "glob(['xyzzy']) + glob(['foo'], exclude=['bar'])");
+ GlobList<String> cat = GlobList.concat(glob1, glob2);
+ assertEquals(glob1.toExpression() + " + " + glob2.toExpression(), cat.toExpression());
+ }
+
+ @Test
+ public void testConcat_GlobToList() throws Exception {
+ GlobList<String> glob = GlobList.parse(
+ "glob(['abc'], exclude=['def']) + glob(['xyz'])");
+ List<String> list = ImmutableList.of("xyzzy", "foo", "bar");
+ GlobList<String> cat = GlobList.concat(list, glob);
+ assertEquals("['xyzzy', 'foo', 'bar'] + glob(['abc'], exclude=['def']) + glob(['xyz'])",
+ cat.toExpression());
+ }
+
+ @Test
+ public void testConcat_ListToGlob() throws Exception {
+ GlobList<String> glob = GlobList.parse(
+ "glob(['abc'], exclude=['def']) + glob(['xyz'])");
+ List<String> list = ImmutableList.of("xyzzy", "foo", "bar");
+ GlobList<String> cat = GlobList.concat(glob, list);
+ assertEquals("glob(['abc'], exclude=['def']) + glob(['xyz']) + ['xyzzy', 'foo', 'bar']",
+ cat.toExpression());
+ }
+
+ @Test
+ public void testGetCriteria() throws Exception {
+ List<String> include = ImmutableList.of("abc", "def", "ghi");
+ List<String> exclude = ImmutableList.of("rst", "uvw", "xyz");
+ List<String> matches = ImmutableList.of("xyzzy", "foo", "bar");
+ GlobList<String> glob = GlobList.captureResults(include, exclude, matches);
+ assertEquals(matches, glob);
+ ImmutableList<GlobCriteria> criteria = glob.getCriteria();
+ assertEquals(1, criteria.size());
+ assertEquals(include, criteria.get(0).getIncludePatterns());
+ assertEquals(exclude, criteria.get(0).getExcludePatterns());
+ }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/syntax/LabelTest.java b/src/test/java/com/google/devtools/build/lib/syntax/LabelTest.java
new file mode 100644
index 0000000000..a0daf20ebd
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/syntax/LabelTest.java
@@ -0,0 +1,364 @@
+// Copyright 2005 Google Inc. 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.devtools.build.lib.testutil.MoreAsserts.assertContainsRegex;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import com.google.devtools.build.lib.syntax.Label.SyntaxException;
+import com.google.devtools.build.lib.testutil.TestUtils;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.regex.Pattern;
+
+/**
+ * Tests for {@link Label}.
+ */
+@RunWith(JUnit4.class)
+public class LabelTest {
+
+ private static final String BAD_PACKAGE_CHARS =
+ "package names may contain only A-Z, a-z, 0-9, '/', '-' and '_'";
+
+ private static final String TARGET_UPLEVEL =
+ "target names may not contain up-level references '..'";
+
+ @Test
+ public void testAbsolute() throws Exception {
+ {
+ Label l = Label.parseAbsolute("//foo/bar:baz");
+ assertEquals("foo/bar", l.getPackageName());
+ assertEquals("baz", l.getName());
+ }
+ {
+ Label l = Label.parseAbsolute("//foo/bar");
+ assertEquals("foo/bar", l.getPackageName());
+ assertEquals("bar", l.getName());
+ }
+ }
+
+ private static String parseCommandLine(String label, String prefix) throws SyntaxException {
+ return Label.parseCommandLineLabel(label, new PathFragment(prefix)).toString();
+ }
+
+ @Test
+ public void testLabelResolution() throws Exception {
+ assertEquals("//absolute:label", parseCommandLine("//absolute:label", ""));
+ assertEquals("//absolute:label", parseCommandLine("//absolute:label", "absolute"));
+ assertEquals("//absolute:label", parseCommandLine(":label", "absolute"));
+ assertEquals("//absolute:label", parseCommandLine("label", "absolute"));
+ assertEquals("//absolute:label", parseCommandLine("absolute:label", ""));
+ assertEquals("//absolute/path:label", parseCommandLine("path:label", "absolute"));
+ assertEquals("//absolute/path:label/path", parseCommandLine("path:label/path", "absolute"));
+ assertEquals("//absolute:label/path", parseCommandLine("label/path", "absolute"));
+ }
+
+ @Test
+ public void testLabelResolutionAbsolutePath() throws Exception {
+ try {
+ parseCommandLine("//absolute:label", "/absolute");
+ fail();
+ } catch (IllegalArgumentException e) {
+ // Expected exception
+ }
+ }
+
+ @Test
+ public void testLabelResolutionBadSyntax() throws Exception {
+ try {
+ parseCommandLine("//absolute:A+bad%syntax", "");
+ fail();
+ } catch (SyntaxException e) {
+ // Expected exception
+ }
+ }
+
+ @Test
+ public void testGetRelative() throws Exception {
+ Label base = Label.parseAbsolute("//foo/bar:baz");
+ {
+ Label l = base.getRelative("//p1/p2:target");
+ assertEquals("p1/p2", l.getPackageName());
+ assertEquals("target", l.getName());
+ }
+ {
+ Label l = base.getRelative(":quux");
+ assertEquals("foo/bar", l.getPackageName());
+ assertEquals("quux", l.getName());
+ }
+ try {
+ base.getRelative("/p1/p2:target");
+ fail();
+ } catch (Label.SyntaxException e) {
+ /* ok */
+ }
+ try {
+ base.getRelative("quux:");
+ fail();
+ } catch (Label.SyntaxException e) {
+ /* ok */
+ }
+ try {
+ base.getRelative(":");
+ fail();
+ } catch (Label.SyntaxException e) {
+ /* ok */
+ }
+ try {
+ base.getRelative("::");
+ fail();
+ } catch (Label.SyntaxException e) {
+ /* ok */
+ }
+ }
+
+ @Test
+ public void testFactory() throws Exception {
+ Label l = Label.create("foo/bar", "quux");
+ assertEquals("foo/bar", l.getPackageName());
+ assertEquals("quux", l.getName());
+ }
+
+ @Test
+ public void testIdentities() throws Exception {
+
+ Label l1 = Label.parseAbsolute("//foo/bar:baz");
+ Label l2 = Label.parseAbsolute("//foo/bar:baz");
+ Label l3 = Label.parseAbsolute("//foo/bar:quux");
+
+ assertTrue(l1.equals(l1));
+ assertTrue(l2.equals(l1));
+ assertTrue(l1.equals(l2));
+ assertTrue(l2.equals(l1));
+
+ assertFalse(l3.equals(l1));
+ assertFalse(l1.equals(l3));
+
+ assertEquals(l1.hashCode(), l2.hashCode());
+ }
+
+ @Test
+ public void testToString() throws Exception {
+ {
+ String s = "//foo/bar:baz";
+ Label l = Label.parseAbsolute(s);
+ assertEquals(s, l.toString());
+ }
+ {
+ Label l = Label.parseAbsolute("//foo/bar");
+ assertEquals("//foo/bar:bar", l.toString());
+ }
+ }
+
+ @Test
+ public void testDotDot() throws Exception {
+ Label.parseAbsolute("//foo/bar:baz..gif");
+ }
+
+ /**
+ * Asserts that creating a label throws a SyntaxException.
+ * @param label the label to create.
+ */
+ private static void assertSyntaxError(String expectedError, String label) {
+ try {
+ Label.parseAbsolute(label);
+ fail("Label '" + label + "' did not contain a syntax error");
+ } catch (SyntaxException e) {
+ assertContainsRegex(Pattern.quote(expectedError), e.getMessage());
+ }
+ }
+
+ @Test
+ public void testBadCharacters() throws Exception {
+ assertSyntaxError("package names may contain only",
+ "//foo/bar baz");
+ assertSyntaxError("target names may not contain ':'",
+ "//foo:bar:baz");
+ assertSyntaxError("target names may not contain ':'",
+ "//foo:bar:");
+ assertSyntaxError("target names may not contain ':'",
+ "//foo/bar::");
+ assertSyntaxError("target names may not contain '&'",
+ "//foo:bar&");
+ assertSyntaxError("target names may not contain '$'",
+ "//foo/bar:baz$a");
+ assertSyntaxError("target names may not contain '('",
+ "//foo/bar:baz(foo)");
+ assertSyntaxError("target names may not contain ')'",
+ "//foo/bar:bazfoo)");
+ }
+
+ @Test
+ public void testUplevelReferences() throws Exception {
+ assertSyntaxError(BAD_PACKAGE_CHARS,
+ "//foo/bar/..:baz");
+ assertSyntaxError(BAD_PACKAGE_CHARS,
+ "//foo/../baz:baz");
+ assertSyntaxError(BAD_PACKAGE_CHARS,
+ "//../bar/baz:baz");
+ assertSyntaxError(BAD_PACKAGE_CHARS,
+ "//..:foo");
+ assertSyntaxError(TARGET_UPLEVEL,
+ "//foo:bar/../baz");
+ assertSyntaxError(TARGET_UPLEVEL,
+ "//foo:../bar/baz");
+ assertSyntaxError(TARGET_UPLEVEL,
+ "//foo:bar/baz/..");
+ assertSyntaxError(TARGET_UPLEVEL,
+ "//foo:..");
+ }
+
+ @Test
+ public void testDotAsAPathSegment() throws Exception {
+ assertSyntaxError("package names may contain only A-Z, a-z, 0-9, '/', '-' and '_'",
+ "//foo/bar/.:baz");
+ assertSyntaxError(BAD_PACKAGE_CHARS,
+ "//foo/./baz:baz");
+ assertSyntaxError(BAD_PACKAGE_CHARS,
+ "//./bar/baz:baz");
+ assertSyntaxError(BAD_PACKAGE_CHARS,
+ "//.:foo");
+ assertSyntaxError("target names may not contain '.' as a path segment",
+ "//foo:bar/./baz");
+ assertSyntaxError("target names may not contain '.' as a path segment",
+ "//foo:./bar/baz");
+ // TODO(bazel-team): enable when we have removed the "Workaround" in Label
+ // that rewrites broken Labels by removing the trailing '.'
+ //assertSyntaxError(TARGET_UPLEVEL,
+ // "//foo:bar/baz/.");
+ //assertSyntaxError(TARGET_UPLEVEL,
+ // "//foo:.");
+ }
+
+ @Test
+ public void testTrailingDotSegment() throws Exception {
+ assertEquals(Label.parseAbsolute("//foo:dir/."), Label.parseAbsolute("//foo:dir"));
+ }
+
+ @Test
+ public void testSomeOtherBadLabels() throws Exception {
+ assertSyntaxError("package names may not end with '/'",
+ "//foo/:bar");
+ assertSyntaxError("empty package name", "//:foo");
+ assertSyntaxError("package names may not start with '/'", "///p:foo");
+ assertSyntaxError("package names may not contain '//' path separators",
+ "//a//b:foo");
+ }
+
+ @Test
+ public void testSomeGoodLabels() throws Exception {
+ Label.parseAbsolute("//foo:..bar");
+ Label.parseAbsolute("//Foo:..bar");
+ Label.parseAbsolute("//-Foo:..bar");
+ Label.parseAbsolute("//00:..bar");
+ Label.parseAbsolute("//package:foo+bar");
+ Label.parseAbsolute("//package:foo_bar");
+ Label.parseAbsolute("//package:foo=bar");
+ Label.parseAbsolute("//package:foo-bar");
+ Label.parseAbsolute("//package:foo.bar");
+ Label.parseAbsolute("//package:foo@bar");
+ Label.parseAbsolute("//package:foo~bar");
+ }
+
+ /**
+ * Regression test: we previously expanded the set of characters which are considered label chars
+ * to include "@" (see test above). An unexpected side-effect is that "@D" in genrule(cmd) was
+ * considered to be a valid relative label! The fix is to forbid "@x" in package names.
+ */
+ @Test
+ public void testAtVersionIsIllegal() throws Exception {
+ assertSyntaxError(BAD_PACKAGE_CHARS, "//foo/bar@123:baz");
+ }
+
+ @Test
+ public void testDoubleSlashPathSeparator() throws Exception {
+ assertSyntaxError("package names may not contain '//' path separators",
+ "//foo//bar:baz");
+ assertSyntaxError("target names may not contain '//' path separator",
+ "//foo:bar//baz");
+ }
+
+ @Test
+ public void testNonPrintableCharacters() throws Exception {
+ assertSyntaxError(
+ "target names may not contain non-printable characters: '\\x02'",
+ "//foo:..\002bar");
+ }
+
+ /** Make sure that control characters - such as CR - are escaped on output. */
+ @Test
+ public void testInvalidLineEndings() throws Exception {
+ assertSyntaxError("invalid target name '..bar\\r': "
+ + "target names may not end with carriage returns", "//foo:..bar\r");
+ }
+
+ @Test
+ public void testEmptyName() throws Exception {
+ assertSyntaxError("invalid target name '': empty target name", "//foo/bar:");
+ }
+
+ @Test
+ public void testSerializationSimple() throws Exception {
+ checkSerialization("//a", 91);
+ }
+
+ @Test
+ public void testSerializationNested() throws Exception {
+ checkSerialization("//foo/bar:baz", 99);
+ }
+
+ @Test
+ public void testSerializationWithoutTargetName() throws Exception {
+ checkSerialization("//foo/bar", 99);
+ }
+
+ private void checkSerialization(String labelString, int expectedSize) throws Exception {
+ Label a = Label.parseAbsolute(labelString);
+ byte[] sa = TestUtils.serializeObject(a);
+ assertEquals(expectedSize, sa.length);
+
+ Label a2 = (Label) TestUtils.deserializeObject(sa);
+ assertEquals(a, a2);
+ }
+
+ @Test
+ public void testRepoLabel() throws Exception {
+ Label label = Label.parseRepositoryLabel("@foo//bar/baz:bat/boo");
+ assertEquals("@foo//bar/baz:bat/boo", label.toString());
+ }
+
+ @Test
+ public void testNoRepo() throws Exception {
+ Label label = Label.parseRepositoryLabel("//bar/baz:bat/boo");
+ assertEquals("//bar/baz:bat/boo", label.toString());
+ }
+
+ @Test
+ public void testInvalidRepo() throws Exception {
+ try {
+ Label.parseRepositoryLabel("foo//bar/baz:bat/boo");
+ fail();
+ } catch (SyntaxException e) {
+ assertEquals("invalid repository name 'foo': workspace name must start with '@'",
+ e.getMessage());
+ }
+ }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/syntax/LexerTest.java b/src/test/java/com/google/devtools/build/lib/syntax/LexerTest.java
new file mode 100644
index 0000000000..fd5385c31d
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/syntax/LexerTest.java
@@ -0,0 +1,399 @@
+// Copyright 2006 Google Inc. 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 com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.events.EventKind;
+import com.google.devtools.build.lib.events.Location;
+import com.google.devtools.build.lib.events.Reporter;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.util.FsApparatus;
+
+import junit.framework.TestCase;
+
+/**
+ * Tests of tokenization behavior of the {@link Lexer}.
+ */
+public class LexerTest extends TestCase implements EventHandler {
+
+ private FsApparatus scratch = FsApparatus.newInMemory();
+
+ /**
+ * Create a lexer which takes input from the specified string. Resets the
+ * error handler beforehand.
+ */
+ private Lexer createLexer(String input) {
+ Path somePath = scratch.path("/some/path.txt");
+ ParserInputSource inputSource = ParserInputSource.create(input, somePath);
+ Reporter reporter = new Reporter();
+ reporter.addHandler(this);
+ return new Lexer(inputSource, reporter);
+ }
+
+ public Token[] tokens(String input) {
+ return createLexer(input).getTokens().toArray(new Token[0]);
+ }
+
+ /**
+ * Lexes the specified input string, and returns a string containing just the
+ * linenumbers of each token.
+ */
+ private String linenums(String input) {
+ Lexer lexer = createLexer(input);
+ StringBuilder buf = new StringBuilder();
+ for (Token tok : lexer.getTokens()) {
+ if (buf.length() > 0) {
+ buf.append(' ');
+ }
+ int line =
+ lexer.createLocation(tok.left, tok.left).getStartLineAndColumn().getLine();
+ buf.append(line);
+ }
+ return buf.toString();
+ }
+
+ private String lastError;
+
+ private Location lastErrorLocation;
+
+ @Override
+ public void handle(Event event) {
+ if (EventKind.ERRORS.contains(event.getKind())) {
+ lastErrorLocation = event.getLocation();
+ lastError = lastErrorLocation.getPath() + ":"
+ + event.getLocation().getStartLineAndColumn().getLine() + ": "
+ + event.getMessage();
+ }
+ }
+
+ /**
+ * Returns a string containing the names of the tokens and their associated
+ * values. (String-literals are printed without escaping.)
+ */
+ private static String values(Token[] tokens) {
+ StringBuilder buffer = new StringBuilder();
+ for (Token token : tokens) {
+ if (buffer.length() > 0) {
+ buffer.append(' ');
+ }
+ buffer.append(token.kind.name());
+ if (token.value != null) {
+ buffer.append('(').append(token.value).append(')');
+ }
+ }
+ return buffer.toString();
+ }
+
+ /**
+ * Returns a string containing just the names of the tokens.
+ */
+ private static String names(Token[] tokens) {
+ StringBuilder buf = new StringBuilder();
+ for (Token tok : tokens) {
+ if (buf.length() > 0) {
+ buf.append(' ');
+ }
+ buf.append(tok.kind.name());
+ }
+ return buf.toString();
+ }
+
+ /**
+ * Returns a string containing just the half-open position intervals of each
+ * token. e.g. "[3,4) [4,9)".
+ */
+ private static String positions(Token[] tokens) {
+ StringBuilder buf = new StringBuilder();
+ for (Token tok : tokens) {
+ if (buf.length() > 0) {
+ buf.append(' ');
+ }
+ buf.append('[')
+ .append(tok.left)
+ .append(',')
+ .append(tok.right)
+ .append(')');
+ }
+ return buf.toString();
+ }
+
+ public void testBasics1() throws Exception {
+ assertEquals("IDENTIFIER RPAREN NEWLINE EOF", names(tokens("wiz) ")));
+ assertEquals("IDENTIFIER RPAREN NEWLINE EOF", names(tokens("wiz )")));
+ assertEquals("IDENTIFIER RPAREN NEWLINE EOF", names(tokens(" wiz)")));
+ assertEquals("IDENTIFIER RPAREN NEWLINE EOF", names(tokens(" wiz ) ")));
+ assertEquals("IDENTIFIER RPAREN NEWLINE EOF", names(tokens("wiz\t)")));
+ }
+
+ public void testBasics2() throws Exception {
+ assertEquals("RPAREN NEWLINE EOF", names(tokens(")")));
+ assertEquals("RPAREN NEWLINE EOF", names(tokens(" )")));
+ assertEquals("RPAREN NEWLINE EOF", names(tokens(" ) ")));
+ assertEquals("RPAREN NEWLINE EOF", names(tokens(") ")));
+ }
+
+ public void testBasics3() throws Exception {
+ assertEquals("INT COMMENT NEWLINE INT NEWLINE EOF", names(tokens("123#456\n789")));
+ assertEquals("INT COMMENT NEWLINE INT NEWLINE EOF", names(tokens("123 #456\n789")));
+ assertEquals("INT COMMENT NEWLINE INT NEWLINE EOF", names(tokens("123#456 \n789")));
+ assertEquals("INT COMMENT NEWLINE INDENT INT NEWLINE OUTDENT NEWLINE EOF",
+ names(tokens("123#456\n 789")));
+ assertEquals("INT COMMENT NEWLINE INT NEWLINE EOF", names(tokens("123#456\n789 ")));
+ }
+
+ public void testBasics4() throws Exception {
+ assertEquals("NEWLINE EOF", names(tokens("")));
+ assertEquals("COMMENT NEWLINE EOF", names(tokens("# foo")));
+ assertEquals("INT INT INT INT NEWLINE EOF", names(tokens("1 2 3 4")));
+ assertEquals("INT DOT INT NEWLINE EOF", names(tokens("1.234")));
+ assertEquals("IDENTIFIER LPAREN IDENTIFIER COMMA IDENTIFIER RPAREN "
+ + "NEWLINE EOF", names(tokens("foo(bar, wiz)")));
+ }
+
+ public void testIntegers() throws Exception {
+ // Detection of MINUS immediately following integer constant proves we
+ // don't consume too many chars.
+
+ // decimal
+ assertEquals("INT(12345) MINUS NEWLINE EOF", values(tokens("12345-")));
+
+ // octal
+ assertEquals("INT(5349) MINUS NEWLINE EOF", values(tokens("012345-")));
+
+ // octal (bad)
+ assertEquals("INT(0) MINUS NEWLINE EOF", values(tokens("012349-")));
+ assertEquals("/some/path.txt:1: invalid base-8 integer constant: 012349",
+ lastError.toString());
+
+ // hexadecimal (uppercase)
+ assertEquals("INT(1193055) MINUS NEWLINE EOF", values(tokens("0X12345F-")));
+
+ // hexadecimal (lowercase)
+ assertEquals("INT(1193055) MINUS NEWLINE EOF", values(tokens("0x12345f-")));
+
+ // hexadecimal (lowercase) [note: "g" cause termination of token]
+ assertEquals("INT(74565) IDENTIFIER(g) MINUS NEWLINE EOF",
+ values(tokens("0x12345g-")));
+ }
+
+ public void testIntegersAndDot() throws Exception {
+ assertEquals("INT(1) DOT INT(2345) NEWLINE EOF", values(tokens("1.2345")));
+
+ assertEquals("INT(1) DOT INT(2) DOT INT(345) NEWLINE EOF",
+ values(tokens("1.2.345")));
+
+ assertEquals("INT(1) DOT INT(0) NEWLINE EOF", values(tokens("1.23E10")));
+ assertEquals("/some/path.txt:1: invalid base-10 integer constant: 23E10",
+ lastError.toString());
+
+ assertEquals("INT(1) DOT INT(0) MINUS INT(10) NEWLINE EOF",
+ values(tokens("1.23E-10")));
+ assertEquals("/some/path.txt:1: invalid base-10 integer constant: 23E",
+ lastError.toString());
+
+ assertEquals("DOT INT(123) NEWLINE EOF", values(tokens(". 123")));
+ assertEquals("DOT INT(123) NEWLINE EOF", values(tokens(".123")));
+ assertEquals("DOT IDENTIFIER(abc) NEWLINE EOF", values(tokens(".abc")));
+
+ assertEquals("IDENTIFIER(foo) DOT INT(123) NEWLINE EOF",
+ values(tokens("foo.123")));
+ assertEquals("IDENTIFIER(foo) DOT IDENTIFIER(bcd) NEWLINE EOF",
+ values(tokens("foo.bcd"))); // 'b' are hex chars
+ assertEquals("IDENTIFIER(foo) DOT IDENTIFIER(xyz) NEWLINE EOF",
+ values(tokens("foo.xyz")));
+ }
+
+ public void testStringDelimiters() throws Exception {
+ assertEquals("STRING(foo) NEWLINE EOF", values(tokens("\"foo\"")));
+ assertEquals("STRING(foo) NEWLINE EOF", values(tokens("'foo'")));
+ }
+
+ public void testQuotesInStrings() throws Exception {
+ assertEquals("STRING(foo'bar) NEWLINE EOF", values(tokens("'foo\\'bar'")));
+ assertEquals("STRING(foo'bar) NEWLINE EOF", values(tokens("\"foo'bar\"")));
+ assertEquals("STRING(foo\"bar) NEWLINE EOF", values(tokens("'foo\"bar'")));
+ assertEquals("STRING(foo\"bar) NEWLINE EOF",
+ values(tokens("\"foo\\\"bar\"")));
+ }
+
+ public void testStringEscapes() throws Exception {
+ assertEquals("STRING(a\tb\nc\rd) NEWLINE EOF",
+ values(tokens("'a\\tb\\nc\\rd'"))); // \t \r \n
+ assertEquals("STRING(x\\hx) NEWLINE EOF",
+ values(tokens("'x\\hx'"))); // \h is unknown => "\h"
+ assertEquals("STRING(\\$$) NEWLINE EOF", values(tokens("'\\$$'")));
+ assertEquals("STRING(ab) NEWLINE EOF",
+ values(tokens("'a\\\nb'"))); // escape end of line
+
+ assertEquals("STRING(abcd) NEWLINE EOF",
+ values(tokens("r'abcd'")));
+ assertEquals("STRING(abcd) NEWLINE EOF",
+ values(tokens("r\"abcd\"")));
+ assertEquals("STRING(a\\tb\\nc\\rd) NEWLINE EOF",
+ values(tokens("r'a\\tb\\nc\\rd'"))); // r'a\tb\nc\rd'
+ assertEquals("STRING(a\\\") NEWLINE EOF",
+ values(tokens("r\"a\\\"\""))); // r"a\""
+ assertEquals("STRING(a\\\\b) NEWLINE EOF",
+ values(tokens("r'a\\\\b'"))); // r'a\\b'
+ assertEquals("STRING(ab) IDENTIFIER(r) NEWLINE EOF",
+ values(tokens("r'ab'r")));
+
+ assertEquals("STRING(abcd) NEWLINE EOF",
+ values(tokens("\"ab\\ucd\"")));
+ assertEquals("/some/path.txt:1: escape sequence not implemented: \\u",
+ lastError.toString());
+ }
+
+ public void testOctalEscapes() throws Exception {
+ // Regression test for a bug.
+ assertEquals("STRING(\0 \1 \t \u003f I I1 \u00ff \u00ff \u00fe) NEWLINE EOF",
+ values(tokens("'\\0 \\1 \\11 \\77 \\111 \\1111 \\377 \\777 \\776'")));
+ // Test boundaries (non-octal char, EOF).
+ assertEquals("STRING(\1b \1) NEWLINE EOF", values(tokens("'\\1b \\1'")));
+ }
+
+ public void testTripleQuotedStrings() throws Exception {
+ assertEquals("STRING(a\"b'c \n d\"\"e) NEWLINE EOF",
+ values(tokens("\"\"\"a\"b'c \n d\"\"e\"\"\"")));
+ assertEquals("STRING(a\"b'c \n d\"\"e) NEWLINE EOF",
+ values(tokens("'''a\"b'c \n d\"\"e'''")));
+ }
+
+ public void testBadChar() throws Exception {
+ assertEquals("IDENTIFIER(a) IDENTIFIER(b) NEWLINE EOF",
+ values(tokens("a$b")));
+ assertEquals("/some/path.txt:1: invalid character: '$'",
+ lastError.toString());
+ }
+
+ public void testIndentation() throws Exception {
+ assertEquals("INT(1) NEWLINE INT(2) NEWLINE INT(3) NEWLINE EOF",
+ values(tokens("1\n2\n3")));
+ assertEquals("INT(1) NEWLINE INDENT INT(2) NEWLINE INT(3) NEWLINE OUTDENT "
+ + "INT(4) NEWLINE EOF", values(tokens("1\n 2\n 3\n4 ")));
+ assertEquals("INT(1) NEWLINE INDENT INT(2) NEWLINE INT(3) NEWLINE OUTDENT "
+ + "NEWLINE EOF", values(tokens("1\n 2\n 3")));
+ assertEquals("INT(1) NEWLINE INDENT INT(2) NEWLINE INDENT INT(3) NEWLINE "
+ + "OUTDENT OUTDENT NEWLINE EOF",
+ values(tokens("1\n 2\n 3")));
+ assertEquals("INT(1) NEWLINE INDENT INT(2) NEWLINE INDENT INT(3) NEWLINE "
+ + "OUTDENT INT(4) NEWLINE OUTDENT INT(5) NEWLINE EOF",
+ values(tokens("1\n 2\n 3\n 4\n5")));
+
+ assertEquals("INT(1) NEWLINE INDENT INT(2) NEWLINE INDENT INT(3) NEWLINE "
+ + "OUTDENT INT(4) NEWLINE OUTDENT INT(5) NEWLINE EOF",
+ values(tokens("1\n 2\n 3\n 4\n5")));
+ assertEquals("/some/path.txt:4: indentation error", lastError.toString());
+ }
+
+ public void testIndentationInsideParens() throws Exception {
+ // Indentation is ignored inside parens:
+ assertEquals("INT(1) LPAREN INT(2) INT(3) INT(4) INT(5) NEWLINE EOF",
+ values(tokens("1 (\n 2\n 3\n 4\n5")));
+ assertEquals("INT(1) LBRACE INT(2) INT(3) INT(4) INT(5) NEWLINE EOF",
+ values(tokens("1 {\n 2\n 3\n 4\n5")));
+ assertEquals("INT(1) LBRACKET INT(2) INT(3) INT(4) INT(5) NEWLINE EOF",
+ values(tokens("1 [\n 2\n 3\n 4\n5")));
+ assertEquals("INT(1) LBRACKET INT(2) RBRACKET NEWLINE INDENT INT(3) "
+ + "NEWLINE INT(4) NEWLINE OUTDENT INT(5) NEWLINE EOF",
+ values(tokens("1 [\n 2]\n 3\n 4\n5")));
+ }
+
+ public void testIndentationAtEOF() throws Exception {
+ // Matching OUTDENTS are created at EOF:
+ assertEquals("INDENT INT(1) NEWLINE OUTDENT NEWLINE EOF",
+ values(tokens("\n 1")));
+ }
+
+ public void testBlankLineIndentation() throws Exception {
+ // Blank lines and comment lines should not generate any newlines indents
+ // (but note that every input ends with NEWLINE EOF).
+ assertEquals("COMMENT NEWLINE EOF", names(tokens("\n #\n")));
+ assertEquals("COMMENT NEWLINE EOF", names(tokens(" #")));
+ assertEquals("COMMENT NEWLINE EOF", names(tokens(" #\n")));
+ assertEquals("COMMENT NEWLINE EOF", names(tokens(" #comment\n")));
+ assertEquals("DEF IDENTIFIER LPAREN IDENTIFIER RPAREN COLON NEWLINE "
+ + "COMMENT INDENT RETURN IDENTIFIER NEWLINE "
+ + "OUTDENT NEWLINE EOF",
+ names(tokens("def f(x):\n"
+ + " # comment\n"
+ + "\n"
+ + " \n"
+ + " return x\n")));
+ }
+
+ public void testMultipleCommentLines() throws Exception {
+ assertEquals("COMMENT NEWLINE COMMENT COMMENT COMMENT "
+ + "DEF IDENTIFIER LPAREN IDENTIFIER RPAREN COLON NEWLINE "
+ + "INDENT RETURN IDENTIFIER NEWLINE OUTDENT NEWLINE EOF",
+ names(tokens("# Copyright\n"
+ + "#\n"
+ + "# A comment line\n"
+ + "# An adjoining line\n"
+ + "def f(x):\n"
+ + " return x\n")));
+ }
+
+ public void testBackslash() throws Exception {
+ assertEquals("IDENTIFIER IDENTIFIER NEWLINE EOF",
+ names(tokens("a\\\nb")));
+ assertEquals("IDENTIFIER ILLEGAL IDENTIFIER NEWLINE EOF",
+ names(tokens("a\\ b")));
+ assertEquals("IDENTIFIER LPAREN INT RPAREN NEWLINE EOF",
+ names(tokens("a(\\\n2)")));
+ }
+
+ public void testTokenPositions() throws Exception {
+ // foo ( bar , { 1 :
+ assertEquals("[0,3) [3,4) [4,7) [7,8) [9,10) [10,11) [11,12)"
+ // 'quux' } ) NEWLINE EOF
+ + " [13,19) [19,20) [20,21) [20,21) [21,21)",
+ positions(tokens("foo(bar, {1: 'quux'})")));
+ }
+
+ public void testLineNumbers() throws Exception {
+ assertEquals("1 1 1 1 2 2 2 2 4 4 4 4 4",
+ linenums("foo = 1\nbar = 2\n\nwiz = 3"));
+
+ assertEquals("IDENTIFIER(foo) EQUALS INT(1) NEWLINE "
+ + "IDENTIFIER(bar) EQUALS INT(2) NEWLINE "
+ + "IDENTIFIER(wiz) EQUALS NEWLINE "
+ + "IDENTIFIER(bar) EQUALS INT(2) NEWLINE EOF",
+ values(tokens("foo = 1\nbar = 2\n\nwiz = $\nbar = 2")));
+ assertEquals("/some/path.txt:4: invalid character: '$'",
+ lastError.toString());
+
+ // '\\n' in string should not increment linenum:
+ String s = "1\n'foo\\nbar'\3";
+ assertEquals("INT(1) NEWLINE STRING(foo\nbar) NEWLINE EOF",
+ values(tokens(s)));
+ assertEquals("1 1 2 2 2", linenums(s));
+ }
+
+ public void testContainsErrors() throws Exception {
+ Lexer lexerSuccess = createLexer("foo");
+ assertFalse(lexerSuccess.containsErrors());
+
+ Lexer lexerFail = createLexer("f$o");
+ assertTrue(lexerFail.containsErrors());
+
+ String s = "'unterminated";
+ lexerFail = createLexer(s);
+ assertTrue(lexerFail.containsErrors());
+ assertEquals(0, lastErrorLocation.getStartOffset());
+ assertEquals(s.length(), lastErrorLocation.getEndOffset());
+ assertEquals("STRING(unterminated) NEWLINE EOF", values(tokens(s)));
+ }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/syntax/LineNumberTableTest.java b/src/test/java/com/google/devtools/build/lib/syntax/LineNumberTableTest.java
new file mode 100644
index 0000000000..f1fdd77f90
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/syntax/LineNumberTableTest.java
@@ -0,0 +1,113 @@
+// Copyright 2006 Google Inc. 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 com.google.devtools.build.lib.events.Location.LineAndColumn;
+import com.google.devtools.build.lib.util.Pair;
+import com.google.devtools.build.lib.vfs.util.FsApparatus;
+
+import junit.framework.TestCase;
+
+/**
+ * Tests for {@link LineNumberTable}.
+ */
+public class LineNumberTableTest extends TestCase {
+ private FsApparatus scratch = FsApparatus.newInMemory();
+
+ private LineNumberTable create(String buffer) {
+ return LineNumberTable.create(buffer.toCharArray(),
+ scratch.path("/fake/file"));
+ }
+
+ public void testEmpty() {
+ LineNumberTable table = create("");
+ assertEquals(new LineAndColumn(1, 1), table.getLineAndColumn(0));
+ }
+
+ public void testNewline() {
+ LineNumberTable table = create("\n");
+ assertEquals(new LineAndColumn(1, 1), table.getLineAndColumn(0));
+ assertEquals(new LineAndColumn(2, 1), table.getLineAndColumn(1));
+ }
+
+ public void testOneLiner() {
+ LineNumberTable table = create("foo");
+ assertEquals(new LineAndColumn(1, 1), table.getLineAndColumn(0));
+ assertEquals(new LineAndColumn(1, 2), table.getLineAndColumn(1));
+ assertEquals(new LineAndColumn(1, 3), table.getLineAndColumn(2));
+ assertEquals(Pair.of(0, 3), table.getOffsetsForLine(1));
+ }
+
+ public void testMultiLiner() {
+ LineNumberTable table = create("\ntwo\nthree\n\nfive\n");
+
+ // \n
+ assertEquals(new LineAndColumn(1, 1), table.getLineAndColumn(0));
+ assertEquals(Pair.of(0, 1), table.getOffsetsForLine(1));
+
+ // two\n
+ assertEquals(new LineAndColumn(2, 1), table.getLineAndColumn(1));
+ assertEquals(new LineAndColumn(2, 2), table.getLineAndColumn(2));
+ assertEquals(new LineAndColumn(2, 3), table.getLineAndColumn(3));
+ assertEquals(new LineAndColumn(2, 4), table.getLineAndColumn(4));
+ assertEquals(Pair.of(1, 5), table.getOffsetsForLine(2));
+
+ // three\n
+ assertEquals(new LineAndColumn(3, 1), table.getLineAndColumn(5));
+ assertEquals(new LineAndColumn(3, 6), table.getLineAndColumn(10));
+ assertEquals(Pair.of(5, 11), table.getOffsetsForLine(3));
+
+ // \n
+ assertEquals(new LineAndColumn(4, 1), table.getLineAndColumn(11));
+ assertEquals(Pair.of(11, 12), table.getOffsetsForLine(4));
+
+ // five\n
+ assertEquals(new LineAndColumn(5, 1), table.getLineAndColumn(12));
+ assertEquals(new LineAndColumn(5, 5), table.getLineAndColumn(16));
+ assertEquals(Pair.of(12, 17), table.getOffsetsForLine(5));
+ }
+
+ public void testHashLine() {
+ String data = "#\n"
+ + "#line 67 \"/foo\"\n"
+ + "cc_binary(name='a',\n"
+ + " srcs=[])\n"
+ + "#line 23 \"/ba.r\"\n"
+ + "vardef(x,y)\n";
+
+ LineNumberTable table = create(data);
+
+ // Note: no attempt is made to return accurate column information.
+ assertEquals(new LineAndColumn(67, 1), table.getLineAndColumn(data.indexOf("cc_binary")));
+ assertEquals(new LineAndColumn(67, 1), table.getLineAndColumn(data.indexOf("name='a'")));
+ assertEquals("/fake/file", table.getPath(0).toString());
+ // Note: newlines ignored; "srcs" is still (intentionally) considered to be
+ // on L67. Consider the alternative, and assume that rule 'a' is 50 lines
+ // when pretty-printed: the last line of 'a' would be reported as line 67 +
+ // 50, which may be in a part of the original BUILD file that has nothing
+ // to do with this rule. In other words, the size of rules before and
+ // after pretty printing are essentially unrelated.
+ assertEquals(new LineAndColumn(67, 1), table.getLineAndColumn(data.indexOf("srcs")));
+ assertEquals("/foo", table.getPath(data.indexOf("srcs")).toString());
+ assertEquals(Pair.of(2, 57), table.getOffsetsForLine(67));
+
+ assertEquals(new LineAndColumn(23, 1), table.getLineAndColumn(data.indexOf("vardef")));
+ assertEquals(new LineAndColumn(23, 1), table.getLineAndColumn(data.indexOf("x,y")));
+ assertEquals("/ba.r", table.getPath(data.indexOf("x,y")).toString());
+ assertEquals(Pair.of(57, 86), table.getOffsetsForLine(23));
+
+ assertEquals(Pair.of(0, 0), table.getOffsetsForLine(42));
+ }
+
+}
diff --git a/src/test/java/com/google/devtools/build/lib/syntax/MixedModeFunctionTest.java b/src/test/java/com/google/devtools/build/lib/syntax/MixedModeFunctionTest.java
new file mode 100644
index 0000000000..c7c2c40134
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/syntax/MixedModeFunctionTest.java
@@ -0,0 +1,130 @@
+// Copyright 2006 Google Inc. 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 com.google.common.collect.ImmutableList;
+
+import java.util.Arrays;
+
+/**
+ * Tests for {@link MixedModeFunction}.
+ */
+public class MixedModeFunctionTest extends AbstractEvaluationTestCase {
+
+ private Environment singletonEnv(String id, Object value) {
+ Environment env = new Environment();
+ env.update(id, value);
+ return env;
+ }
+
+ /**
+ * Handy implementation of {@link MixedModeFunction} that just tuples up its args and returns
+ * them.
+ */
+ private static class TestingMixedModeFunction extends MixedModeFunction {
+ TestingMixedModeFunction(Iterable<String> parameters,
+ int numMandatoryParameters,
+ boolean onlyNamedArguments) {
+ super("mixed", parameters, numMandatoryParameters, onlyNamedArguments);
+ }
+ @Override
+ public Object call(Object[] namedParameters, FuncallExpression ast) {
+ return Arrays.asList(namedParameters);
+ }
+ }
+
+ private void checkMixedMode(Function func,
+ String callExpression,
+ String expectedOutput) throws Exception {
+ Environment env = singletonEnv(func.getName(), func);
+
+ if (expectedOutput.charAt(0) == '[') { // a tuple => expected to pass
+ assertEquals(expectedOutput,
+ eval(callExpression, env).toString());
+ } else { // expected to fail with an exception
+ try {
+ eval(callExpression, env);
+ fail();
+ } catch (EvalException e) {
+ assertEquals(expectedOutput, e.getMessage());
+ }
+ }
+ }
+
+ private static final String[] mixedModeExpressions = {
+ "mixed()",
+ "mixed(1)",
+ "mixed(1, 2)",
+ "mixed(1, 2, 3)",
+ "mixed(1, 2, wiz=3, quux=4)",
+ "mixed(foo=1)",
+ "mixed(foo=1, bar=2)",
+ "mixed(bar=2, foo=1)",
+ "mixed(2, foo=1)",
+ "mixed(bar=2, foo=1, wiz=3)",
+ };
+
+ public void checkMixedModeFunctions(boolean onlyNamedArguments,
+ String expectedSignature,
+ String[] expectedResults)
+ throws Exception {
+ MixedModeFunction func =
+ new TestingMixedModeFunction(ImmutableList.of("foo", "bar"), 1, onlyNamedArguments);
+
+ assertEquals(expectedSignature, func.getSignature());
+
+ for (int ii = 0; ii < mixedModeExpressions.length; ++ii) {
+ String expr = mixedModeExpressions[ii];
+ String expected = expectedResults[ii];
+ checkMixedMode(func, expr, expected);
+ }
+ }
+
+ public void testNoSurplusArguments() throws Exception {
+ checkMixedModeFunctions(false,
+ "mixed(foo, bar = null)",
+ new String[]
+ {
+ "mixed(foo, bar = null) received insufficient arguments",
+ "[1, null]",
+ "[1, 2]",
+ "too many positional arguments in call to mixed(foo, bar = null)",
+ "unexpected keywords 'quux', 'wiz' in call to mixed(foo, bar = null)",
+ "[1, null]",
+ "[1, 2]",
+ "[1, 2]",
+ "mixed(foo, bar = null) got multiple values for keyword"
+ + " argument 'foo'",
+ "unexpected keyword 'wiz' in call to mixed(foo, bar = null)",
+ });
+ }
+
+ public void testOnlyNamedArguments() throws Exception {
+ checkMixedModeFunctions(true,
+ "mixed(foo, bar = null)",
+ new String[]
+ {
+ "mixed(foo, bar = null) received insufficient arguments",
+ "mixed(foo, bar = null) does not accept positional arguments",
+ "mixed(foo, bar = null) does not accept positional arguments",
+ "mixed(foo, bar = null) does not accept positional arguments",
+ "mixed(foo, bar = null) does not accept positional arguments",
+ "[1, null]",
+ "[1, 2]",
+ "[1, 2]",
+ "mixed(foo, bar = null) does not accept positional arguments",
+ "unexpected keyword 'wiz' in call to mixed(foo, bar = null)",
+ });
+ }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/syntax/ParserInputSourceTest.java b/src/test/java/com/google/devtools/build/lib/syntax/ParserInputSourceTest.java
new file mode 100644
index 0000000000..1d0e50c8fc
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/syntax/ParserInputSourceTest.java
@@ -0,0 +1,116 @@
+// Copyright 2006 Google Inc. 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.devtools.build.lib.util.StringUtilities.joinLines;
+
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.util.FsApparatus;
+
+import junit.framework.TestCase;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * A test case for {@link ParserInputSource}.
+ */
+public class ParserInputSourceTest extends TestCase {
+
+ private FsApparatus scratch = FsApparatus.newInMemory();
+
+ public void testCreateFromFile() throws IOException {
+ String content = joinLines("Line 1", "Line 2", "Line 3", "");
+ Path file = scratch.file("/tmp/my/file.txt", content);
+ ParserInputSource input = ParserInputSource.create(file);
+ assertEquals(content, new String(input.getContent()));
+ assertEquals("/tmp/my/file.txt", input.getPath().toString());
+ }
+
+ public void testCreateFromString() {
+ String content = "Content provided as a string.";
+ String pathName = "/the/name/of/the/content.txt";
+ Path path = scratch.path(pathName);
+ ParserInputSource input = ParserInputSource.create(content, path);
+ assertEquals(content, new String(input.getContent()));
+ assertEquals(pathName, input.getPath().toString());
+ }
+
+ public void testCreateFromCharArray() {
+ String content = "Content provided as a string.";
+ String pathName = "/the/name/of/the/content.txt";
+ Path path = scratch.path(pathName);
+ char[] contentChars = content.toCharArray();
+ ParserInputSource input = ParserInputSource.create(contentChars, path);
+ assertEquals(content, new String(input.getContent()));
+ assertEquals(pathName, input.getPath().toString());
+ }
+
+ public void testCreateFromInputStream() throws IOException {
+ String content = "Content provided as a string.";
+ byte[] bytes = content.getBytes("ISO-8859-1");
+ ByteArrayInputStream in = new ByteArrayInputStream(bytes);
+ String pathName = "/the/name/of/the/content.txt";
+ Path path = scratch.path(pathName);
+ ParserInputSource input = ParserInputSource.create(in, path);
+ assertEquals(content, new String(input.getContent()));
+ assertEquals(pathName, input.getPath().toString());
+ }
+
+ public void testIOExceptionIfInputFileDoesNotExistForSingleArgConstructor() {
+ try {
+ Path path = scratch.path("/does/not/exist");
+ ParserInputSource.create(path);
+ fail();
+ } catch (IOException e) {
+ String expected = "/does/not/exist (No such file or directory)";
+ assertEquals(expected, e.getMessage());
+ }
+ }
+
+ public void testWillNotTryToReadInputFileIfContentProvidedAsString() {
+ Path path = scratch.path("/will/not/try/to/read");
+ ParserInputSource.create("Content provided as string.", path);
+ }
+
+ public void testWillNotTryToReadInputFileIfContentProvidedAsChars() {
+ Path path = scratch.path("/will/not/try/to/read");
+ char[] content = "Content provided as char array.".toCharArray();
+ ParserInputSource.create(content, path);
+ }
+
+ public void testWillCloseStreamWhenReadingFromInputStream() {
+ final StringBuilder log = new StringBuilder();
+ InputStream in = new InputStream() {
+ @Override
+ public int read() throws IOException {
+ throw new IOException("Fault injected.");
+ }
+ @Override
+ public void close() {
+ log.append("Stream closed.");
+ }
+ };
+ try {
+ Path path = scratch.path("/will/not/try/to/read");
+ ParserInputSource.create(in, path);
+ fail();
+ } catch (IOException e) {
+ assertEquals("Fault injected.", e.getMessage());
+ }
+ assertEquals("Stream closed.", log.toString());
+ }
+
+}
diff --git a/src/test/java/com/google/devtools/build/lib/syntax/ParserTest.java b/src/test/java/com/google/devtools/build/lib/syntax/ParserTest.java
new file mode 100644
index 0000000000..e698728553
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/syntax/ParserTest.java
@@ -0,0 +1,876 @@
+// Copyright 2014 Google Inc. 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.common.truth.Truth.assertWithMessage;
+
+import com.google.devtools.build.lib.events.Location;
+import com.google.devtools.build.lib.syntax.DictionaryLiteral.DictionaryEntryLiteral;
+
+import java.util.List;
+
+/**
+ * Tests of parser behaviour.
+ *
+ */
+public class ParserTest extends AbstractParserTestCase {
+
+ private static String getText(String text, ASTNode node) {
+ return text.substring(node.getLocation().getStartOffset(),
+ node.getLocation().getEndOffset());
+ }
+
+ // helper func for testListLiterals:
+ private static int getIntElem(DictionaryEntryLiteral entry, boolean key) {
+ return ((IntegerLiteral) (key ? entry.getKey() : entry.getValue())).getValue();
+ }
+
+ // helper func for testListLiterals:
+ private static DictionaryEntryLiteral getElem(DictionaryLiteral list, int index) {
+ return list.getEntries().get(index);
+ }
+
+ // helper func for testListLiterals:
+ private static int getIntElem(ListLiteral list, int index) {
+ return ((IntegerLiteral) list.getElements().get(index)).getValue();
+ }
+
+ // helper func for testListLiterals:
+ private static Expression getElem(ListLiteral list, int index) {
+ return list.getElements().get(index);
+ }
+
+ // helper func for testing arguments:
+ private static Expression getArg(FuncallExpression f, int index) {
+ return f.getArguments().get(index).getValue();
+ }
+
+ public void testPrecedence1() throws Exception {
+ BinaryOperatorExpression e =
+ (BinaryOperatorExpression) parseExpr("'%sx' % 'foo' + 'bar'");
+
+ assertEquals(Operator.PLUS, e.getOperator());
+ }
+
+ public void testPrecedence2() throws Exception {
+ BinaryOperatorExpression e =
+ (BinaryOperatorExpression) parseExpr("('%sx' % 'foo') + 'bar'");
+ assertEquals(Operator.PLUS, e.getOperator());
+ }
+
+ public void testPrecedence3() throws Exception {
+ BinaryOperatorExpression e =
+ (BinaryOperatorExpression) parseExpr("'%sx' % ('foo' + 'bar')");
+ assertEquals(Operator.PERCENT, e.getOperator());
+ }
+
+ public void testPrecedence4() throws Exception {
+ BinaryOperatorExpression e =
+ (BinaryOperatorExpression) parseExpr("1 + - (2 - 3)");
+ assertEquals(Operator.PLUS, e.getOperator());
+ }
+
+ public void testUnaryMinusExpr() throws Exception {
+ FuncallExpression e = (FuncallExpression) parseExpr("-5");
+ FuncallExpression e2 = (FuncallExpression) parseExpr("- 5");
+
+ assertEquals("-", e.getFunction().getName());
+ assertEquals("-", e2.getFunction().getName());
+
+ assertThat(e.getArguments()).hasSize(1);
+ assertEquals(1, e.getNumPositionalArguments());
+
+ IntegerLiteral arg0 = (IntegerLiteral) e.getArguments().get(0).getValue();
+ assertEquals(5, (int) arg0.getValue());
+ }
+
+ public void testFuncallExpr() throws Exception {
+ FuncallExpression e = (FuncallExpression) parseExpr("foo(1, 2, bar=wiz)");
+
+ Ident ident = e.getFunction();
+ assertEquals("foo", ident.getName());
+
+ assertThat(e.getArguments()).hasSize(3);
+ assertEquals(2, e.getNumPositionalArguments());
+
+ IntegerLiteral arg0 = (IntegerLiteral) e.getArguments().get(0).getValue();
+ assertEquals(1, (int) arg0.getValue());
+
+ IntegerLiteral arg1 = (IntegerLiteral) e.getArguments().get(1).getValue();
+ assertEquals(2, (int) arg1.getValue());
+
+ Argument.Passed arg2 = e.getArguments().get(2);
+ assertEquals("bar", arg2.getName());
+ Ident arg2val = (Ident) arg2.getValue();
+ assertEquals("wiz", arg2val.getName());
+ }
+
+ public void testMethCallExpr() throws Exception {
+ FuncallExpression e =
+ (FuncallExpression) parseExpr("foo.foo(1, 2, bar=wiz)");
+
+ Ident ident = e.getFunction();
+ assertEquals("foo", ident.getName());
+
+ assertThat(e.getArguments()).hasSize(3);
+ assertEquals(2, e.getNumPositionalArguments());
+
+ IntegerLiteral arg0 = (IntegerLiteral) e.getArguments().get(0).getValue();
+ assertEquals(1, (int) arg0.getValue());
+
+ IntegerLiteral arg1 = (IntegerLiteral) e.getArguments().get(1).getValue();
+ assertEquals(2, (int) arg1.getValue());
+
+ Argument.Passed arg2 = e.getArguments().get(2);
+ assertEquals("bar", arg2.getName());
+ Ident arg2val = (Ident) arg2.getValue();
+ assertEquals("wiz", arg2val.getName());
+ }
+
+ public void testChainedMethCallExpr() throws Exception {
+ FuncallExpression e =
+ (FuncallExpression) parseExpr("foo.replace().split(1)");
+
+ Ident ident = e.getFunction();
+ assertEquals("split", ident.getName());
+
+ assertThat(e.getArguments()).hasSize(1);
+ assertEquals(1, e.getNumPositionalArguments());
+
+ IntegerLiteral arg0 = (IntegerLiteral) e.getArguments().get(0).getValue();
+ assertEquals(1, (int) arg0.getValue());
+ }
+
+ public void testPropRefExpr() throws Exception {
+ DotExpression e = (DotExpression) parseExpr("foo.foo");
+
+ Ident ident = e.getField();
+ assertEquals("foo", ident.getName());
+ }
+
+ public void testStringMethExpr() throws Exception {
+ FuncallExpression e = (FuncallExpression) parseExpr("'foo'.foo()");
+
+ Ident ident = e.getFunction();
+ assertEquals("foo", ident.getName());
+
+ assertThat(e.getArguments()).isEmpty();
+ }
+
+ public void testStringLiteralOptimizationValue() throws Exception {
+ StringLiteral l = (StringLiteral) parseExpr("'abc' + 'def'");
+ assertEquals("abcdef", l.value);
+ }
+
+ public void testStringLiteralOptimizationToString() throws Exception {
+ StringLiteral l = (StringLiteral) parseExpr("'abc' + 'def'");
+ assertEquals("'abcdef'", l.toString());
+ }
+
+ public void testStringLiteralOptimizationLocation() throws Exception {
+ StringLiteral l = (StringLiteral) parseExpr("'abc' + 'def'");
+ assertEquals(0, l.getLocation().getStartOffset());
+ assertEquals(13, l.getLocation().getEndOffset());
+ }
+
+ public void testStringLiteralOptimizationDifferentQuote() throws Exception {
+ assertThat(parseExpr("'abc' + \"def\"")).isInstanceOf(BinaryOperatorExpression.class);
+ }
+
+ public void testSubstring() throws Exception {
+ FuncallExpression e = (FuncallExpression) parseExpr("'FOO.CC'[:].lower()[1:]");
+ assertEquals("$substring", e.getFunction().getName());
+ assertThat(e.getArguments()).hasSize(2);
+
+ e = (FuncallExpression) parseExpr("'FOO.CC'.lower()[1:].startswith('oo')");
+ assertEquals("startswith", e.getFunction().getName());
+ assertThat(e.getArguments()).hasSize(1);
+
+ e = (FuncallExpression) parseExpr("'FOO.CC'[1:][:2]");
+ assertEquals("$substring", e.getFunction().getName());
+ assertThat(e.getArguments()).hasSize(2);
+ }
+
+ private void assertLocation(int start, int end, Location location)
+ throws Exception {
+ int actualStart = location.getStartOffset();
+ int actualEnd = location.getEndOffset();
+
+ if (actualStart != start || actualEnd != end) {
+ fail("Expected location = [" + start + ", " + end + "), found ["
+ + actualStart + ", " + actualEnd + ")");
+ }
+ }
+
+ public void testErrorRecovery() throws Exception {
+ syntaxEvents.setFailFast(false);
+
+ String expr = "f(1, [x for foo foo foo], 3)";
+ FuncallExpression e = (FuncallExpression) parseExpr(expr);
+
+ syntaxEvents.assertContainsEvent("syntax error at 'foo'");
+
+ // Test that the actual parameters are: (1, $error$, 3):
+
+ Ident ident = e.getFunction();
+ assertEquals("f", ident.getName());
+
+ assertThat(e.getArguments()).hasSize(3);
+ assertEquals(3, e.getNumPositionalArguments());
+
+ IntegerLiteral arg0 = (IntegerLiteral) e.getArguments().get(0).getValue();
+ assertEquals(1, (int) arg0.getValue());
+
+ Argument.Passed arg1 = e.getArguments().get(1);
+ Ident arg1val = ((Ident) arg1.getValue());
+ assertEquals("$error$", arg1val.getName());
+
+ assertLocation(5, 24, arg1val.getLocation());
+ assertEquals("[x for foo foo foo]", expr.substring(5, 24));
+ assertEquals(25, arg1val.getLocation().getEndLineAndColumn().getColumn());
+
+ IntegerLiteral arg2 = (IntegerLiteral) e.getArguments().get(2).getValue();
+ assertEquals(3, (int) arg2.getValue());
+ }
+
+ public void testDoesntGetStuck() throws Exception {
+ syntaxEvents.setFailFast(false);
+
+ // Make sure the parser does not get stuck when trying
+ // to parse an expression containing a syntax error.
+ // This usually results in OutOfMemoryError because the
+ // parser keeps filling up the error log.
+ // We need to make sure that we will always advance
+ // in the token stream.
+ parseExpr("f(1, ], 3)");
+ parseExpr("f(1, ), 3)");
+ parseExpr("[ ) for v in 3)");
+
+ syntaxEvents.assertContainsEvent(""); // "" matches any;
+ // i.e. there were some events
+ }
+
+ public void testSecondaryLocation() {
+ String expr = "f(1 % 2)";
+ FuncallExpression call = (FuncallExpression) parseExpr(expr);
+ Argument.Passed arg = call.getArguments().get(0);
+ assertTrue(arg.getLocation().getEndOffset() < call.getLocation().getEndOffset());
+ }
+
+ public void testPrimaryLocation() {
+ String expr = "f(1 + 2)";
+ FuncallExpression call = (FuncallExpression) parseExpr(expr);
+ Argument.Passed arg = call.getArguments().get(0);
+ assertTrue(arg.getLocation().getEndOffset() < call.getLocation().getEndOffset());
+ }
+
+ public void testAssignLocation() {
+ String expr = "a = b;c = d\n";
+ List<Statement> statements = parseFile(expr);
+ Statement statement = statements.get(0);
+ assertEquals(5, statement.getLocation().getEndOffset());
+ }
+
+ public void testAssign() {
+ String expr = "list[0] = 5; dict['key'] = value\n";
+ List<Statement> statements = parseFile(expr);
+ assertThat(statements).hasSize(2);
+ }
+
+ public void testInvalidAssign() {
+ syntaxEvents.setFailFast(false);
+ parseExpr("1 + (b = c)");
+ syntaxEvents.assertContainsEvent("syntax error");
+ syntaxEvents.collector().clear();
+ }
+
+ public void testAugmentedAssign() throws Exception {
+ assertEquals("[x = x + 1\n]", parseFile("x += 1").toString());
+ }
+
+ public void testPrettyPrintFunctions() throws Exception {
+ assertEquals("[x[1:3]\n]", parseFile("x[1:3]").toString());
+ assertEquals("[str[42]\n]", parseFile("str[42]").toString());
+ assertEquals("[ctx.new_file(['hello'])\n]", parseFile("ctx.new_file('hello')").toString());
+ assertEquals("[new_file(['hello'])\n]", parseFile("new_file('hello')").toString());
+ }
+
+ public void testFuncallLocation() {
+ String expr = "a(b);c = d\n";
+ List<Statement> statements = parseFile(expr);
+ Statement statement = statements.get(0);
+ assertEquals(4, statement.getLocation().getEndOffset());
+ }
+
+ public void testSpecialFuncallLocation() throws Exception {
+ List<Statement> statements = parseFile("-x\n");
+ assertLocation(0, 3, statements.get(0).getLocation());
+
+ statements = parseFile("arr[15]\n");
+ assertLocation(0, 8, statements.get(0).getLocation());
+
+ statements = parseFile("str[1:12]\n");
+ assertLocation(0, 10, statements.get(0).getLocation());
+ }
+
+ public void testListPositions() throws Exception {
+ String expr = "[0,f(1),2]";
+ ListLiteral list = (ListLiteral) parseExpr(expr);
+ assertEquals("[0,f(1),2]", getText(expr, list));
+ assertEquals("0", getText(expr, getElem(list, 0)));
+ assertEquals("f(1)", getText(expr, getElem(list, 1)));
+ assertEquals("2", getText(expr, getElem(list, 2)));
+ }
+
+ public void testDictPositions() throws Exception {
+ String expr = "{1:2,2:f(1),3:4}";
+ DictionaryLiteral list = (DictionaryLiteral) parseExpr(expr);
+ assertEquals("{1:2,2:f(1),3:4}", getText(expr, list));
+ assertEquals("1:2", getText(expr, getElem(list, 0)));
+ assertEquals("2:f(1)", getText(expr, getElem(list, 1)));
+ assertEquals("3:4", getText(expr, getElem(list, 2)));
+ }
+
+ public void testArgumentPositions() throws Exception {
+ String stmt = "f(0,g(1,2),2)";
+ FuncallExpression f = (FuncallExpression) parseExpr(stmt);
+ assertEquals(stmt, getText(stmt, f));
+ assertEquals("0", getText(stmt, getArg(f, 0)));
+ assertEquals("g(1,2)", getText(stmt, getArg(f, 1)));
+ assertEquals("2", getText(stmt, getArg(f, 2)));
+ }
+
+ public void testListLiterals1() throws Exception {
+ ListLiteral list = (ListLiteral) parseExpr("[0,1,2]");
+ assertFalse(list.isTuple());
+ assertThat(list.getElements()).hasSize(3);
+ assertFalse(list.isTuple());
+ for (int i = 0; i < 3; ++i) {
+ assertEquals(i, getIntElem(list, i));
+ }
+ }
+
+ public void testTupleLiterals2() throws Exception {
+ ListLiteral tuple = (ListLiteral) parseExpr("(0,1,2)");
+ assertTrue(tuple.isTuple());
+ assertThat(tuple.getElements()).hasSize(3);
+ assertTrue(tuple.isTuple());
+ for (int i = 0; i < 3; ++i) {
+ assertEquals(i, getIntElem(tuple, i));
+ }
+ }
+
+ public void testTupleLiterals3() throws Exception {
+ ListLiteral emptyTuple = (ListLiteral) parseExpr("()");
+ assertTrue(emptyTuple.isTuple());
+ assertThat(emptyTuple.getElements()).isEmpty();
+ }
+
+ public void testTupleLiterals4() throws Exception {
+ ListLiteral singletonTuple = (ListLiteral) parseExpr("(42,)");
+ assertTrue(singletonTuple.isTuple());
+ assertThat(singletonTuple.getElements()).hasSize(1);
+ assertEquals(42, getIntElem(singletonTuple, 0));
+ }
+
+ public void testTupleLiterals5() throws Exception {
+ IntegerLiteral intLit = (IntegerLiteral) parseExpr("(42)"); // not a tuple!
+ assertEquals(42, (int) intLit.getValue());
+ }
+
+ public void testListLiterals6() throws Exception {
+ ListLiteral emptyList = (ListLiteral) parseExpr("[]");
+ assertFalse(emptyList.isTuple());
+ assertThat(emptyList.getElements()).isEmpty();
+ }
+
+ public void testListLiterals7() throws Exception {
+ ListLiteral singletonList = (ListLiteral) parseExpr("[42,]");
+ assertFalse(singletonList.isTuple());
+ assertThat(singletonList.getElements()).hasSize(1);
+ assertEquals(42, getIntElem(singletonList, 0));
+ }
+
+ public void testListLiterals8() throws Exception {
+ ListLiteral singletonList = (ListLiteral) parseExpr("[42]"); // a singleton
+ assertFalse(singletonList.isTuple());
+ assertThat(singletonList.getElements()).hasSize(1);
+ assertEquals(42, getIntElem(singletonList, 0));
+ }
+
+ public void testDictionaryLiterals() throws Exception {
+ DictionaryLiteral dictionaryList =
+ (DictionaryLiteral) parseExpr("{1:42}"); // a singleton dictionary
+ assertThat(dictionaryList.getEntries()).hasSize(1);
+ DictionaryEntryLiteral tuple = getElem(dictionaryList, 0);
+ assertEquals(1, getIntElem(tuple, true));
+ assertEquals(42, getIntElem(tuple, false));
+ }
+
+ public void testDictionaryLiterals1() throws Exception {
+ DictionaryLiteral dictionaryList =
+ (DictionaryLiteral) parseExpr("{}"); // an empty dictionary
+ assertThat(dictionaryList.getEntries()).isEmpty();
+ }
+
+ public void testDictionaryLiterals2() throws Exception {
+ DictionaryLiteral dictionaryList =
+ (DictionaryLiteral) parseExpr("{1:42,}"); // a singleton dictionary
+ assertThat(dictionaryList.getEntries()).hasSize(1);
+ DictionaryEntryLiteral tuple = getElem(dictionaryList, 0);
+ assertEquals(1, getIntElem(tuple, true));
+ assertEquals(42, getIntElem(tuple, false));
+ }
+
+ public void testDictionaryLiterals3() throws Exception {
+ DictionaryLiteral dictionaryList = (DictionaryLiteral) parseExpr("{1:42,2:43,3:44}");
+ assertThat(dictionaryList.getEntries()).hasSize(3);
+ for (int i = 0; i < 3; i++) {
+ DictionaryEntryLiteral tuple = getElem(dictionaryList, i);
+ assertEquals(i + 1, getIntElem(tuple, true));
+ assertEquals(i + 42, getIntElem(tuple, false));
+ }
+ }
+
+ public void testListLiterals9() throws Exception {
+ ListLiteral singletonList =
+ (ListLiteral) parseExpr("[ abi + opt_level + \'/include\' ]");
+ assertFalse(singletonList.isTuple());
+ assertThat(singletonList.getElements()).hasSize(1);
+ }
+
+ public void testListComprehensionSyntax() throws Exception {
+ syntaxEvents.setFailFast(false);
+
+ parseExpr("[x for");
+ syntaxEvents.assertContainsEvent("syntax error at 'newline'");
+ syntaxEvents.collector().clear();
+
+ parseExpr("[x for x");
+ syntaxEvents.assertContainsEvent("syntax error at 'newline'");
+ syntaxEvents.collector().clear();
+
+ parseExpr("[x for x in");
+ syntaxEvents.assertContainsEvent("syntax error at 'newline'");
+ syntaxEvents.collector().clear();
+
+ parseExpr("[x for x in []");
+ syntaxEvents.assertContainsEvent("syntax error at 'newline'");
+ syntaxEvents.collector().clear();
+
+ parseExpr("[x for x for y in ['a']]");
+ syntaxEvents.assertContainsEvent("syntax error at 'for'");
+ syntaxEvents.collector().clear();
+ }
+
+ public void testListComprehension() throws Exception {
+ ListComprehension list =
+ (ListComprehension) parseExpr(
+ "['foo/%s.java' % x "
+ + "for x in []]");
+ assertThat(list.getLists()).hasSize(1);
+
+ list = (ListComprehension) parseExpr("['foo/%s.java' % x "
+ + "for x in ['bar', 'wiz', 'quux']]");
+ assertThat(list.getLists()).hasSize(1);
+
+ list = (ListComprehension) parseExpr("['%s/%s.java' % (x, y) "
+ + "for x in ['foo', 'bar'] for y in ['baz', 'wiz', 'quux']]");
+ assertThat(list.getLists()).hasSize(2);
+ }
+
+ public void testParserContainsErrorsIfSyntaxException() throws Exception {
+ syntaxEvents.setFailFast(false);
+ parseExpr("'foo' %%");
+ syntaxEvents.assertContainsEvent("syntax error at '%'");
+ }
+
+ public void testParserDoesNotContainErrorsIfSuccess() throws Exception {
+ parseExpr("'foo'");
+ }
+
+ public void testParserContainsErrors() throws Exception {
+ syntaxEvents.setFailFast(false);
+ parseStmt("+");
+ syntaxEvents.assertContainsEvent("syntax error at '+'");
+ }
+
+ public void testSemicolonAndNewline() throws Exception {
+ List<Statement> stmts = parseFile(
+ "foo='bar'; foo(bar)" + '\n'
+ + "" + '\n'
+ + "foo='bar'; foo(bar)"
+ );
+ assertThat(stmts).hasSize(4);
+ }
+
+ public void testSemicolonAndNewline2() throws Exception {
+ syntaxEvents.setFailFast(false);
+ List<Statement> stmts = parseFile(
+ "foo='foo' error(bar)" + '\n'
+ + "" + '\n'
+ );
+ syntaxEvents.assertContainsEvent("syntax error at 'error'");
+ assertThat(stmts).hasSize(2);
+ }
+
+ public void testExprAsStatement() throws Exception {
+ List<Statement> stmts = parseFile(
+ "li = []\n"
+ + "li.append('a.c')\n"
+ + "\"\"\" string comment \"\"\"\n"
+ + "foo(bar)"
+ );
+ assertThat(stmts).hasSize(4);
+ }
+
+ public void testParseBuildFileWithSingeRule() throws Exception {
+ List<Statement> stmts = parseFile(
+ "genrule(name = 'foo'," + '\n'
+ + " srcs = ['input.csv']," + '\n'
+ + " outs = [ 'result.txt'," + '\n'
+ + " 'result.log']," + '\n'
+ + " cmd = 'touch result.txt result.log')" + '\n'
+ );
+ assertThat(stmts).hasSize(1);
+ }
+
+ public void testParseBuildFileWithMultipleRules() throws Exception {
+ List<Statement> stmts = parseFile(
+ "genrule(name = 'foo'," + '\n'
+ + " srcs = ['input.csv']," + '\n'
+ + " outs = [ 'result.txt'," + '\n'
+ + " 'result.log']," + '\n'
+ + " cmd = 'touch result.txt result.log')" + '\n'
+ + "" + '\n'
+ + "genrule(name = 'bar'," + '\n'
+ + " srcs = ['input.csv']," + '\n'
+ + " outs = [ 'graph.svg']," + '\n'
+ + " cmd = 'touch graph.svg')" + '\n'
+ );
+ assertThat(stmts).hasSize(2);
+ }
+
+ public void testParseBuildFileWithComments() throws Exception {
+ Parser.ParseResult result = parseFileWithComments(
+ "# Test BUILD file" + '\n'
+ + "# with multi-line comment" + '\n'
+ + "" + '\n'
+ + "genrule(name = 'foo'," + '\n'
+ + " srcs = ['input.csv']," + '\n'
+ + " outs = [ 'result.txt'," + '\n'
+ + " 'result.log']," + '\n'
+ + " cmd = 'touch result.txt result.log')" + '\n'
+ );
+ assertThat(result.statements).hasSize(1);
+ assertThat(result.comments).hasSize(2);
+ }
+
+ public void testParseBuildFileWithManyComments() throws Exception {
+ Parser.ParseResult result = parseFileWithComments(
+ "# 1" + '\n'
+ + "# 2" + '\n'
+ + "" + '\n'
+ + "# 4 " + '\n'
+ + "# 5" + '\n'
+ + "#" + '\n' // 6 - find empty comment for syntax highlighting
+ + "# 7 " + '\n'
+ + "# 8" + '\n'
+ + "genrule(name = 'foo'," + '\n'
+ + " srcs = ['input.csv']," + '\n'
+ + " # 11" + '\n'
+ + " outs = [ 'result.txt'," + '\n'
+ + " 'result.log'], # 13" + '\n'
+ + " cmd = 'touch result.txt result.log')" + '\n'
+ + "# 15" + '\n'
+ );
+ assertThat(result.statements).hasSize(1); // Single genrule
+ StringBuilder commentLines = new StringBuilder();
+ for (Comment comment : result.comments) {
+ // Comments start and end on the same line
+ assertEquals(comment.getLocation().getStartLineAndColumn().getLine() + " ends on "
+ + comment.getLocation().getEndLineAndColumn().getLine(),
+ comment.getLocation().getStartLineAndColumn().getLine(),
+ comment.getLocation().getEndLineAndColumn().getLine());
+ commentLines.append('(');
+ commentLines.append(comment.getLocation().getStartLineAndColumn().getLine());
+ commentLines.append(',');
+ commentLines.append(comment.getLocation().getStartLineAndColumn().getColumn());
+ commentLines.append(") ");
+ }
+ assertWithMessage("Found: " + commentLines)
+ .that(result.comments.size()).isEqualTo(10); // One per '#'
+ }
+
+ public void testMissingComma() throws Exception {
+ syntaxEvents.setFailFast(false);
+ // Regression test.
+ // Note: missing comma after name='foo'
+ parseFile("genrule(name = 'foo'\n"
+ + " srcs = ['in'])");
+ syntaxEvents.assertContainsEvent("syntax error at 'srcs'");
+ }
+
+ public void testDoubleSemicolon() throws Exception {
+ syntaxEvents.setFailFast(false);
+ // Regression test.
+ parseFile("x = 1; ; x = 2;");
+ syntaxEvents.assertContainsEvent("syntax error at ';'");
+ }
+
+ public void testFunctionDefinitionErrorRecovery() throws Exception {
+ // Parser skips over entire function definitions, and reports a meaningful
+ // error.
+ syntaxEvents.setFailFast(false);
+ List<Statement> stmts = parseFile(
+ "x = 1;\n"
+ + "def foo(x, y, **z):\n"
+ + " # a comment\n"
+ + " x = 2\n"
+ + " foo(bar)\n"
+ + " return z\n"
+ + "x = 3");
+ assertThat(stmts).hasSize(2);
+ }
+
+ public void testFunctionDefinitionIgnored() throws Exception {
+ // Parser skips over entire function definitions without reporting error,
+ // when parsePython is set to true.
+ List<Statement> stmts = parseFile(
+ "x = 1;\n"
+ + "def foo(x, y, **z):\n"
+ + " # a comment\n"
+ + " if true:"
+ + " x = 2\n"
+ + " foo(bar)\n"
+ + " return z\n"
+ + "x = 3", true /* parsePython */);
+ assertThat(stmts).hasSize(2);
+
+ stmts = parseFile(
+ "x = 1;\n"
+ + "def foo(x, y, **z): return x\n"
+ + "x = 3", true /* parsePython */);
+ assertThat(stmts).hasSize(2);
+ }
+
+ public void testMissingBlock() throws Exception {
+ syntaxEvents.setFailFast(false);
+ List<Statement> stmts = parseFile(
+ "x = 1;\n"
+ + "def foo(x):\n"
+ + "x = 2;\n",
+ true /* parsePython */);
+ assertThat(stmts).hasSize(2);
+ syntaxEvents.assertContainsEvent("expected an indented block");
+ }
+
+ public void testInvalidDef() throws Exception {
+ syntaxEvents.setFailFast(false);
+ parseFile(
+ "x = 1;\n"
+ + "def foo(x)\n"
+ + "x = 2;\n",
+ true /* parsePython */);
+ syntaxEvents.assertContainsEvent("syntax error at 'EOF'");
+ }
+
+ public void testSkipIfBlock() throws Exception {
+ // Skip over 'if' blocks, when parsePython is set
+ List<Statement> stmts = parseFile(
+ "x = 1;\n"
+ + "if x == 1:\n"
+ + " foo(x)\n"
+ + "else:\n"
+ + " bar(x)\n"
+ + "x = 3;\n",
+ true /* parsePython */);
+ assertThat(stmts).hasSize(2);
+ }
+
+ public void testSkipIfBlockFail() throws Exception {
+ // Do not parse 'if' blocks, when parsePython is not set
+ syntaxEvents.setFailFast(false);
+ List<Statement> stmts = parseFile(
+ "x = 1;\n"
+ + "if x == 1:\n"
+ + " x = 2\n"
+ + "x = 3;\n",
+ false /* no parsePython */);
+ assertThat(stmts).hasSize(2);
+ syntaxEvents.assertContainsEvent("This Python-style construct is not supported");
+ }
+
+ public void testForLoopMultipleVariablesFail() throws Exception {
+ // For loops with multiple variables are not allowed, when parsePython is not set
+ syntaxEvents.setFailFast(false);
+ List<Statement> stmts = parseFile(
+ "[ i for i, j, k in [(1, 2, 3)] ]\n",
+ false /* no parsePython */);
+ assertThat(stmts).hasSize(1);
+ syntaxEvents.assertContainsEvent("For loops with multiple variables are not yet supported.");
+ }
+
+ public void testForLoopMultipleVariables() throws Exception {
+ // For loops with multiple variables is ok, when parsePython is set
+ List<Statement> stmts1 = parseFile(
+ "[ i for i, j, k in [(1, 2, 3)] ]\n",
+ true /* parsePython */);
+ assertThat(stmts1).hasSize(1);
+
+ List<Statement> stmts2 = parseFile(
+ "[ i for i, j in [(1, 2, 3)] ]\n",
+ true /* parsePython */);
+ assertThat(stmts2).hasSize(1);
+
+ List<Statement> stmts3 = parseFile(
+ "[ i for (i, j, k) in [(1, 2, 3)] ]\n",
+ true /* parsePython */);
+ assertThat(stmts3).hasSize(1);
+ }
+
+ public void testForLoopBadSyntax() throws Exception {
+ syntaxEvents.setFailFast(false);
+ parseFile(
+ "[1 for (a, b, c in var]\n",
+ false /* no parsePython */);
+ syntaxEvents.assertContainsEvent("syntax error");
+ }
+
+ public void testForLoopBadSyntax2() throws Exception {
+ syntaxEvents.setFailFast(false);
+ parseFile(
+ "[1 for () in var]\n",
+ false /* no parsePython */);
+ syntaxEvents.assertContainsEvent("syntax error");
+ }
+
+ public void testFunCallBadSyntax() throws Exception {
+ syntaxEvents.setFailFast(false);
+ parseFile("f(1,\n");
+ syntaxEvents.assertContainsEvent("syntax error");
+ }
+
+ public void testFunCallBadSyntax2() throws Exception {
+ syntaxEvents.setFailFast(false);
+ parseFile("f(1, 5, ,)\n");
+ syntaxEvents.assertContainsEvent("syntax error");
+ }
+
+ public void testLoadOneSymbol() throws Exception {
+ List<Statement> statements = parseFileForSkylark(
+ "load('/foo/bar/file', 'fun_test')\n");
+ LoadStatement stmt = (LoadStatement) statements.get(0);
+ assertEquals("/foo/bar/file.bzl", stmt.getImportPath().toString());
+ assertThat(stmt.getSymbols()).hasSize(1);
+ }
+
+ public void testLoadMultipleSymbols() throws Exception {
+ List<Statement> statements = parseFileForSkylark(
+ "load('file', 'foo', 'bar')\n");
+ LoadStatement stmt = (LoadStatement) statements.get(0);
+ assertEquals("file.bzl", stmt.getImportPath().toString());
+ assertThat(stmt.getSymbols()).hasSize(2);
+ }
+
+ public void testLoadSyntaxError() throws Exception {
+ syntaxEvents.setFailFast(false);
+ parseFileForSkylark("load(non_quoted, 'a')\n");
+ syntaxEvents.assertContainsEvent("syntax error");
+ }
+
+ public void testLoadSyntaxError2() throws Exception {
+ syntaxEvents.setFailFast(false);
+ parseFileForSkylark("load('non_quoted', a)\n");
+ syntaxEvents.assertContainsEvent("syntax error");
+ }
+
+ public void testLoadNotAtTopLevel() throws Exception {
+ syntaxEvents.setFailFast(false);
+ parseFileForSkylark("if 1: load(8)\n");
+ syntaxEvents.assertContainsEvent("function 'load' does not exist");
+ }
+
+ public void testParseErrorNotComparison() throws Exception {
+ syntaxEvents.setFailFast(false);
+ parseFile("2 < not 3");
+ syntaxEvents.assertContainsEvent("syntax error at 'not'");
+ }
+
+ public void testNotWithArithmeticOperatorsBadSyntax() throws Exception {
+ syntaxEvents.setFailFast(false);
+ parseFile("0 + not 0");
+ syntaxEvents.assertContainsEvent("syntax error at 'not'");
+ }
+
+ public void testOptionalArgBeforeMandatoryArgInFuncDef() throws Exception {
+ syntaxEvents.setFailFast(false);
+ parseFileForSkylark("def func(a, b = 'a', c):\n return 0\n");
+ syntaxEvents.assertContainsEvent(
+ "a mandatory positional parameter must not follow an optional parameter");
+ }
+
+ public void testKwargBeforePositionalArg() throws Exception {
+ syntaxEvents.setFailFast(false);
+ parseFileForSkylark(
+ "def func(a, b): return a + b\n"
+ + "func(**{'b': 1}, 'a')");
+ syntaxEvents.assertContainsEvent("unexpected tokens after kwarg");
+ }
+
+ public void testDuplicateKwarg() throws Exception {
+ syntaxEvents.setFailFast(false);
+ parseFileForSkylark(
+ "def func(a, b): return a + b\n"
+ + "func(**{'b': 1}, **{'a': 2})");
+ syntaxEvents.assertContainsEvent("unexpected tokens after kwarg");
+ }
+
+ public void testUnnamedStar() throws Exception {
+ syntaxEvents.setFailFast(false);
+ parseFileForSkylark(
+ "def func(a, b1=2, b2=3, *, c1, c2, d=4): return a + b1 + b2 + c1 + c2 + d\n");
+ syntaxEvents.assertContainsEvent("no star, star-star or named-only parameters (for now)");
+ }
+
+ public void testTopLevelForFails() throws Exception {
+ syntaxEvents.setFailFast(false);
+ parseFileForSkylark("for i in []: 0\n");
+ syntaxEvents.assertContainsEvent(
+ "for loops are not allowed on top-level. Put it into a function");
+ }
+
+ public void testNestedFunctionFails() throws Exception {
+ syntaxEvents.setFailFast(false);
+ parseFileForSkylark(
+ "def func(a):\n"
+ + " def bar(): return 0\n"
+ + " return bar()\n");
+ syntaxEvents.assertContainsEvent(
+ "nested functions are not allowed. Move the function to top-level");
+ }
+
+ public void testIncludeFailureSkylark() throws Exception {
+ syntaxEvents.setFailFast(false);
+ parseFileForSkylark("include('//foo:bar')");
+ syntaxEvents.assertContainsEvent("function 'include' does not exist");
+ }
+
+ public void testIncludeFailure() throws Exception {
+ syntaxEvents.setFailFast(false);
+ parseFile("include('nonexistent')\n");
+ syntaxEvents.assertContainsEvent("Invalid label 'nonexistent'");
+ }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/syntax/SkylarkEvaluationTest.java b/src/test/java/com/google/devtools/build/lib/syntax/SkylarkEvaluationTest.java
new file mode 100644
index 0000000000..5ddb2af202
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/syntax/SkylarkEvaluationTest.java
@@ -0,0 +1,799 @@
+// Copyright 2015 Google Inc. 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 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.actions.Artifact;
+import com.google.devtools.build.lib.actions.Artifact.SpecialArtifact;
+import com.google.devtools.build.lib.analysis.FileConfiguredTarget;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTarget;
+import com.google.devtools.build.lib.analysis.TransitiveInfoCollection;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.events.EventKind;
+import com.google.devtools.build.lib.events.util.EventCollectionApparatus;
+import com.google.devtools.build.lib.packages.MethodLibrary;
+import com.google.devtools.build.lib.rules.SkylarkModules;
+import com.google.devtools.build.lib.syntax.ClassObject.SkylarkClassObject;
+
+import java.util.List;
+
+/**
+ * Evaluation tests with Skylark Environment.
+ */
+public class SkylarkEvaluationTest extends EvaluationTest {
+
+ @SkylarkModule(name = "Mock", doc = "")
+ static class Mock {
+ @SkylarkCallable(doc = "")
+ public static Integer valueOf(String str) {
+ return Integer.valueOf(str);
+ }
+ @SkylarkCallable(doc = "")
+ public Boolean isEmpty(String str) {
+ return str.isEmpty();
+ }
+ public void value() {}
+ @SkylarkCallable(doc = "")
+ public Mock returnMutable() {
+ return new Mock();
+ }
+ @SkylarkCallable(name = "struct_field", doc = "", structField = true)
+ public String structField() {
+ return "a";
+ }
+ @SkylarkCallable(name = "function", doc = "", structField = false)
+ public String function() {
+ return "a";
+ }
+ @SuppressWarnings("unused")
+ @SkylarkCallable(name = "nullfunc_failing", doc = "", allowReturnNones = false)
+ public Object nullfuncFailing(String p1, Integer p2) {
+ return null;
+ }
+ @SkylarkCallable(name = "nullfunc_working", doc = "", allowReturnNones = true)
+ public Object nullfuncWorking() {
+ return null;
+ }
+ @SkylarkCallable(name = "voidfunc", doc = "")
+ public void voidfunc() {}
+ @SkylarkCallable(name = "string_list", doc = "")
+ public ImmutableList<String> stringList() {
+ return ImmutableList.<String>of("a", "b");
+ }
+ @SkylarkCallable(name = "string", doc = "")
+ public String string() {
+ return "a";
+ }
+ }
+
+ @SkylarkModule(name = "MockInterface", doc = "")
+ static interface MockInterface {
+ @SkylarkCallable(doc = "")
+ public Boolean isEmptyInterface(String str);
+ }
+
+ static 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();
+ }
+ @SkylarkCallable(doc = "")
+ public Boolean isEmptyClassNotAnnotated(String str) {
+ return str.isEmpty();
+ }
+ }
+
+ static final class MockClassObject implements ClassObject {
+ @Override
+ public Object getValue(String name) {
+ switch (name) {
+ case "field": return "a";
+ case "nset": return NestedSetBuilder.stableOrder().build();
+ }
+ throw new IllegalStateException();
+ }
+
+ @Override
+ public ImmutableCollection<String> getKeys() {
+ return ImmutableList.of("field", "nset");
+ }
+
+ @Override
+ public String errorMessage(String name) {
+ return null;
+ }
+ }
+
+ @SkylarkModule(name = "MockMultipleMethodClass", doc = "")
+ static final class MockMultipleMethodClass {
+ @SuppressWarnings("unused")
+ @SkylarkCallable(doc = "")
+ public void method(Object o) {}
+ @SuppressWarnings("unused")
+ @SkylarkCallable(doc = "")
+ public void method(String i) {}
+ }
+
+ private static final ImmutableMap<String, SkylarkType> MOCK_TYPES = ImmutableMap
+ .<String, SkylarkType>of("mock", SkylarkType.UNKNOWN, "Mock", SkylarkType.UNKNOWN);
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ syntaxEvents = new EventCollectionApparatus(EventKind.ALL_EVENTS);
+ env = new SkylarkEnvironment(syntaxEvents.collector());
+ MethodLibrary.setupMethodEnvironment(env);
+ }
+
+ @Override
+ public Environment singletonEnv(String id, Object value) {
+ SkylarkEnvironment env = new SkylarkEnvironment(syntaxEvents.collector());
+ env.update(id, value);
+ return env;
+ }
+
+ public void testSimpleIf() throws Exception {
+ exec(parseFileForSkylark(
+ "def foo():\n"
+ + " a = 0\n"
+ + " x = 0\n"
+ + " if x: a = 5\n"
+ + " return a\n"
+ + "a = foo()"), env);
+ assertEquals(0, env.lookup("a"));
+ }
+
+ public void testNestedIf() throws Exception {
+ executeNestedIf(0, 0, env);
+ assertEquals(0, env.lookup("x"));
+
+ executeNestedIf(1, 0, env);
+ assertEquals(3, env.lookup("x"));
+
+ executeNestedIf(1, 1, env);
+ assertEquals(5, env.lookup("x"));
+ }
+
+ private void executeNestedIf(int x, int y, Environment env) throws Exception {
+ List<Statement> input = parseFileForSkylark(
+ "def foo():\n"
+ + " x = " + x + "\n"
+ + " y = " + y + "\n"
+ + " a = 0\n"
+ + " b = 0\n"
+ + " if x:\n"
+ + " if y:\n"
+ + " a = 2\n"
+ + " b = 3\n"
+ + " return a + b\n"
+ + "x = foo()");
+ exec(input, env);
+ }
+
+ public void testIfElse() throws Exception {
+ executeIfElse("something", 2);
+ executeIfElse("", 3);
+ }
+
+ private void executeIfElse(String y, int expectedA) throws Exception {
+ List<Statement> input = parseFileForSkylark(
+ "def foo():\n"
+ + " y = '" + y + "'\n"
+ + " x = 5\n"
+ + " if x:\n"
+ + " if y: a = 2\n"
+ + " else: a = 3\n"
+ + " return a\n"
+ + "a = foo()");
+
+ exec(input, env);
+ assertEquals(expectedA, env.lookup("a"));
+ }
+
+ public void testIfElifElse_IfExecutes() throws Exception {
+ execIfElifElse(1, 0, 1);
+ }
+
+ public void testIfElifElse_ElifExecutes() throws Exception {
+ execIfElifElse(0, 1, 2);
+ }
+
+ public void testIfElifElse_ElseExecutes() throws Exception {
+ execIfElifElse(0, 0, 3);
+ }
+
+ private void execIfElifElse(int x, int y, int v) throws Exception {
+ List<Statement> input = parseFileForSkylark(
+ "def foo():\n"
+ + " x = " + x + "\n"
+ + " y = " + y + "\n"
+ + " if x:\n"
+ + " return 1\n"
+ + " elif y:\n"
+ + " return 2\n"
+ + " else:\n"
+ + " return 3\n"
+ + "v = foo()");
+ exec(input, env);
+ assertEquals(v, env.lookup("v"));
+ }
+
+ public void testForOnList() throws Exception {
+ List<Statement> input = parseFileForSkylark(
+ "def foo():\n"
+ + " s = ''\n"
+ + " for i in ['hello', ' ', 'world']:\n"
+ + " s = s + i\n"
+ + " return s\n"
+ + "s = foo()\n");
+
+ exec(input, env);
+ assertEquals("hello world", env.lookup("s"));
+ }
+
+ @SuppressWarnings("unchecked")
+ public void testForOnString() throws Exception {
+ List<Statement> input = parseFileForSkylark(
+ "def foo():\n"
+ + " s = []\n"
+ + " for i in 'abc':\n"
+ + " s = s + [i]\n"
+ + " return s\n"
+ + "s = foo()\n");
+
+ exec(input, env);
+ assertThat((Iterable<Object>) env.lookup("s")).containsExactly("a", "b", "c").inOrder();
+ }
+
+ public void testForAssignmentList() throws Exception {
+ List<Statement> input = parseFileForSkylark(
+ "def foo():\n"
+ + " d = ['a', 'b', 'c']\n"
+ + " s = ''\n"
+ + " for i in d:\n"
+ + " s = s + i\n"
+ + " d = ['d', 'e', 'f']\n" // check that we use the old list
+ + " return s\n"
+ + "s = foo()\n");
+
+ exec(input, env);
+ assertEquals("abc", env.lookup("s"));
+ }
+
+ public void testForAssignmentDict() throws Exception {
+ List<Statement> input = parseFileForSkylark(
+ "def func():\n"
+ + " d = {'a' : 1, 'b' : 2, 'c' : 3}\n"
+ + " s = ''\n"
+ + " for i in d:\n"
+ + " s = s + i\n"
+ + " d = {'d' : 1, 'e' : 2, 'f' : 3}\n"
+ + " return s\n"
+ + "s = func()");
+
+ exec(input, env);
+ assertEquals("abc", env.lookup("s"));
+ }
+
+ public void testForNotIterable() throws Exception {
+ env.update("mock", new Mock());
+ List<Statement> input = parseFileForSkylark(
+ "def func():\n"
+ + " for i in mock.value_of('1'): a = i\n"
+ + "func()\n", MOCK_TYPES);
+ checkEvalError(input, env, "type 'int' is not an iterable");
+ }
+
+ public void testForOnDictionary() throws Exception {
+ List<Statement> input = parseFileForSkylark(
+ "def foo():\n"
+ + " d = {1: 'a', 2: 'b', 3: 'c'}\n"
+ + " s = ''\n"
+ + " for i in d: s = s + d[i]\n"
+ + " return s\n"
+ + "s = foo()");
+
+ exec(input, env);
+ assertEquals("abc", env.lookup("s"));
+ }
+
+ public void testForLoopReuseVariable() throws Exception {
+ List<Statement> input = parseFileForSkylark(
+ "def foo():\n"
+ + " s = ''\n"
+ + " for i in ['a', 'b']:\n"
+ + " for i in ['c', 'd']: s = s + i\n"
+ + " return s\n"
+ + "s = foo()");
+
+ exec(input, env);
+ assertEquals("cdcd", env.lookup("s"));
+ }
+
+ public void testNoneAssignment() throws Exception {
+ List<Statement> input = parseFileForSkylark(
+ "def foo(x=None):\n"
+ + " x = 1\n"
+ + " x = None\n"
+ + " return 2\n"
+ + "s = foo()");
+
+ exec(input, env);
+ assertEquals(2, env.lookup("s"));
+ }
+
+ public void testJavaCalls() throws Exception {
+ env.update("mock", new Mock());
+ List<Statement> input = parseFileForSkylark(
+ "b = mock.is_empty('a')", MOCK_TYPES);
+ exec(input, env);
+ assertEquals(Boolean.FALSE, env.lookup("b"));
+ }
+
+ public void testJavaCallsOnSubClass() throws Exception {
+ env.update("mock", new MockSubClass());
+ List<Statement> input = parseFileForSkylark(
+ "b = mock.is_empty('a')", MOCK_TYPES);
+ exec(input, env);
+ assertEquals(Boolean.FALSE, env.lookup("b"));
+ }
+
+ public void testJavaCallsOnInterface() throws Exception {
+ env.update("mock", new MockSubClass());
+ List<Statement> input = parseFileForSkylark(
+ "b = mock.is_empty_interface('a')", MOCK_TYPES);
+ exec(input, env);
+ assertEquals(Boolean.FALSE, env.lookup("b"));
+ }
+
+ public void testJavaCallsNotSkylarkCallable() throws Exception {
+ env.update("mock", new Mock());
+ List<Statement> input = parseFileForSkylark("mock.value()", MOCK_TYPES);
+ checkEvalError(input, env, "No matching method found for value() in Mock");
+ }
+
+ public void testJavaCallsNoMethod() throws Exception {
+ List<Statement> input = parseFileForSkylark(
+ "s = 3.bad()");
+ checkEvalError(input, env, "No matching method found for bad() in int");
+ }
+
+ public void testJavaCallsNoMethodErrorMsg() throws Exception {
+ List<Statement> input = parseFileForSkylark(
+ "s = 3.bad('a', 'b', 'c')");
+ checkEvalError(input, env,
+ "No matching method found for bad(string, string, string) in int");
+ }
+
+ public void testJavaCallsMultipleMethod() throws Exception {
+ env.update("mock", new MockMultipleMethodClass());
+ List<Statement> input = parseFileForSkylark(
+ "s = mock.method('string')", MOCK_TYPES);
+ checkEvalError(input, env,
+ "Multiple matching methods for method(string) in MockMultipleMethodClass");
+ }
+
+ public void testJavaCallWithKwargs() throws Exception {
+ List<Statement> input = parseFileForSkylark("comp = 3.compare_to(x = 4)");
+ checkEvalError(input, env, "Keyword arguments are not allowed when calling a java method"
+ + "\nwhile calling method 'compare_to' on object 3 of type int");
+ }
+
+ public void testNoJavaCallsWithoutSkylark() throws Exception {
+ List<Statement> input = parseFileForSkylark("s = 3.to_string()\n");
+ checkEvalError(input, env, "No matching method found for to_string() in int");
+ }
+
+ public void testNoJavaCallsIfClassNotAnnotated() throws Exception {
+ env.update("mock", new MockSubClass());
+ List<Statement> input = parseFileForSkylark(
+ "b = mock.is_empty_class_not_annotated('a')", MOCK_TYPES);
+ checkEvalError(input, env,
+ "No matching method found for is_empty_class_not_annotated(string) in MockSubClass");
+ }
+
+ public void testStructAccess() throws Exception {
+ env.update("mock", new Mock());
+ List<Statement> input = parseFileForSkylark(
+ "v = mock.struct_field", MOCK_TYPES);
+ exec(input, env);
+ assertEquals("a", env.lookup("v"));
+ }
+
+ public void testStructAccessAsFuncall() throws Exception {
+ env.update("mock", new Mock());
+ checkEvalError(parseFileForSkylark("v = mock.struct_field()", MOCK_TYPES), env,
+ "No matching method found for struct_field() in Mock");
+ }
+
+ public void testStructAccessOfMethod() throws Exception {
+ env.update("mock", new Mock());
+ checkEvalError(parseFileForSkylark(
+ "v = mock.function", MOCK_TYPES), env, "Object of type 'Mock' has no field 'function'");
+ }
+
+ public void testJavaFunctionReturnsMutableObject() throws Exception {
+ env.update("mock", new Mock());
+ List<Statement> input = parseFileForSkylark("mock.return_mutable()", MOCK_TYPES);
+ checkEvalError(input, env, "Method 'return_mutable' returns a mutable object (type of Mock)");
+ }
+
+ public void testJavaFunctionReturnsNullFails() throws Exception {
+ env.update("mock", new Mock());
+ List<Statement> input = parseFileForSkylark("mock.nullfunc_failing('abc', 1)", MOCK_TYPES);
+ checkEvalError(input, env, "Method invocation returned None,"
+ + " please contact Skylark developers: nullfunc_failing(\"abc\", 1)");
+ }
+
+ public void testClassObjectAccess() throws Exception {
+ env.update("mock", new MockClassObject());
+ exec(parseFileForSkylark("v = mock.field", MOCK_TYPES), env);
+ assertEquals("a", env.lookup("v"));
+ }
+
+ public void testClassObjectCannotAccessNestedSet() throws Exception {
+ env.update("mock", new MockClassObject());
+ checkEvalError(parseFileForSkylark("v = mock.nset", MOCK_TYPES), env,
+ "Type is not allowed in Skylark: EmptyNestedSet");
+ }
+
+ public void testJavaFunctionReturnsNone() throws Exception {
+ env.update("mock", new Mock());
+ exec(parseFileForSkylark("v = mock.nullfunc_working()", MOCK_TYPES), env);
+ assertSame(Environment.NONE, env.lookup("v"));
+ }
+
+ public void testVoidJavaFunctionReturnsNone() throws Exception {
+ env.update("mock", new Mock());
+ exec(parseFileForSkylark("v = mock.voidfunc()", MOCK_TYPES), env);
+ assertSame(Environment.NONE, env.lookup("v"));
+ }
+
+ public void testAugmentedAssignment() throws Exception {
+ exec(parseFileForSkylark(
+ "def f1(x):\n"
+ + " x += 1\n"
+ + " return x\n"
+ + "\n"
+ + "foo = f1(41)\n"), env);
+ assertEquals(42, env.lookup("foo"));
+ }
+
+ public void testStaticDirectJavaCall() throws Exception {
+ List<Statement> input = parseFileForSkylark(
+ "val = Mock.value_of('8')", MOCK_TYPES);
+
+ env.update("Mock", Mock.class);
+ exec(input, env);
+ assertEquals(8, env.lookup("val"));
+ }
+
+ public void testStaticDirectJavaCallMethodIsNonStatic() throws Exception {
+ List<Statement> input = parseFileForSkylark(
+ "val = Mock.is_empty('a')", MOCK_TYPES);
+
+ env.update("Mock", Mock.class);
+ checkEvalError(input, env, "Method 'is_empty' is not static");
+ }
+
+ public void testDictComprehensions_IterationOrder() throws Exception {
+ List<Statement> input = parseFileForSkylark(
+ "def foo():\n"
+ + " d = {x : x for x in ['c', 'a', 'b']}\n"
+ + " s = ''\n"
+ + " for a in d:\n"
+ + " s += a\n"
+ + " return s\n"
+ + "s = foo()");
+ exec(input, env);
+ assertEquals("cab", env.lookup("s"));
+ }
+
+ public void testStructCreation() throws Exception {
+ exec(parseFileForSkylark("x = struct(a = 1, b = 2)"), env);
+ assertThat(env.lookup("x")).isInstanceOf(ClassObject.class);
+ }
+
+ public void testStructFields() throws Exception {
+ exec(parseFileForSkylark("x = struct(a = 1, b = 2)"), env);
+ ClassObject x = (ClassObject) env.lookup("x");
+ assertEquals(1, x.getValue("a"));
+ assertEquals(2, x.getValue("b"));
+ }
+
+ public void testStructAccessingFieldsFromSkylark() throws Exception {
+ exec(parseFileForSkylark(
+ "x = struct(a = 1, b = 2)\n"
+ + "x1 = x.a\n"
+ + "x2 = x.b\n"), env);
+ assertEquals(1, env.lookup("x1"));
+ assertEquals(2, env.lookup("x2"));
+ }
+
+ public void testStructAccessingUnknownField() throws Exception {
+ checkEvalError(parseFileForSkylark(
+ "x = struct(a = 1, b = 2)\n"
+ + "y = x.c\n"), env, "Object of type 'struct' has no field 'c'");
+ }
+
+ public void testStructAccessingFieldsWithArgs() throws Exception {
+ checkEvalError(parseFileForSkylark(
+ "x = struct(a = 1, b = 2)\n"
+ + "x1 = x.a(1)\n"),
+ env, "No matching method found for a(int) in struct");
+ }
+
+ public void testStructPosArgs() throws Exception {
+ checkEvalError(parseFileForSkylark(
+ "x = struct(1, b = 2)\n"),
+ env, "struct only supports keyword arguments");
+ }
+
+ public void testStructConcatenationFieldNames() throws Exception {
+ exec(parseFileForSkylark(
+ "x = struct(a = 1, b = 2)\n"
+ + "y = struct(c = 1, d = 2)\n"
+ + "z = x + y\n"), env);
+ SkylarkClassObject z = (SkylarkClassObject) env.lookup("z");
+ assertEquals(ImmutableSet.of("a", "b", "c", "d"), z.getKeys());
+ }
+
+ public void testStructConcatenationFieldValues() throws Exception {
+ exec(parseFileForSkylark(
+ "x = struct(a = 1, b = 2)\n"
+ + "y = struct(c = 1, d = 2)\n"
+ + "z = x + y\n"), env);
+ SkylarkClassObject z = (SkylarkClassObject) env.lookup("z");
+ assertEquals(1, z.getValue("a"));
+ assertEquals(2, z.getValue("b"));
+ assertEquals(1, z.getValue("c"));
+ assertEquals(2, z.getValue("d"));
+ }
+
+ public void testStructConcatenationCommonFields() throws Exception {
+ checkEvalError(parseFileForSkylark(
+ "x = struct(a = 1, b = 2)\n"
+ + "y = struct(c = 1, a = 2)\n"
+ + "z = x + y\n"), env, "Cannot concat structs with common field(s): a");
+ }
+
+ public void testDotExpressionOnNonStructObject() throws Exception {
+ checkEvalError(parseFileForSkylark(
+ "x = 'a'.field"), env, "Object of type 'string' has no field 'field'");
+ }
+
+ public void testPlusEqualsOnDict() throws Exception {
+ MethodLibrary.setupMethodEnvironment(env);
+ exec(parseFileForSkylark(
+ "def func():\n"
+ + " d = {'a' : 1}\n"
+ + " d += {'b' : 2}\n"
+ + " return d\n"
+ + "d = func()"), env);
+ assertEquals(ImmutableMap.of("a", 1, "b", 2), env.lookup("d"));
+ }
+
+ public void testDictAssignmentAsLValue() throws Exception {
+ exec(parseFileForSkylark(
+ "def func():\n"
+ + " d = {'a' : 1}\n"
+ + " d['b'] = 2\n"
+ + " return d\n"
+ + "d = func()"), env);
+ assertEquals(ImmutableMap.of("a", 1, "b", 2), env.lookup("d"));
+ }
+
+ public void testDictAssignmentAsLValueNoSideEffects() throws Exception {
+ MethodLibrary.setupMethodEnvironment(env);
+ exec(parseFileForSkylark(
+ "def func(d):\n"
+ + " d['b'] = 2\n"
+ + "d = {'a' : 1}\n"
+ + "func(d)"), env);
+ assertEquals(ImmutableMap.of("a", 1), env.lookup("d"));
+ }
+
+ public void testListIndexAsLValueAsLValue() throws Exception {
+ checkEvalError(parseFileForSkylark(
+ "def id(l):\n"
+ + " return l\n"
+ + "def func():\n"
+ + " l = id([1])\n"
+ + " l[0] = 2\n"
+ + " return l\n"
+ + "l = func()"), env, "unsupported operand type(s) for +: 'list' and 'dict'");
+ }
+
+ public void testTopLevelDict() throws Exception {
+ exec(parseFileForSkylark(
+ "if 1:\n"
+ + " v = 'a'\n"
+ + "else:\n"
+ + " v = 'b'"), env);
+ assertEquals("a", env.lookup("v"));
+ }
+
+ public void testUserFunctionKeywordArgs() throws Exception {
+ exec(parseFileForSkylark(
+ "def foo(a, b, c):\n"
+ + " return a + b + c\n"
+ + "s = foo(1, c=2, b=3)"), env);
+ assertEquals(6, env.lookup("s"));
+ }
+
+ public void testNoneTrueFalseInSkylark() throws Exception {
+ exec(parseFileForSkylark(
+ "a = None\n"
+ + "b = True\n"
+ + "c = False"), env);
+ assertSame(Environment.NONE, env.lookup("a"));
+ assertTrue((Boolean) env.lookup("b"));
+ assertFalse((Boolean) env.lookup("c"));
+ }
+
+ public void testHasattr() throws Exception {
+ exec(parseFileForSkylark(
+ "s = struct(a=1)\n"
+ + "x = hasattr(s, 'a')\n"
+ + "y = hasattr(s, 'b')\n"), env);
+ assertTrue((Boolean) env.lookup("x"));
+ assertFalse((Boolean) env.lookup("y"));
+ }
+
+ public void testHasattrMethods() throws Exception {
+ env.update("mock", new Mock());
+ ValidationEnvironment validEnv = SkylarkModules.getValidationEnvironment();
+ validEnv.update("mock", SkylarkType.of(Mock.class), null);
+ exec(Parser.parseFileForSkylark(createLexer(
+ "a = hasattr(mock, 'struct_field')\n"
+ + "b = hasattr(mock, 'function')\n"
+ + "c = hasattr(mock, 'is_empty')\n"
+ + "d = hasattr('str', 'replace')\n"
+ + "e = hasattr(mock, 'other')\n"),
+ syntaxEvents.reporter(), null, validEnv).statements, env);
+ assertTrue((Boolean) env.lookup("a"));
+ assertTrue((Boolean) env.lookup("b"));
+ assertTrue((Boolean) env.lookup("c"));
+ assertTrue((Boolean) env.lookup("d"));
+ assertFalse((Boolean) env.lookup("e"));
+ }
+
+ public void testGetattr() throws Exception {
+ exec(parseFileForSkylark(
+ "s = struct(a='val')\n"
+ + "x = getattr(s, 'a')\n"
+ + "y = getattr(s, 'b', 'def')\n"
+ + "z = getattr(s, 'b', default = 'def')\n"
+ + "w = getattr(s, 'a', default='ignored')"), env);
+ assertEquals("val", env.lookup("x"));
+ assertEquals("def", env.lookup("y"));
+ assertEquals("def", env.lookup("z"));
+ assertEquals("val", env.lookup("w"));
+ }
+
+ public void testGetattrNoAttr() throws Exception {
+ checkEvalError(parseFileForSkylark(
+ "s = struct(a='val')\n"
+ + "getattr(s, 'b')"),
+ env, "Object of type 'struct' has no field 'b'");
+ }
+
+ @SuppressWarnings("unchecked")
+ public void testListAnTupleConcatenationDoesNotWorkInSkylark() throws Exception {
+ checkEvalError(parseFileForSkylark("[1, 2] + (3, 4)"), env,
+ "cannot concatenate lists and tuples");
+ }
+
+ public void testCannotCreateMixedListInSkylark() throws Exception {
+ env.update("mock", new Mock());
+ checkEvalError(parseFileForSkylark("[mock.string(), 1, 2]", MOCK_TYPES), env,
+ "Incompatible types in list: found a int but the first element is a string");
+ }
+
+ public void testCannotConcatListInSkylarkWithDifferentGenericTypes() throws Exception {
+ env.update("mock", new Mock());
+ checkEvalError(parseFileForSkylark("mock.string_list() + [1, 2]", MOCK_TYPES), env,
+ "cannot concatenate list of string with list of int");
+ }
+
+ @SuppressWarnings("unchecked")
+ public void testConcatEmptyListWithNonEmptyWorks() throws Exception {
+ exec(parseFileForSkylark("l = [] + ['a', 'b']", MOCK_TYPES), env);
+ assertThat((Iterable<Object>) env.lookup("l")).containsExactly("a", "b").inOrder();
+ }
+
+ public void testFormatStringWithTuple() throws Exception {
+ exec(parseFileForSkylark("v = '%s%s' % ('a', 1)"), env);
+ assertEquals("a1", env.lookup("v"));
+ }
+
+ @SuppressWarnings("unchecked")
+ public void testDirFindsClassObjectFields() throws Exception {
+ env.update("mock", new MockClassObject());
+ exec(parseFileForSkylark("v = dir(mock)", MOCK_TYPES), env);
+ assertThat((Iterable<String>) env.lookup("v")).containsExactly("field", "nset").inOrder();
+ }
+
+ @SuppressWarnings("unchecked")
+ public void testDirFindsJavaObjectStructFieldsAndMethods() throws Exception {
+ env.update("mock", new Mock());
+ exec(parseFileForSkylark("v = dir(mock)", MOCK_TYPES), env);
+ assertThat((Iterable<String>) env.lookup("v")).containsExactly("function", "is_empty",
+ "nullfunc_failing", "nullfunc_working", "return_mutable", "string", "string_list",
+ "struct_field", "value_of", "voidfunc").inOrder();
+ }
+
+ public void testPrint() throws Exception {
+ exec(parseFileForSkylark("print('hello')"), env);
+ syntaxEvents.assertContainsEvent("hello");
+ exec(parseFileForSkylark("print('a', 'b')"), env);
+ syntaxEvents.assertContainsEvent("a b");
+ exec(parseFileForSkylark("print('a', 'b', sep='x')"), env);
+ syntaxEvents.assertContainsEvent("axb");
+ }
+
+ public void testPrintBadKwargs() throws Exception {
+ checkEvalError("print(end='x', other='y')", "unexpected keywords: '[end, other]'");
+ }
+
+ public void testSkylarkTypes() {
+ assertEquals(TransitiveInfoCollection.class,
+ EvalUtils.getSkylarkType(FileConfiguredTarget.class));
+ assertEquals(TransitiveInfoCollection.class,
+ EvalUtils.getSkylarkType(RuleConfiguredTarget.class));
+ assertEquals(Artifact.class, EvalUtils.getSkylarkType(SpecialArtifact.class));
+ }
+
+ // Override tests in EvaluationTest incompatible with Skylark
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public void testConcatLists() throws Exception {
+ // list
+ Object x = eval("[1,2] + [3,4]");
+ assertThat((Iterable<Object>) x).containsExactly(1, 2, 3, 4).inOrder();
+ assertFalse(((SkylarkList) x).isTuple());
+
+ // tuple
+ x = eval("(1,2)");
+ assertThat((Iterable<Object>) x).containsExactly(1, 2).inOrder();
+ assertTrue(((SkylarkList) x).isTuple());
+
+ x = eval("(1,2) + (3,4)");
+ assertThat((Iterable<Object>) x).containsExactly(1, 2, 3, 4).inOrder();
+ assertTrue(((SkylarkList) x).isTuple());
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public void testListExprs() throws Exception {
+ assertThat((Iterable<Object>) eval("[1, 2, 3]")).containsExactly(1, 2, 3).inOrder();
+ assertThat((Iterable<Object>) eval("(1, 2, 3)")).containsExactly(1, 2, 3).inOrder();
+ }
+
+ @Override
+ public void testListConcatenation() throws Exception {}
+
+ @Override
+ public void testKeywordArgs() {}
+}
diff --git a/src/test/java/com/google/devtools/build/lib/syntax/SkylarkListTest.java b/src/test/java/com/google/devtools/build/lib/syntax/SkylarkListTest.java
new file mode 100644
index 0000000000..23471980f9
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/syntax/SkylarkListTest.java
@@ -0,0 +1,139 @@
+// Copyright 2014 Google Inc. 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 com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableMap;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+import com.google.devtools.build.lib.packages.MethodLibrary;
+
+import java.util.Iterator;
+
+/**
+ * Tests for SkylarkList.
+ */
+public class SkylarkListTest extends AbstractEvaluationTestCase {
+
+ @Immutable
+ private static final class CustomIterable implements Iterable<Object> {
+
+ @Override
+ public Iterator<Object> iterator() {
+ // Throw an exception whenever we request the iterator, to test that lazy lists
+ // are truly lazy.
+ throw new IllegalArgumentException("Iterator requested");
+ }
+ }
+
+ private static final SkylarkList list =
+ SkylarkList.lazyList(new CustomIterable(), Integer.class);
+ private static final ImmutableMap<String, SkylarkType> extraObjects =
+ ImmutableMap.of("lazy", SkylarkType.of(SkylarkList.class, Integer.class));
+
+ private Environment env;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ env = new SkylarkEnvironment(syntaxEvents.collector());
+ env.update("lazy", list);
+ MethodLibrary.setupMethodEnvironment(env);
+ }
+
+ public void testLazyListIndex() throws Exception {
+ checkError("Iterator requested", "a = lazy[0]");
+ }
+
+ public void testLazyListSize() throws Exception {
+ checkError("Iterator requested", "a = len(lazy)");
+ }
+
+ public void testLazyListEmpty() throws Exception {
+ checkError("Iterator requested", "if lazy:\n a = 1");
+ }
+
+ public void testLazyListConcat() throws Exception {
+ exec("v = [1, 2] + lazy");
+ assertTrue(env.lookup("v") instanceof SkylarkList);
+ }
+
+ public void testConcatListIndex() throws Exception {
+ exec("l = [1, 2] + [3, 4]",
+ "e0 = l[0]",
+ "e1 = l[1]",
+ "e2 = l[2]",
+ "e3 = l[3]");
+ assertEquals(1, env.lookup("e0"));
+ assertEquals(2, env.lookup("e1"));
+ assertEquals(3, env.lookup("e2"));
+ assertEquals(4, env.lookup("e3"));
+ }
+
+ public void testConcatListHierarchicalIndex() throws Exception {
+ exec("l = [1] + (([2] + [3, 4]) + [5])",
+ "e0 = l[0]",
+ "e1 = l[1]",
+ "e2 = l[2]",
+ "e3 = l[3]",
+ "e4 = l[4]");
+ assertEquals(1, env.lookup("e0"));
+ assertEquals(2, env.lookup("e1"));
+ assertEquals(3, env.lookup("e2"));
+ assertEquals(4, env.lookup("e3"));
+ assertEquals(5, env.lookup("e4"));
+ }
+
+ public void testConcatListSize() throws Exception {
+ exec("l = [1, 2] + [3, 4]",
+ "s = len(l)");
+ assertEquals(4, env.lookup("s"));
+ }
+
+ public void testConcatListToString() throws Exception {
+ exec("l = [1, 2] + [3, 4]",
+ "s = str(l)");
+ assertEquals("[1, 2, 3, 4]", env.lookup("s"));
+ }
+
+ public void testConcatListNotEmpty() throws Exception {
+ exec("l = [1, 2] + [3, 4]",
+ "if l:",
+ " v = 1",
+ "else:",
+ " v = 0");
+ assertEquals(1, env.lookup("v"));
+ }
+
+ public void testConcatListEmpty() throws Exception {
+ exec("l = [] + []",
+ "if l:",
+ " v = 1",
+ "else:",
+ " v = 0");
+ assertEquals(0, env.lookup("v"));
+ }
+
+ private void exec(String... input) throws Exception {
+ exec(parseFileForSkylark(Joiner.on("\n").join(input), extraObjects), env);
+ }
+
+ private void checkError(String msg, String... input) throws Exception {
+ try {
+ exec(input);
+ fail();
+ } catch (Exception e) {
+ assertEquals(msg, e.getMessage());
+ }
+ }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/syntax/SkylarkNestedSetTest.java b/src/test/java/com/google/devtools/build/lib/syntax/SkylarkNestedSetTest.java
new file mode 100644
index 0000000000..4a9a446362
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/syntax/SkylarkNestedSetTest.java
@@ -0,0 +1,170 @@
+// Copyright 2015 Google Inc. 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 com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.collect.nestedset.Order;
+import com.google.devtools.build.lib.packages.MethodLibrary;
+import com.google.devtools.build.lib.syntax.Environment.NoSuchVariableException;
+
+/**
+ * Tests for SkylarkNestedSet.
+ */
+public class SkylarkNestedSetTest extends AbstractEvaluationTestCase {
+
+ private Environment env;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ env = new SkylarkEnvironment(syntaxEvents.collector());
+ MethodLibrary.setupMethodEnvironment(env);
+ }
+
+ public void testNsetBuilder() throws Exception {
+ exec("n = set(order='stable')");
+ assertTrue(env.lookup("n") instanceof SkylarkNestedSet);
+ }
+
+ public void testNsetOrder() throws Exception {
+ exec("n = set(['a', 'b'], order='compile')");
+ assertEquals(Order.COMPILE_ORDER, get("n").getSet(String.class).getOrder());
+ }
+
+ public void testEmptyNsetGenericType() throws Exception {
+ exec("n = set()");
+ assertEquals(Object.class, get("n").getGenericType());
+ }
+
+ public void testFunctionReturnsNset() throws Exception {
+ exec("def func():",
+ " n = set()",
+ " n += ['a']",
+ " return n",
+ "s = func()");
+ assertEquals(ImmutableList.of("a"), get("s").toCollection());
+ }
+
+ public void testNsetTwoReferences() throws Exception {
+ exec("def func():",
+ " n1 = set()",
+ " n1 += ['a']",
+ " n2 = n1",
+ " n2 += ['b']",
+ " return n1",
+ "n = func()");
+ assertEquals(ImmutableList.of("a"), get("n").toCollection());
+ }
+
+ public void testNsetNestedItem() throws Exception {
+ exec("def func():",
+ " n1 = set()",
+ " n2 = set()",
+ " n1 += ['a']",
+ " n2 += ['b']",
+ " n1 += n2",
+ " return n1",
+ "n = func()");
+ assertEquals(ImmutableList.of("b", "a"), get("n").toCollection());
+ }
+
+ public void testNsetNestedItemBadOrder() throws Exception {
+ checkError("LINK_ORDER != COMPILE_ORDER",
+ "set(['a', 'b'], order='compile') + set(['c', 'd'], order='link')");
+ }
+
+ public void testNsetItemList() throws Exception {
+ exec("def func():",
+ " n = set()",
+ " n += ['a', 'b']",
+ " return n",
+ "n = func()");
+ assertEquals(ImmutableList.of("a", "b"), get("n").toCollection());
+ }
+
+ public void testNsetFuncParamNoSideEffects() throws Exception {
+ exec("def func1(n):",
+ " n += ['b']",
+ "def func2():",
+ " n = set()",
+ " n += ['a']",
+ " func1(n)",
+ " return n",
+ "n = func2()");
+ assertEquals(ImmutableList.of("a"), get("n").toCollection());
+ }
+
+ public void testNsetTransitiveOrdering() throws Exception {
+ exec("def func():",
+ " na = set(['a'], order='compile')",
+ " nb = set(['b'], order='compile')",
+ " nc = set(['c'], order='compile') + na",
+ " return set() + nb + nc",
+ "n = func()");
+ // The iterator lists the Transitive sets first
+ assertEquals(ImmutableList.of("b", "a", "c"), get("n").toCollection());
+ }
+
+ public void testNsetOrdering() throws Exception {
+ exec("def func():",
+ " na = set()",
+ " na += [4]",
+ " na += [2, 4]",
+ " na += [3, 4, 5]",
+ " return na",
+ "n = func()");
+ // The iterator lists the Transitive sets first
+ assertEquals(ImmutableList.of(4, 2, 3, 5), get("n").toCollection());
+ }
+
+ public void testNsetBadOrder() throws Exception {
+ checkError("Invalid order: non_existing",
+ "set(order='non_existing')");
+ }
+
+ public void testNsetBadRightOperand() throws Exception {
+ checkError("cannot add 'string'-s to nested sets",
+ "l = ['a']\n",
+ "set() + l[0]");
+ }
+
+ public void testNsetBadCompositeItem() throws Exception {
+ checkError("nested set item is composite (type of struct)",
+ "set([struct(a='a')])");
+ }
+
+ public void testNsetToString() throws Exception {
+ exec("s = set() + [2, 4, 6] + [3, 4, 5]",
+ "x = str(s)");
+ assertEquals("set([2, 4, 6, 3, 5])", env.lookup("x"));
+ }
+
+ private void exec(String... input) throws Exception {
+ exec(parseFileForSkylark(Joiner.on("\n").join(input)), env);
+ }
+
+ private SkylarkNestedSet get(String varname) throws NoSuchVariableException {
+ return (SkylarkNestedSet) env.lookup(varname);
+ }
+
+ private void checkError(String msg, String... input) throws Exception {
+ try {
+ exec(input);
+ fail();
+ } catch (Exception e) {
+ assertEquals(msg, e.getMessage());
+ }
+ }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/syntax/SkylarkShell.java b/src/test/java/com/google/devtools/build/lib/syntax/SkylarkShell.java
new file mode 100644
index 0000000000..6c394f9403
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/syntax/SkylarkShell.java
@@ -0,0 +1,94 @@
+// Copyright 2015 Google Inc. 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 com.google.common.collect.ImmutableMap;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.events.util.EventCollectionApparatus;
+import com.google.devtools.build.lib.packages.CachingPackageLocator;
+import com.google.devtools.build.lib.rules.SkylarkModules;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.util.FsApparatus;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+
+/**
+ * SkylarkShell is a standalone shell executing Skylark. This is intended for
+ * testing purposes and not for end-users. This is very limited (environment is
+ * almost empty), but it can be used to play with the language and reproduce
+ * bugs. Imports and includes are not supported.
+ */
+class SkylarkShell {
+ static final EventCollectionApparatus syntaxEvents = new EventCollectionApparatus();
+ static final FsApparatus scratch = FsApparatus.newInMemory();
+ static final CachingPackageLocator locator = new AbstractParserTestCase.EmptyPackageLocator();
+ static final Path path = scratch.path("stdin");
+
+ private static void exec(String inputSource, Environment env) {
+ try {
+ ParserInputSource input = ParserInputSource.create(inputSource, path);
+ Lexer lexer = new Lexer(input, syntaxEvents.reporter());
+ Parser.ParseResult result =
+ Parser.parseFileForSkylark(lexer, syntaxEvents.reporter(), locator,
+ SkylarkModules.getValidationEnvironment(
+ ImmutableMap.<String, SkylarkType>of()));
+
+ Object last = null;
+ for (Statement st : result.statements) {
+ if (st instanceof ExpressionStatement) {
+ last = ((ExpressionStatement) st).getExpression().eval(env);
+ } else {
+ st.exec(env);
+ last = null;
+ }
+ }
+ if (last != null) {
+ System.out.println(last);
+ }
+ } catch (Throwable e) { // Catch everything to avoid killing the shell.
+ e.printStackTrace();
+ }
+ }
+
+ public static void main(String[] args) {
+ Environment env = SkylarkModules.getNewEnvironment(new EventHandler() {
+ @Override
+ public void handle(Event event) {
+ System.out.println(event.getMessage());
+ }
+ });
+ BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
+
+ String currentInput = "";
+ String line;
+ System.out.print(">> ");
+ try {
+ while ((line = br.readLine()) != null) {
+ if (line.isEmpty()) {
+ exec(currentInput, env);
+ currentInput = "";
+ System.out.print(">> ");
+ } else {
+ currentInput = currentInput + "\n" + line;
+ System.out.print(".. ");
+ }
+ }
+ } catch (IOException io) {
+ io.printStackTrace();
+ }
+ }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/syntax/ValidationTests.java b/src/test/java/com/google/devtools/build/lib/syntax/ValidationTests.java
new file mode 100644
index 0000000000..723d72c690
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/syntax/ValidationTests.java
@@ -0,0 +1,576 @@
+// Copyright 2015 Google Inc. 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 com.google.common.base.Joiner;
+
+/**
+ * Tests for the validation process of Skylark files.
+ */
+public class ValidationTests extends AbstractParserTestCase {
+
+ public void testIncompatibleLiteralTypesStringInt() {
+ checkError("bad variable 'a': int is incompatible with string at /some/file.txt",
+ "def foo():\n",
+ " a = '1'",
+ " a = 1");
+ }
+
+ public void testIncompatibleLiteralTypesDictString() {
+ checkError("bad variable 'a': int is incompatible with dict at /some/file.txt",
+ "def foo():\n",
+ " a = {1 : 'x'}",
+ " a = 1");
+ }
+
+ public void testIncompatibleLiteralTypesInIf() {
+ checkError("bad variable 'a': int is incompatible with string at /some/file.txt",
+ "def foo():\n",
+ " if 1:",
+ " a = 'a'",
+ " else:",
+ " a = 1");
+ }
+
+ public void testAssignmentNotValidLValue() {
+ checkError("can only assign to variables, not to ''a''", "'a' = 1");
+ }
+
+ public void testForNotIterable() throws Exception {
+ checkError("type 'int' is not iterable",
+ "def func():\n"
+ + " for i in 5: a = i\n");
+ }
+
+ public void testForIterableWithUknownArgument() throws Exception {
+ parse("def func(x=None):\n"
+ + " for i in x: a = i\n");
+ }
+
+ public void testForNotIterableBinaryExpression() throws Exception {
+ checkError("type 'int' is not iterable",
+ "def func():\n"
+ + " for i in 1 + 1: a = i\n");
+ }
+
+ public void testOptionalArgument() throws Exception {
+ checkError("type 'int' is not iterable",
+ "def func(x=5):\n"
+ + " for i in x: a = i\n");
+ }
+
+ public void testOptionalArgumentHasError() throws Exception {
+ checkError("unsupported operand type(s) for +: 'int' and 'string'",
+ "def func(x=5+'a'):\n"
+ + " return 0\n");
+ }
+
+ public void testTopLevelForStatement() throws Exception {
+ checkError("'For' is not allowed as a top level statement", "for i in [1,2,3]: a = i\n");
+ }
+
+ public void testReturnOutsideFunction() throws Exception {
+ checkError("Return statements must be inside a function", "return 2\n");
+ }
+
+ public void testTwoReturnTypes() throws Exception {
+ checkError("bad return type of foo: string is incompatible with int at /some/file.txt:3:5",
+ "def foo(x):",
+ " if x:",
+ " return 1",
+ " else:",
+ " return 'a'");
+ }
+
+ public void testTwoFunctionsWithTheSameName() throws Exception {
+ checkError("function foo already exists",
+ "def foo():",
+ " return 1",
+ "def foo(x, y):",
+ " return 1");
+ }
+
+ public void testDynamicTypeCheck() throws Exception {
+ checkError("bad variable 'a': string is incompatible with int at /some/file.txt:2:3",
+ "def foo():",
+ " a = 1",
+ " a = '1'");
+ }
+
+ public void testFunctionLocalVariable() throws Exception {
+ checkError("name 'a' is not defined",
+ "def func2(b):",
+ " c = b",
+ " c = a",
+ "def func1():",
+ " a = 1",
+ " func2(2)");
+ }
+
+ public void testFunctionLocalVariableDoesNotEffectGlobalValidationEnv() throws Exception {
+ checkError("name 'a' is not defined",
+ "def func1():",
+ " a = 1",
+ "def func2(b):",
+ " b = a");
+ }
+
+ public void testFunctionParameterDoesNotEffectGlobalValidationEnv() throws Exception {
+ checkError("name 'a' is not defined",
+ "def func1(a):",
+ " return a",
+ "def func2():",
+ " b = a");
+ }
+
+ public void testLocalValidationEnvironmentsAreSeparated() throws Exception {
+ parse(
+ "def func1():\n"
+ + " a = 1\n"
+ + "def func2():\n"
+ + " a = 'abc'\n");
+ }
+
+ public void testListComprehensionNotIterable() throws Exception {
+ checkError("type 'int' is not iterable",
+ "[i for i in 1 for j in [2]]");
+ }
+
+ public void testListComprehensionNotIterable2() throws Exception {
+ checkError("type 'int' is not iterable",
+ "[i for i in [1] for j in 123]");
+ }
+
+ public void testListIsNotComparable() {
+ checkError("list is not comparable", "['a'] > 1");
+ }
+
+ public void testStringCompareToInt() {
+ checkError("bad comparison: int is incompatible with string", "'a' > 1");
+ }
+
+ public void testInOnInt() {
+ checkError("operand 'in' only works on strings, dictionaries, "
+ + "lists, sets or tuples, not on a(n) int", "1 in 2");
+ }
+
+ public void testUnsupportedOperator() {
+ checkError("unsupported operand type(s) for -: 'string' and 'int'", "'a' - 1");
+ }
+
+ public void testBuiltinSymbolsAreReadOnly() throws Exception {
+ checkError("Variable rule is read only", "rule = 1");
+ }
+
+ public void testSkylarkGlobalVariablesAreReadonly() throws Exception {
+ checkError("Variable a is read only",
+ "a = 1\n"
+ + "a = 2");
+ }
+
+ public void testFunctionDefRecursion() throws Exception {
+ checkError("function 'func' does not exist",
+ "def func():\n"
+ + " func()\n");
+ }
+
+ public void testMutualRecursion() throws Exception {
+ checkError("function 'bar' does not exist",
+ "def foo(i):\n"
+ + " bar(i)\n"
+ + "def bar(i):\n"
+ + " foo(i)\n"
+ + "foo(4)");
+ }
+
+ public void testFunctionReturnValue() {
+ checkError("unsupported operand type(s) for +: 'int' and 'string'",
+ "def foo(): return 1\n"
+ + "a = foo() + 'a'\n");
+ }
+
+ public void testFunctionReturnValueInFunctionDef() {
+ checkError("unsupported operand type(s) for +: 'int' and 'string'",
+ "def foo(): return 1\n"
+ + "def bar(): a = foo() + 'a'\n");
+ }
+
+ public void testFunctionDoesNotExistInFunctionDef() {
+ checkError("function 'foo' does not exist",
+ "def bar(): a = foo() + 'a'\n"
+ + "def foo(): return 1\n");
+ }
+
+ public void testStructMembersAreImmutable() {
+ checkError("can only assign to variables, not to 's.x'",
+ "s = struct(x = 'a')\n"
+ + "s.x = 'b'\n");
+ }
+
+ public void testStructDictMembersAreImmutable() {
+ checkError("can only assign to variables, not to 's.x['b']'",
+ "s = struct(x = {'a' : 1})\n"
+ + "s.x['b'] = 2\n");
+ }
+
+ public void testTupleAssign() throws Exception {
+ checkError("unsupported operand type(s) for +: 'list' and 'dict'",
+ "d = (1, 2)\n"
+ + "d[0] = 2\n");
+ }
+
+ public void testAssignOnNonCollection() throws Exception {
+ checkError("unsupported operand type(s) for +: 'string' and 'dict'",
+ "d = 'abc'\n"
+ + "d[0] = 2");
+ }
+
+ public void testNsetBadRightOperand() throws Exception {
+ checkError("can only concatenate nested sets with other nested sets or list of items, "
+ + "not 'string'", "set() + 'a'");
+ }
+
+ public void testNsetBadItemType() throws Exception {
+ checkError("bad nested set: incompatible generic variable types int with string",
+ "(set() + ['a']) + [1]");
+ }
+
+ public void testNsetBadNestedItemType() throws Exception {
+ checkError("bad nested set: incompatible generic variable types int with string",
+ "(set() + ['b']) + (set() + [1])");
+ }
+
+ public void testTypeInferenceForMethodLibraryFunction() throws Exception {
+ checkError("bad variable 'l': string is incompatible with int at /some/file.txt:2:3",
+ "def foo():\n"
+ + " l = len('abc')\n"
+ + " l = 'a'");
+ }
+
+ public void testListLiteralBadTypes() throws Exception {
+ checkError("bad list literal: int is incompatible with string at /some/file.txt:1:1",
+ "['a', 1]");
+ }
+
+ public void testTupleLiteralWorksForDifferentTypes() throws Exception {
+ parse("('a', 1)");
+ }
+
+ public void testDictLiteralBadKeyTypes() throws Exception {
+ checkError("bad dict literal: int is incompatible with string at /some/file.txt:1:1",
+ "{'a': 1, 1: 2}");
+ }
+
+ public void testDictLiteralDifferentValueTypeWorks() throws Exception {
+ parse("{'a': 1, 'b': 'c'}");
+ }
+
+ public void testListConcatBadTypes() throws Exception {
+ checkError("bad list concatenation: incompatible generic variable types int with string",
+ "['a'] + [1]");
+ }
+
+ public void testDictConcatBadKeyTypes() throws Exception {
+ checkError("bad dict concatenation: incompatible generic variable types int with string",
+ "{'a': 1} + {1: 2}");
+ }
+
+ public void testDictLiteralBadKeyType() throws Exception {
+ checkError("Dict cannot contain composite type 'list' as key", "{['a']: 1}");
+ }
+
+ public void testAndTypeInfer() throws Exception {
+ checkError("unsupported operand type(s) for +: 'string' and 'int'", "('a' and 'b') + 1");
+ }
+
+ public void testOrTypeInfer() throws Exception {
+ checkError("unsupported operand type(s) for +: 'string' and 'int'", "('' or 'b') + 1");
+ }
+
+ public void testAndDifferentTypes() throws Exception {
+ checkError("bad and operator: int is incompatible with string at /some/file.txt:1:1",
+ "'ab' and 3");
+ }
+
+ public void testOrDifferentTypes() throws Exception {
+ checkError("bad or operator: int is incompatible with string at /some/file.txt:1:1",
+ "'ab' or 3");
+ }
+
+ public void testOrNone() throws Exception {
+ parse("a = None or 3");
+ }
+
+ public void testNoneAssignment() throws Exception {
+ parse("def func():\n"
+ + " a = None\n"
+ + " a = 2\n"
+ + " a = None\n");
+ }
+
+ public void testNoneAssignmentError() throws Exception {
+ checkError("bad variable 'a': string is incompatible with int at /some/file.txt",
+ "def func():\n"
+ + " a = None\n"
+ + " a = 2\n"
+ + " a = None\n"
+ + " a = 'b'\n");
+ }
+
+ public void testDictComprehensionNotOnList() throws Exception {
+ checkError("Dict comprehension elements must be a list", "{k : k for k in 'abc'}");
+ }
+
+ public void testTypeInferenceForUserDefinedFunction() throws Exception {
+ checkError("bad variable 'a': string is incompatible with int at /some/file.txt",
+ "def func():\n"
+ + " return 'a'\n"
+ + "def foo():\n"
+ + " a = 1\n"
+ + " a = func()\n");
+ }
+
+ public void testCallingNonFunction() {
+ checkError("a is not a function",
+ "a = '1':\n"
+ + "a()\n");
+ }
+
+ public void testFuncallArgument() {
+ checkError("unsupported operand type(s) for +: 'int' and 'string'",
+ "def foo(x): return x\n"
+ + "a = foo(1 + 'a')");
+ }
+
+ // Skylark built-in functions specific tests
+
+ public void testTypeInferenceForSkylarkBuiltinGlobalFunction() throws Exception {
+ checkError("bad variable 'a': string is incompatible with function at /some/file.txt:3:3",
+ "def impl(ctx): return None\n"
+ + "def foo():\n"
+ + " a = rule(impl)\n"
+ + " a = 'a'\n");
+ }
+
+ public void testTypeInferenceForSkylarkBuiltinObjectFunction() throws Exception {
+ checkError("bad variable 'a': string is incompatible with Attribute at /some/file.txt",
+ "def foo():\n"
+ + " a = attr.int()\n"
+ + " a = 'a'\n");
+ }
+
+ public void testFuncReturningDictAssignmentAsLValue() throws Exception {
+ checkError("can only assign to variables, not to 'dict([])['b']'",
+ "def dict():\n"
+ + " return {'a': 1}\n"
+ + "def func():\n"
+ + " dict()['b'] = 2\n"
+ + " return d\n");
+ }
+
+ public void testListIndexAsLValue() {
+ checkError("unsupported operand type(s) for +: 'list' and 'dict'",
+ "def func():\n"
+ + " l = [1]\n"
+ + " l[0] = 2\n"
+ + " return l\n");
+ }
+
+ public void testStringIndexAsLValue() {
+ checkError("unsupported operand type(s) for +: 'string' and 'dict'",
+ "def func():\n"
+ + " s = 'abc'\n"
+ + " s[0] = 'd'\n"
+ + " return s\n");
+ }
+
+ public void testEmptyLiteralGenericIsSetInLaterConcatWorks() {
+ parse("def func():\n"
+ + " s = {}\n"
+ + " s['a'] = 'b'\n");
+ }
+
+ public void testTypeIsInferredForStructs() {
+ checkError("unsupported operand type(s) for +: 'struct' and 'string'",
+ "(struct(a = 1) + struct(b = 1)) + 'x'");
+ }
+
+ public void testReadOnlyWorksForSimpleBranching() {
+ parse("if 1:\n"
+ + " v = 'a'\n"
+ + "else:\n"
+ + " v = 'b'");
+ }
+
+ public void testReadOnlyWorksForNestedBranching() {
+ parse("if 1:\n"
+ + " if 0:\n"
+ + " v = 'a'\n"
+ + " else:\n"
+ + " v = 'b'\n"
+ + "else:\n"
+ + " if 0:\n"
+ + " v = 'c'\n"
+ + " else:\n"
+ + " v = 'd'\n");
+ }
+
+ public void testTypeCheckWorksForSimpleBranching() {
+ checkError("bad variable 'v': int is incompatible with string at /some/file.txt:2:3",
+ "if 1:\n"
+ + " v = 'a'\n"
+ + "else:\n"
+ + " v = 1");
+ }
+
+ public void testTypeCheckWorksForNestedBranching() {
+ checkError("bad variable 'v': int is incompatible with string at /some/file.txt:5:5",
+ "if 1:\n"
+ + " v = 'a'\n"
+ + "else:\n"
+ + " if 0:\n"
+ + " v = 'b'\n"
+ + " else:\n"
+ + " v = 1\n");
+ }
+
+ public void testTypeCheckWorksForDifferentLevelBranches() {
+ checkError("bad variable 'v': int is incompatible with string at /some/file.txt:2:3",
+ "if 1:\n"
+ + " v = 'a'\n"
+ + "else:\n"
+ + " if 0:\n"
+ + " v = 1\n");
+ }
+
+ public void testReadOnlyWorksForDifferentLevelBranches() {
+ checkError("Variable v is read only",
+ "if 1:\n"
+ + " if 1:\n"
+ + " v = 'a'\n"
+ + " v = 'b'\n");
+ }
+
+ public void testReadOnlyWorksWithinSimpleBranch() {
+ checkError("Variable v is read only",
+ "if 1:\n"
+ + " v = 'a'\n"
+ + "else:\n"
+ + " v = 'b'\n"
+ + " v = 'c'\n");
+ }
+
+ public void testReadOnlyWorksWithinNestedBranch() {
+ checkError("Variable v is read only",
+ "if 1:\n"
+ + " v = 'a'\n"
+ + "else:\n"
+ + " if 1:\n"
+ + " v = 'b'\n"
+ + " else:\n"
+ + " v = 'c'\n"
+ + " v = 'd'\n");
+ }
+
+ public void testReadOnlyWorksAfterSimpleBranch() {
+ checkError("Variable v is read only",
+ "if 1:\n"
+ + " v = 'a'\n"
+ + "else:\n"
+ + " w = 'a'\n"
+ + "v = 'b'");
+ }
+
+ public void testReadOnlyWorksAfterNestedBranch() {
+ checkError("Variable v is read only",
+ "if 1:\n"
+ + " if 1:\n"
+ + " v = 'a'\n"
+ + "v = 'b'");
+ }
+
+ public void testReadOnlyWorksAfterNestedBranch2() {
+ checkError("Variable v is read only",
+ "if 1:\n"
+ + " v = 'a'\n"
+ + "else:\n"
+ + " if 0:\n"
+ + " w = 1\n"
+ + "v = 'b'\n");
+ }
+
+ public void testModulesReadOnlyInFuncDefBody() {
+ checkError("Variable cmd_helper is read only",
+ "def func():",
+ " cmd_helper = set()");
+ }
+
+ public void testBuiltinGlobalFunctionsReadOnlyInFuncDefBody() {
+ checkError("Variable rule is read only",
+ "def func():",
+ " rule = 'abc'");
+ }
+
+ public void testBuiltinGlobalFunctionsReadOnlyAsFuncDefArg() {
+ checkError("Variable rule is read only",
+ "def func(rule):",
+ " return rule");
+ }
+
+ public void testFilesModulePlusStringErrorMessage() throws Exception {
+ checkError("unsupported operand type(s) for +: 'cmd_helper (a language module)' and 'string'",
+ "cmd_helper += 'a'");
+ }
+
+ public void testFunctionReturnsFunction() {
+ parse(
+ "def impl(ctx):",
+ " return None",
+ "",
+ "skylark_rule = rule(implementation = impl)",
+ "",
+ "def macro(name):",
+ " skylark_rule(name = name)");
+ }
+
+ public void testTypeForBooleanLiterals() {
+ parse("len([1, 2]) == 0 and True");
+ parse("len([1, 2]) == 0 and False");
+ }
+
+ public void testLoadRelativePathOneSegment() throws Exception {
+ parse("load('extension', 'a')\n");
+ }
+
+ public void testLoadAbsolutePathMultipleSegments() throws Exception {
+ parse("load('/pkg/extension', 'a')\n");
+ }
+
+ public void testLoadRelativePathMultipleSegments() throws Exception {
+ checkError("Path 'pkg/extension.bzl' is not valid. It should either start with "
+ + "a slash or refer to a file in the current directory.",
+ "load('pkg/extension', 'a')\n");
+ }
+
+ private void parse(String... lines) {
+ parseFileForSkylark(Joiner.on("\n").join(lines));
+ syntaxEvents.assertNoEvents();
+ }
+
+ private void checkError(String errorMsg, String... lines) {
+ syntaxEvents.setFailFast(false);
+ parseFileForSkylark(Joiner.on("\n").join(lines));
+ syntaxEvents.assertContainsEvent(errorMsg);
+ }
+}