diff options
author | 2015-02-26 13:39:28 +0000 | |
---|---|---|
committer | 2015-02-26 13:39:28 +0000 | |
commit | 89f012dd8b5c75573668c0a5a984d814da31c46f (patch) | |
tree | 74d4c305f67ab2be73d18e22eb7597e8da6ec588 /src/test/java/com/google/devtools | |
parent | 5a4f28664237fd5d53273c791f5f2decbf27d45b (diff) |
Open source all the tests under lib/syntax/.
--
MOS_MIGRATED_REVID=87244284
Diffstat (limited to 'src/test/java/com/google/devtools')
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); + } +} |