| // 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.assertNotNull; |
| import static org.junit.Assert.fail; |
| |
| 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 org.junit.Before; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| import org.junit.runners.JUnit4; |
| |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.Map; |
| |
| /** |
| * A test class for functions and scoping. |
| */ |
| @RunWith(JUnit4.class) |
| 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)); |
| |
| @Before |
| public void setUp() throws Exception { |
| |
| env = new SkylarkEnvironment(syntaxEvents.collector()); |
| } |
| |
| @Test |
| 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); |
| } |
| |
| @Test |
| 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"); |
| } |
| |
| @Test |
| 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); |
| } |
| |
| @Test |
| 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")); |
| } |
| |
| @Test |
| 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")); |
| } |
| |
| @Test |
| 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")); |
| } |
| |
| @Test |
| 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."); |
| } |
| } |
| |
| @Test |
| 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") |
| @Test |
| 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(); |
| } |
| |
| @Test |
| 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")); |
| } |
| |
| @Test |
| 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")); |
| } |
| |
| @Test |
| 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")); |
| } |
| |
| @Test |
| 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(); |
| } |
| |
| @Test |
| 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")); |
| } |
| |
| @Test |
| 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")); |
| } |
| |
| @Test |
| 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")); |
| } |
| |
| @Test |
| 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") |
| @Test |
| 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); |
| } |
| |
| @Test |
| 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")); |
| } |
| |
| @Test |
| 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")); |
| } |
| |
| @Test |
| 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"; |
| } |
| |
| @Test |
| 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")); |
| } |
| |
| @Test |
| 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")); |
| } |
| |
| @Test |
| 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()"); |
| } |
| |
| @Test |
| 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")); |
| } |
| |
| @Test |
| public void testKwargsBadKey() throws Exception { |
| checkError("Keywords must be strings, not int", |
| "def func(a, b):", |
| " return a + b", |
| "func('a', **{3: 1})"); |
| } |
| |
| @Test |
| public void testKwargsIsNotDict() throws Exception { |
| checkError("Argument after ** must be a dictionary, not int", |
| "def func(a, b):", |
| " return a + b", |
| "func('a', **42)"); |
| } |
| |
| @Test |
| 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'})"); |
| } |
| |
| @Test |
| 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'})"); |
| } |
| |
| @Test |
| 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")); |
| } |
| |
| @Test |
| 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")); |
| } |
| |
| @Test |
| 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()); |
| } |
| } |
| } |