| // Copyright 2014 The Bazel Authors. All rights reserved. | |
| // | |
| // Licensed under the Apache License, Version 2.0 (the "License"); | |
| // you may not use this file except in compliance with the License. | |
| // You may obtain a copy of the License at | |
| // | |
| // http://www.apache.org/licenses/LICENSE-2.0 | |
| // | |
| // Unless required by applicable law or agreed to in writing, software | |
| // distributed under the License is distributed on an "AS IS" BASIS, | |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| // See the License for the specific language governing permissions and | |
| // limitations under the License. | |
| package com.google.devtools.build.lib.syntax; | |
| import static com.google.common.truth.Truth.assertThat; | |
| import com.google.devtools.build.lib.syntax.util.EvaluationTestCase; | |
| import java.util.ArrayList; | |
| import java.util.List; | |
| import java.util.Map; | |
| import org.junit.Test; | |
| import org.junit.runner.RunWith; | |
| import org.junit.runners.JUnit4; | |
| /** | |
| * A test class for functions and scoping. | |
| */ | |
| @RunWith(JUnit4.class) | |
| public class FunctionTest extends EvaluationTestCase { | |
| @Test | |
| public void testFunctionDef() throws Exception { | |
| eval("def func(a,b,c):", | |
| " a = 1", | |
| " b = a\n"); | |
| UserDefinedFunction stmt = (UserDefinedFunction) lookup("func"); | |
| assertThat(stmt).isNotNull(); | |
| assertThat(stmt.getName()).isEqualTo("func"); | |
| assertThat(stmt.getSignature().getSignature().getShape().getMandatoryPositionals()) | |
| .isEqualTo(3); | |
| assertThat(stmt.getStatements()).hasSize(2); | |
| } | |
| @Test | |
| public void testFunctionDefDuplicateArguments() throws Exception { | |
| setFailFast(false); | |
| parseFile("def func(a,b,a):", | |
| " a = 1\n"); | |
| assertContainsError("duplicate parameter name in function definition"); | |
| } | |
| @Test | |
| public void testFunctionDefCallOuterFunc() throws Exception { | |
| List<Object> params = new ArrayList<>(); | |
| createOuterFunction(params); | |
| eval("def func(a):", | |
| " outer_func(a)", | |
| "func(1)", | |
| "func(2)"); | |
| assertThat(params).containsExactly(1, 2).inOrder(); | |
| } | |
| private void createOuterFunction(final List<Object> params) throws Exception { | |
| BaseFunction outerFunc = new BaseFunction("outer_func") { | |
| @Override | |
| public Object call(List<Object> args, Map<String, Object> kwargs, FuncallExpression ast, | |
| Environment env) throws EvalException, InterruptedException { | |
| params.addAll(args); | |
| return Runtime.NONE; | |
| } | |
| }; | |
| update("outer_func", outerFunc); | |
| } | |
| @Test | |
| public void testFunctionDefNoEffectOutsideScope() throws Exception { | |
| update("a", 1); | |
| eval("def func():", | |
| " a = 2", | |
| "func()\n"); | |
| assertThat(lookup("a")).isEqualTo(1); | |
| } | |
| @Test | |
| public void testFunctionDefGlobalVaribleReadInFunction() throws Exception { | |
| eval("a = 1", | |
| "def func():", | |
| " b = a", | |
| " return b", | |
| "c = func()\n"); | |
| assertThat(lookup("c")).isEqualTo(1); | |
| } | |
| @Test | |
| public void testFunctionDefLocalGlobalScope() throws Exception { | |
| eval("a = 1", | |
| "def func():", | |
| " a = 2", | |
| " b = a", | |
| " return b", | |
| "c = func()\n"); | |
| assertThat(lookup("c")).isEqualTo(2); | |
| } | |
| @Test | |
| public void testFunctionDefLocalVariableReferencedBeforeAssignment() throws Exception { | |
| checkEvalErrorContains( | |
| "local variable 'a' is referenced before assignment.", | |
| "a = 1", | |
| "def func():", | |
| " b = a", | |
| " a = 2", | |
| " return b", | |
| "c = func()\n"); | |
| } | |
| @Test | |
| public void testFunctionDefLocalVariableReferencedInCallBeforeAssignment() throws Exception { | |
| checkEvalErrorContains( | |
| "local variable 'a' is referenced before assignment.", | |
| "def dummy(x):", | |
| " pass", | |
| "a = 1", | |
| "def func():", | |
| " dummy(a)", | |
| " a = 2", | |
| "func()\n"); | |
| } | |
| @Test | |
| public void testFunctionDefLocalVariableReferencedAfterAssignment() throws Exception { | |
| eval("a = 1", | |
| "def func():", | |
| " a = 2", | |
| " b = a", | |
| " a = 3", | |
| " return b", | |
| "c = func()\n"); | |
| assertThat(lookup("c")).isEqualTo(2); | |
| } | |
| @SuppressWarnings("unchecked") | |
| @Test | |
| public void testSkylarkGlobalComprehensionIsAllowed() throws Exception { | |
| eval("a = [i for i in [1, 2, 3]]\n"); | |
| assertThat((Iterable<Object>) lookup("a")).containsExactly(1, 2, 3).inOrder(); | |
| } | |
| @Test | |
| public void testFunctionReturn() throws Exception { | |
| eval("def func():", | |
| " return 2", | |
| "b = func()\n"); | |
| assertThat(lookup("b")).isEqualTo(2); | |
| } | |
| @Test | |
| public void testFunctionReturnFromALoop() throws Exception { | |
| eval("def func():", | |
| " for i in [1, 2, 3, 4, 5]:", | |
| " return i", | |
| "b = func()\n"); | |
| assertThat(lookup("b")).isEqualTo(1); | |
| } | |
| @Test | |
| public void testFunctionExecutesProperly() throws Exception { | |
| eval("def func(a):", | |
| " b = 1", | |
| " if a:", | |
| " b = 2", | |
| " return b", | |
| "c = func(0)", | |
| "d = func(1)\n"); | |
| assertThat(lookup("c")).isEqualTo(1); | |
| assertThat(lookup("d")).isEqualTo(2); | |
| } | |
| @Test | |
| public void testFunctionCallFromFunction() throws Exception { | |
| final List<Object> params = new ArrayList<>(); | |
| createOuterFunction(params); | |
| eval("def func2(a):", | |
| " outer_func(a)", | |
| "def func1(b):", | |
| " func2(b)", | |
| "func1(1)", | |
| "func1(2)\n"); | |
| assertThat(params).containsExactly(1, 2).inOrder(); | |
| } | |
| @Test | |
| public void testFunctionCallFromFunctionReadGlobalVar() throws Exception { | |
| eval("a = 1", | |
| "def func2():", | |
| " return a", | |
| "def func1():", | |
| " return func2()", | |
| "b = func1()\n"); | |
| assertThat(lookup("b")).isEqualTo(1); | |
| } | |
| @Test | |
| public void testFunctionParamCanShadowGlobalVarAfterGlobalVarIsRead() throws Exception { | |
| eval("a = 1", | |
| "def func2(a):", | |
| " return 0", | |
| "def func1():", | |
| " dummy = a", | |
| " return func2(2)", | |
| "b = func1()\n"); | |
| assertThat(lookup("b")).isEqualTo(0); | |
| } | |
| @Test | |
| public void testSingleLineFunction() throws Exception { | |
| eval("def func(): return 'a'", | |
| "s = func()\n"); | |
| assertThat(lookup("s")).isEqualTo("a"); | |
| } | |
| @Test | |
| public void testFunctionReturnsDictionary() throws Exception { | |
| eval("def func(): return {'a' : 1}", | |
| "d = func()", | |
| "a = d['a']\n"); | |
| assertThat(lookup("a")).isEqualTo(1); | |
| } | |
| @Test | |
| public void testFunctionReturnsList() throws Exception { | |
| eval("def func(): return [1, 2, 3]", | |
| "d = func()", | |
| "a = d[1]\n"); | |
| assertThat(lookup("a")).isEqualTo(2); | |
| } | |
| @Test | |
| public void testFunctionNameAliasing() throws Exception { | |
| eval("def func(a):", | |
| " return a + 1", | |
| "alias = func", | |
| "r = alias(1)"); | |
| assertThat(lookup("r")).isEqualTo(2); | |
| } | |
| @Test | |
| public void testCallingFunctionsWithMixedModeArgs() throws Exception { | |
| eval("def func(a, b, c):", | |
| " return a + b + c", | |
| "v = func(1, c = 2, b = 3)"); | |
| assertThat(lookup("v")).isEqualTo(6); | |
| } | |
| 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 { | |
| eval(functionWithOptionalArgs(), | |
| "v1 = func('1', 1, 1)", | |
| "v2 = func(b = 2, a = '2', c = 2)", | |
| "v3 = func('3')", | |
| "v4 = func('4', c = 1)\n"); | |
| assertThat(lookup("v1")).isEqualTo("1abc"); | |
| assertThat(lookup("v2")).isEqualTo("2abc"); | |
| assertThat(lookup("v3")).isEqualTo("3a"); | |
| assertThat(lookup("v4")).isEqualTo("4ac"); | |
| } | |
| @Test | |
| public void testDefaultArguments() throws Exception { | |
| eval("def func(a, b = 'b', c = 'c'):", | |
| " return a + b + c", | |
| "v1 = func('a', 'x', 'y')", | |
| "v2 = func(b = 'x', a = 'a', c = 'y')", | |
| "v3 = func('a')", | |
| "v4 = func('a', c = 'y')\n"); | |
| assertThat(lookup("v1")).isEqualTo("axy"); | |
| assertThat(lookup("v2")).isEqualTo("axy"); | |
| assertThat(lookup("v3")).isEqualTo("abc"); | |
| assertThat(lookup("v4")).isEqualTo("aby"); | |
| } | |
| @Test | |
| public void testDefaultArgumentsInsufficientArgNum() throws Exception { | |
| checkEvalError("insufficient arguments received by func(a, b = \"b\", c = \"c\") " | |
| + "(got 0, expected at least 1)", | |
| "def func(a, b = 'b', c = 'c'):", | |
| " return a + b + c", | |
| "func()"); | |
| } | |
| @Test | |
| public void testArgsIsNotIterable() throws Exception { | |
| checkEvalError( | |
| "argument after * must be an iterable, not int", | |
| "def func1(a, b): return a + b", | |
| "func1('a', *42)"); | |
| checkEvalError( | |
| "argument after * must be an iterable, not string", | |
| "def func2(a, b): return a + b", | |
| "func2('a', *'str')"); | |
| } | |
| @Test | |
| public void testKeywordOnlyIsForbidden() throws Exception { | |
| checkEvalErrorContains("forbidden", "def foo(a, b, *, c): return a + b + c"); | |
| } | |
| @Test | |
| public void testParamAfterStarArgs() throws Exception { | |
| checkEvalErrorContains("forbidden", "def foo(a, *b, c): return a"); | |
| } | |
| @Test | |
| public void testKwargsBadKey() throws Exception { | |
| checkEvalError( | |
| "keywords must be strings, not 'int'", | |
| "def func(a, b): return a + b", | |
| "func('a', **{3: 1})"); | |
| } | |
| @Test | |
| public void testKwargsIsNotDict() throws Exception { | |
| checkEvalError( | |
| "argument after ** must be a dictionary, not 'int'", | |
| "def func(a, b): return a + b", | |
| "func('a', **42)"); | |
| } | |
| @Test | |
| public void testKwargsCollision() throws Exception { | |
| checkEvalError("argument 'b' passed both by position and by name in call to func(a, b)", | |
| "def func(a, b): return a + b", | |
| "func('a', 'b', **{'b': 'foo'})"); | |
| } | |
| @Test | |
| public void testKwargsCollisionWithNamed() throws Exception { | |
| checkEvalError("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 { | |
| eval("a = 2", | |
| "def foo(x=a): return x", | |
| "def bar():", | |
| " a = 3", | |
| " return foo()", | |
| "v = bar()\n"); | |
| assertThat(lookup("v")).isEqualTo(2); | |
| } | |
| @Test | |
| public void testMixingPositionalOptional() throws Exception { | |
| eval("def f(name, value = '', optional = ''): return value", | |
| "v = f('name', 'value')\n"); | |
| assertThat(lookup("v")).isEqualTo("value"); | |
| } | |
| @Test | |
| public void testStarArg() throws Exception { | |
| eval( | |
| "def f(name, value = '1', optional = '2'): return name + value + optional", | |
| "v1 = f(*['name', 'value'])", | |
| "v2 = f('0', *['name', 'value'])", | |
| "v3 = f('0', optional = '3', *['b'])", | |
| "v4 = f(name='a', *[])\n"); | |
| assertThat(lookup("v1")).isEqualTo("namevalue2"); | |
| assertThat(lookup("v2")).isEqualTo("0namevalue"); | |
| assertThat(lookup("v3")).isEqualTo("0b3"); | |
| assertThat(lookup("v4")).isEqualTo("a12"); | |
| } | |
| @Test | |
| public void testStarParam() throws Exception { | |
| eval("def f(name, value = '1', optional = '2', *rest):", | |
| " r = name + value + optional + '|'", | |
| " for x in rest: r += x", | |
| " return r", | |
| "v1 = f('a', 'b', 'c', 'd', 'e')", | |
| "v2 = f('a', optional='b', value='c')", | |
| "v3 = f('a')"); | |
| assertThat(lookup("v1")).isEqualTo("abc|de"); | |
| assertThat(lookup("v2")).isEqualTo("acb|"); | |
| assertThat(lookup("v3")).isEqualTo("a12|"); | |
| } | |
| } |