blob: f33b500c320399d5f36bda691656a946f22b5634 [file] [log] [blame]
// 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 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.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 org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
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.)
*/
@RunWith(JUnit4.class)
public class EvaluationTest extends AbstractEvaluationTestCase {
protected Environment env;
@Before
public void setUp() throws Exception {
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);
}
@Test
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'");
}
@Test
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)"));
}
@Test
public void testStringFormatMultipleArgs() throws Exception {
assertEquals("XYZ", eval("'%sY%s' % ('X', 'Z')"));
}
@Test
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"));
}
@Test
public void testNot() throws Exception {
assertEquals(false, eval("not 1"));
assertEquals(true, eval("not ''"));
}
@Test
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)"));
}
@Test
public void testNotWithArithmeticOperators() throws Exception {
assertEquals(true, eval("not 0 + 0"));
assertEquals(false, eval("not 2 - 1"));
}
@Test
public void testNotWithCollections() throws Exception {
assertEquals(true, eval("not []"));
assertEquals(false, eval("not {'a' : 1}"));
}
@Test
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"));
}
@Test
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]"));
}
@Test
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"));
}
@Test
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'"));
}
@Test
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'"));
}
@Test
public void testCompareStringInt() throws Exception {
checkEvalError("'a' >= 1", "Cannot compare string with int");
}
@Test
public void testNotComparable() throws Exception {
checkEvalError("[1, 2] < [1, 3]", "[1, 2] is not comparable");
}
@Test
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));
}
@Test
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));
}
@Test
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"));
}
@Test
public void testConcatStrings() throws Exception {
assertEquals("foobar", eval("'foo' + 'bar'"));
}
@Test
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")
@Test
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")
@Test
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)
}
@Test
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']}"));
}
@Test
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']}"));
}
@Test
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());
}
@Test
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");
}
@Test
public void testListComprehensionFailsOnNonSequence() throws Exception {
checkEvalError("[x + 1 for x in 123]", "type 'int' is not an iterable");
}
@SuppressWarnings("unchecked")
@Test
public void testListComprehensionOnString() throws Exception {
assertThat((Iterable<Object>) eval("[x for x in 'abc']")).containsExactly("a", "b", "c")
.inOrder();
}
@Test
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'");
}
@Test
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));
}
@Test
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());
}
@Test
public void testInOnListContains() throws Exception {
assertEquals(Boolean.TRUE, eval("'b' in ['a', 'b']"));
}
@Test
public void testInOnListDoesNotContain() throws Exception {
assertEquals(Boolean.FALSE, eval("'c' in ['a', 'b']"));
}
@Test
public void testInOnTupleContains() throws Exception {
assertEquals(Boolean.TRUE, eval("'b' in ('a', 'b')"));
}
@Test
public void testInOnTupleDoesNotContain() throws Exception {
assertEquals(Boolean.FALSE, eval("'c' in ('a', 'b')"));
}
@Test
public void testInOnDictContains() throws Exception {
assertEquals(Boolean.TRUE, eval("'b' in {'a' : 1, 'b' : 2}"));
}
@Test
public void testInOnDictDoesNotContainKey() throws Exception {
assertEquals(Boolean.FALSE, eval("'c' in {'a' : 1, 'b' : 2}"));
}
@Test
public void testInOnDictDoesNotContainVal() throws Exception {
assertEquals(Boolean.FALSE, eval("1 in {'a' : 1, 'b' : 2}"));
}
@Test
public void testInOnStringContains() throws Exception {
assertEquals(Boolean.TRUE, eval("'b' in 'abc'"));
}
@Test
public void testInOnStringDoesNotContain() throws Exception {
assertEquals(Boolean.FALSE, eval("'d' in 'abc'"));
}
@Test
public void testInOnStringLeftNotString() throws Exception {
checkEvalError("1 in '123'",
"in operator only works on strings if the left operand is also a string");
}
@Test
public void testInFailsOnNonIterable() throws Exception {
checkEvalError("'a' in 1",
"in operator only works on lists, tuples, dictionaries and strings");
}
@Test
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";
}
};
}
@Test
public void testPercOnObject() throws Exception {
env.update("obj", createObjWithStr());
assertEquals("str marker", eval("'%s' % obj", env));
}
@Test
public void testPercOnObjectList() throws Exception {
env.update("obj", createObjWithStr());
assertEquals("str marker str marker", eval("'%s %s' % (obj, obj)", env));
}
@Test
public void testPercOnObjectInvalidFormat() throws Exception {
env.update("obj", createObjWithStr());
checkEvalError("'%d' % obj", env, "invalid arguments for format string");
}
@SuppressWarnings("unchecked")
@Test
public void testDictKeys() throws Exception {
exec("v = {'a': 1}.keys() + ['b', 'c']", env);
assertThat((Iterable<Object>) env.lookup("v")).containsExactly("a", "b", "c").inOrder();
}
@Test
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());
}
}
}