| // Copyright 2006 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 static com.google.common.truth.Truth.assertWithMessage; |
| import static com.google.devtools.build.lib.testutil.MoreAsserts.assertThrows; |
| |
| import com.google.devtools.build.lib.syntax.util.EvaluationTestCase; |
| import java.util.Arrays; |
| import java.util.Map; |
| import java.util.TreeMap; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| import org.junit.runners.JUnit4; |
| |
| /** Tests of the argument processing of {@code Starlark.matchSignature}. */ |
| // TODO(adonovan): rename. |
| @RunWith(JUnit4.class) |
| public final class BaseFunctionTest extends EvaluationTestCase { |
| |
| private void checkFunction(StarlarkCallable fn, String callExpression, String expectedOutput) |
| throws Exception { |
| initialize(); |
| update(fn.getName(), fn); |
| |
| if (expectedOutput.charAt(0) == '[') { // a tuple => expected to pass |
| assertWithMessage("Wrong output for " + callExpression) |
| .that(eval(callExpression).toString()) |
| .isEqualTo(expectedOutput); |
| |
| } else { // expected to fail with an exception |
| EvalException e = assertThrows(EvalException.class, () -> eval(callExpression)); |
| assertWithMessage("Wrong exception for " + callExpression) |
| .that(e.getMessage()) |
| .isEqualTo(expectedOutput); |
| } |
| } |
| |
| // TODO(adonovan): redesign this test so that inputs and expected outputs are adjacent. |
| private static final String[] BASE_FUNCTION_EXPRESSIONS = { |
| "mixed()", |
| "mixed(1)", |
| "mixed(1, 2)", |
| "mixed(1, 2, 3)", |
| "mixed(1, 2, wiz=3, quux=4)", |
| "mixed(foo=1)", |
| "mixed(bar=2)", |
| "mixed(foo=1, bar=2)", |
| "mixed(bar=2, foo=1)", |
| "mixed(2, foo=1)", |
| "mixed(bar=2, foo=1, wiz=3)", |
| }; |
| |
| public void checkFunctions( |
| boolean onlyNamedArguments, String expectedSignature, String... expectedResults) |
| throws Exception { |
| FunctionSignature sig = |
| onlyNamedArguments |
| ? FunctionSignature.namedOnly(1, "foo", "bar") |
| : FunctionSignature.of(1, "foo", "bar"); |
| // This test uses BaseFunction only for its 'repr' implementation. |
| // The meat of this test exercises only StarlarkCallable. |
| // TODO(adonovan): make it easier to get repr correct and eliminate BaseFunction here. |
| BaseFunction func = |
| new BaseFunction() { |
| @Override |
| public String getName() { |
| return "mixed"; |
| } |
| |
| @Override |
| public FunctionSignature getSignature() { |
| return sig; |
| } |
| |
| @Override |
| public Object fastcall(StarlarkThread thread, Object[] positional, Object[] named) |
| throws EvalException { |
| Object[] arguments = |
| Starlark.matchSignature( |
| sig, this, /*defaults=*/ null, thread.mutability(), positional, named); |
| return Arrays.asList(arguments); |
| } |
| }; |
| |
| assertThat(func.toString()).isEqualTo(expectedSignature); |
| |
| for (int i = 0; i < BASE_FUNCTION_EXPRESSIONS.length; i++) { |
| String expr = BASE_FUNCTION_EXPRESSIONS[i]; |
| String expected = expectedResults[i]; |
| checkFunction(func, expr, expected); |
| } |
| } |
| |
| @Test |
| public void testNoSurplusArguments() throws Exception { |
| checkFunctions( |
| false, |
| "mixed(foo, bar = ?)", |
| "insufficient arguments received by mixed(foo, bar = ?) (got 0, expected at least 1)", |
| "[1, null]", |
| "[1, 2]", |
| "too many (3) positional arguments in call to mixed(foo, bar = ?)", |
| "unexpected keywords 'quux', 'wiz' in call to mixed(foo, bar = ?)", |
| "[1, null]", |
| "missing mandatory positional argument 'foo' while calling mixed(foo, bar = ?)", |
| "[1, 2]", |
| "[1, 2]", |
| "mixed(foo, bar = ?) got multiple values for parameter 'foo'", |
| "unexpected keyword 'wiz' in call to mixed(foo, bar = ?)"); |
| } |
| |
| @Test |
| public void testOnlyNamedArguments() throws Exception { |
| checkFunctions( |
| true, |
| "mixed(*, foo, bar = ?)", |
| "missing mandatory keyword arguments in call to mixed(*, foo, bar = ?)", |
| "mixed(*, foo, bar = ?) does not accept positional arguments, but got 1", |
| "mixed(*, foo, bar = ?) does not accept positional arguments, but got 2", |
| "mixed(*, foo, bar = ?) does not accept positional arguments, but got 3", |
| "mixed(*, foo, bar = ?) does not accept positional arguments, but got 2", |
| "[1, null, null]", |
| "missing mandatory named-only argument 'foo' while calling mixed(*, foo, bar = ?)", |
| "[1, 2, null]", |
| "[1, 2, null]", |
| "mixed(*, foo, bar = ?) does not accept positional arguments, but got 1", |
| "unexpected keyword 'wiz' in call to mixed(*, foo, bar = ?)"); |
| } |
| |
| @SuppressWarnings("unchecked") |
| @Test |
| public void testKwParam() throws Exception { |
| exec( |
| "def foo(a, b, c=3, d=4, g=7, h=8, *args, **kwargs):\n" |
| + " return (a, b, c, d, g, h, args, kwargs)\n" |
| + "v1 = foo(1, 2)\n" |
| + "v2 = foo(1, h=9, i=0, *['x', 'y', 'z', 't'])\n" |
| + "v3 = foo(1, i=0, *[2, 3, 4, 5, 6, 7, 8])\n" |
| + "def bar(**kwargs):\n" |
| + " return kwargs\n" |
| + "b1 = bar(name='foo', type='jpg', version=42)\n" |
| + "b2 = bar()\n"); |
| |
| assertThat(Starlark.repr(lookup("v1"))).isEqualTo("(1, 2, 3, 4, 7, 8, (), {})"); |
| assertThat(Starlark.repr(lookup("v2"))) |
| .isEqualTo("(1, \"x\", \"y\", \"z\", \"t\", 9, (), {\"i\": 0})"); |
| assertThat(Starlark.repr(lookup("v3"))).isEqualTo("(1, 2, 3, 4, 5, 6, (7, 8), {\"i\": 0})"); |
| |
| // NB: the conversion to a TreeMap below ensures the keys are sorted. |
| assertThat(Starlark.repr(new TreeMap<String, Object>((Map<String, Object>) lookup("b1")))) |
| .isEqualTo("{\"name\": \"foo\", \"type\": \"jpg\", \"version\": 42}"); |
| assertThat(Starlark.repr(lookup("b2"))).isEqualTo("{}"); |
| } |
| |
| @Test |
| public void testTrailingCommas() throws Exception { |
| // Test that trailing commas are allowed in function definitions and calls |
| // even after last *args or **kwargs expressions, like python3 |
| exec( |
| "def f(*args, **kwargs): pass\n" |
| + "v1 = f(1,)\n" |
| + "v2 = f(*(1,2),)\n" |
| + "v3 = f(a=1,)\n" |
| + "v4 = f(**{\"a\": 1},)\n"); |
| |
| assertThat(Starlark.repr(lookup("v1"))).isEqualTo("None"); |
| assertThat(Starlark.repr(lookup("v2"))).isEqualTo("None"); |
| assertThat(Starlark.repr(lookup("v3"))).isEqualTo("None"); |
| assertThat(Starlark.repr(lookup("v4"))).isEqualTo("None"); |
| } |
| } |