| // Copyright 2017 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.packages; | |
| import static com.google.common.truth.Truth.assertThat; | |
| import static org.junit.Assert.assertThrows; | |
| import static org.mockito.Mockito.mock; | |
| import static org.mockito.Mockito.verifyNoInteractions; | |
| import com.google.common.collect.ImmutableList; | |
| import com.google.common.collect.ImmutableMap; | |
| import com.google.common.testing.EqualsTester; | |
| import com.google.devtools.build.lib.cmdline.Label; | |
| import net.starlark.java.eval.Dict; | |
| import net.starlark.java.eval.EvalException; | |
| import net.starlark.java.eval.Mutability; | |
| import net.starlark.java.eval.Starlark; | |
| import net.starlark.java.eval.StarlarkCallable; | |
| import net.starlark.java.eval.StarlarkInt; | |
| import net.starlark.java.eval.StarlarkSemantics; | |
| import net.starlark.java.eval.StarlarkThread; | |
| import net.starlark.java.eval.Tuple; | |
| import net.starlark.java.syntax.Location; | |
| import org.junit.Test; | |
| import org.junit.runner.RunWith; | |
| import org.junit.runners.JUnit4; | |
| /** Tests for {@link StarlarkProvider}. */ | |
| @RunWith(JUnit4.class) | |
| public final class StarlarkProviderTest { | |
| @Test | |
| public void unexportedProvider_accessors() { | |
| StarlarkProvider provider = StarlarkProvider.builder(Location.BUILTIN).build(); | |
| assertThat(provider.isExported()).isFalse(); | |
| assertThat(provider.getName()).isEqualTo("<no name>"); | |
| assertThat(provider.getPrintableName()).isEqualTo("<no name>"); | |
| assertThat(provider.createRawConstructor().getName()).isEqualTo("<raw constructor>"); | |
| assertThat(provider.getErrorMessageForUnknownField("foo")) | |
| .isEqualTo("'struct' value has no field or method 'foo'"); | |
| assertThat(provider.isImmutable()).isFalse(); | |
| assertThat(Starlark.repr(provider)).isEqualTo("<provider>"); | |
| assertThrows(IllegalStateException.class, provider::getKey); | |
| } | |
| @Test | |
| public void exportedProvider_accessors() throws Exception { | |
| StarlarkProvider.Key key = | |
| new StarlarkProvider.Key(Label.parseCanonical("//foo:bar.bzl"), "prov"); | |
| StarlarkProvider provider = StarlarkProvider.builder(Location.BUILTIN).setExported(key).build(); | |
| assertThat(provider.isExported()).isTrue(); | |
| assertThat(provider.getName()).isEqualTo("prov"); | |
| assertThat(provider.getPrintableName()).isEqualTo("prov"); | |
| assertThat(provider.createRawConstructor().getName()).isEqualTo("<raw constructor for prov>"); | |
| assertThat(provider.getErrorMessageForUnknownField("foo")) | |
| .isEqualTo("'prov' value has no field or method 'foo'"); | |
| assertThat(provider.isImmutable()).isTrue(); | |
| assertThat(Starlark.repr(provider)).isEqualTo("<provider>"); | |
| assertThat(provider.getKey()).isEqualTo(key); | |
| } | |
| @Test | |
| public void basicInstantiation() throws Exception { | |
| StarlarkProvider provider = StarlarkProvider.builder(Location.BUILTIN).build(); | |
| StarlarkInfo infoFromNormalConstructor = instantiateWithA1B2C3(provider); | |
| assertHasExactlyValuesA1B2C3(infoFromNormalConstructor); | |
| assertThat(infoFromNormalConstructor.getProvider()).isEqualTo(provider); | |
| StarlarkInfo infoFromRawConstructor = instantiateWithA1B2C3(provider.createRawConstructor()); | |
| assertHasExactlyValuesA1B2C3(infoFromRawConstructor); | |
| assertThat(infoFromRawConstructor.getProvider()).isEqualTo(provider); | |
| assertThat(infoFromNormalConstructor).isEqualTo(infoFromRawConstructor); | |
| } | |
| @Test | |
| public void instantiationWithInit() throws Exception { | |
| StarlarkProvider provider = StarlarkProvider.builder(Location.BUILTIN).setInit(initBC).build(); | |
| StarlarkInfo infoFromNormalConstructor = instantiateWithA1(provider); | |
| assertHasExactlyValuesA1B2C3(infoFromNormalConstructor); | |
| assertThat(infoFromNormalConstructor.getProvider()).isEqualTo(provider); | |
| } | |
| @Test | |
| public void instantiationWithInitSignatureMismatch_fails() throws Exception { | |
| StarlarkProvider provider = StarlarkProvider.builder(Location.BUILTIN).setInit(initBC).build(); | |
| EvalException e = assertThrows(EvalException.class, () -> instantiateWithA1B2C3(provider)); | |
| assertThat(e).hasMessageThat().contains("expected a single `a` argument"); | |
| } | |
| @Test | |
| public void instantiationWithInitReturnTypeMismatch_fails() throws Exception { | |
| StarlarkCallable initWithInvalidReturnType = | |
| new StarlarkCallable() { | |
| @Override | |
| public Object call(StarlarkThread thread, Tuple args, Dict<String, Object> kwargs) { | |
| return "invalid"; | |
| } | |
| @Override | |
| public String getName() { | |
| return "initWithInvalidReturnType"; | |
| } | |
| @Override | |
| public Location getLocation() { | |
| return Location.BUILTIN; | |
| } | |
| }; | |
| StarlarkProvider provider = | |
| StarlarkProvider.builder(Location.BUILTIN).setInit(initWithInvalidReturnType).build(); | |
| EvalException e = assertThrows(EvalException.class, () -> instantiateWithA1B2C3(provider)); | |
| assertThat(e) | |
| .hasMessageThat() | |
| .contains("got string for 'return value of provider init()', want dict"); | |
| } | |
| @Test | |
| public void instantiationWithFailingInit_fails() throws Exception { | |
| StarlarkCallable failingInit = | |
| new StarlarkCallable() { | |
| @Override | |
| public Object call(StarlarkThread thread, Tuple args, Dict<String, Object> kwargs) | |
| throws EvalException { | |
| throw Starlark.errorf("failingInit fails"); | |
| } | |
| @Override | |
| public String getName() { | |
| return "failingInit"; | |
| } | |
| @Override | |
| public Location getLocation() { | |
| return Location.BUILTIN; | |
| } | |
| }; | |
| StarlarkProvider provider = | |
| StarlarkProvider.builder(Location.BUILTIN).setInit(failingInit).build(); | |
| EvalException e = assertThrows(EvalException.class, () -> instantiateWithA1B2C3(provider)); | |
| assertThat(e).hasMessageThat().contains("failingInit fails"); | |
| } | |
| @Test | |
| public void rawConstructorBypassesInit() throws Exception { | |
| StarlarkCallable init = mock(StarlarkCallable.class, "init"); | |
| StarlarkProvider provider = StarlarkProvider.builder(Location.BUILTIN).setInit(init).build(); | |
| StarlarkInfo infoFromRawConstructor = instantiateWithA1B2C3(provider.createRawConstructor()); | |
| assertHasExactlyValuesA1B2C3(infoFromRawConstructor); | |
| assertThat(infoFromRawConstructor.getProvider()).isEqualTo(provider); | |
| verifyNoInteractions(init); | |
| } | |
| @Test | |
| public void basicInstantiationWithSchemaWithSomeFieldsUnset() throws Exception { | |
| StarlarkProvider provider = | |
| StarlarkProvider.builder(Location.BUILTIN) | |
| .setSchema(ImmutableList.of("a", "b", "c")) | |
| .build(); | |
| StarlarkInfo infoFromNormalConstructor = instantiateWithA1(provider); | |
| assertHasExactlyValuesA1(infoFromNormalConstructor); | |
| StarlarkInfo infoFromRawConstructor = instantiateWithA1(provider.createRawConstructor()); | |
| assertHasExactlyValuesA1(infoFromRawConstructor); | |
| } | |
| @Test | |
| public void basicInstantiationWithSchemaWithAllFieldsSet() throws Exception { | |
| StarlarkProvider provider = | |
| StarlarkProvider.builder(Location.BUILTIN) | |
| .setSchema(ImmutableList.of("a", "b", "c")) | |
| .build(); | |
| StarlarkInfo infoFromNormalConstructor = instantiateWithA1B2C3(provider); | |
| assertHasExactlyValuesA1B2C3(infoFromNormalConstructor); | |
| StarlarkInfo infoFromRawConstructor = instantiateWithA1B2C3(provider.createRawConstructor()); | |
| assertHasExactlyValuesA1B2C3(infoFromRawConstructor); | |
| } | |
| @Test | |
| public void schemaDisallowsUnexpectedFields() throws Exception { | |
| StarlarkProvider provider = | |
| StarlarkProvider.builder(Location.BUILTIN).setSchema(ImmutableList.of("a", "b")).build(); | |
| EvalException e = assertThrows(EvalException.class, () -> instantiateWithA1B2C3(provider)); | |
| assertThat(e) | |
| .hasMessageThat() | |
| .contains("got unexpected field 'c' in call to instantiate provider"); | |
| } | |
| @Test | |
| public void schemaEnforcedOnRawConstructor() throws Exception { | |
| StarlarkProvider provider = | |
| StarlarkProvider.builder(Location.BUILTIN).setSchema(ImmutableList.of("a", "b")).build(); | |
| EvalException e = | |
| assertThrows( | |
| EvalException.class, () -> instantiateWithA1B2C3(provider.createRawConstructor())); | |
| assertThat(e) | |
| .hasMessageThat() | |
| .contains("got unexpected field 'c' in call to instantiate provider"); | |
| } | |
| @Test | |
| public void schemaEnforcedOnInit() throws Exception { | |
| StarlarkProvider provider = | |
| StarlarkProvider.builder(Location.BUILTIN) | |
| .setSchema(ImmutableList.of("a", "b")) | |
| .setInit(initBC) | |
| .build(); | |
| EvalException e = assertThrows(EvalException.class, () -> instantiateWithA1(provider)); | |
| assertThat(e) | |
| .hasMessageThat() | |
| .contains("got unexpected field 'c' in call to instantiate provider"); | |
| } | |
| @Test | |
| public void schemalessProvider_getFields() throws Exception { | |
| StarlarkProvider provider = StarlarkProvider.builder(Location.BUILTIN).build(); | |
| assertThat(provider.getFields()).isNull(); | |
| } | |
| @Test | |
| public void schemafulProvider_getFields() throws Exception { | |
| StarlarkProvider provider = | |
| StarlarkProvider.builder(Location.BUILTIN) | |
| .setSchema(ImmutableList.of("a", "b", "c")) | |
| .build(); | |
| assertThat(provider.getFields()).containsExactly("a", "b", "c").inOrder(); | |
| } | |
| @Test | |
| public void providerEquals() throws Exception { | |
| // All permutations of differing label and differing name. | |
| StarlarkProvider.Key keyFooA = | |
| new StarlarkProvider.Key(Label.parseCanonical("//foo.bzl"), "provA"); | |
| StarlarkProvider.Key keyFooB = | |
| new StarlarkProvider.Key(Label.parseCanonical("//foo.bzl"), "provB"); | |
| StarlarkProvider.Key keyBarA = | |
| new StarlarkProvider.Key(Label.parseCanonical("//bar.bzl"), "provA"); | |
| StarlarkProvider.Key keyBarB = | |
| new StarlarkProvider.Key(Label.parseCanonical("//bar.bzl"), "provB"); | |
| // 1 for each key, plus a duplicate for one of the keys, plus 2 that have no key. | |
| StarlarkProvider provFooA1 = | |
| StarlarkProvider.builder(Location.BUILTIN).setExported(keyFooA).build(); | |
| StarlarkProvider provFooA2 = | |
| StarlarkProvider.builder(Location.BUILTIN).setExported(keyFooA).build(); | |
| StarlarkProvider provFooB = | |
| StarlarkProvider.builder(Location.BUILTIN).setExported(keyFooB).build(); | |
| StarlarkProvider provBarA = | |
| StarlarkProvider.builder(Location.BUILTIN).setExported(keyBarA).build(); | |
| StarlarkProvider provBarB = | |
| StarlarkProvider.builder(Location.BUILTIN).setExported(keyBarB).build(); | |
| StarlarkProvider provUnexported1 = StarlarkProvider.builder(Location.BUILTIN).build(); | |
| StarlarkProvider provUnexported2 = StarlarkProvider.builder(Location.BUILTIN).build(); | |
| // For exported providers, different keys -> unequal, same key -> equal. For unexported | |
| // providers it comes down to object identity. | |
| new EqualsTester() | |
| .addEqualityGroup(provFooA1, provFooA2) | |
| .addEqualityGroup(provFooB) | |
| .addEqualityGroup(provBarA, provBarA) // reflexive equality (exported) | |
| .addEqualityGroup(provBarB) | |
| .addEqualityGroup(provUnexported1, provUnexported1) // reflexive equality (unexported) | |
| .addEqualityGroup(provUnexported2) | |
| .testEquals(); | |
| } | |
| /** Custom init equivalent to `def initBC(a): return {a:a, b:a*2, c:a*3}` */ | |
| private static final StarlarkCallable initBC = | |
| new StarlarkCallable() { | |
| @Override | |
| public Object call(StarlarkThread thread, Tuple args, Dict<String, Object> kwargs) | |
| throws EvalException { | |
| if (!args.isEmpty()) { | |
| throw Starlark.errorf("unexpected positional arguments"); | |
| } | |
| if (kwargs.size() != 1 || !kwargs.containsKey("a")) { | |
| throw Starlark.errorf("expected a single `a` argument"); | |
| } | |
| StarlarkInt a = (StarlarkInt) kwargs.get("a"); | |
| return Dict.builder() | |
| .put("a", a) | |
| .put("b", StarlarkInt.of(a.toIntUnchecked() * 2)) | |
| .put("c", StarlarkInt.of(a.toIntUnchecked() * 3)) | |
| .build(Mutability.IMMUTABLE); | |
| } | |
| @Override | |
| public String getName() { | |
| return "initBC"; | |
| } | |
| @Override | |
| public Location getLocation() { | |
| return Location.BUILTIN; | |
| } | |
| }; | |
| /** Instantiates a {@link StarlarkInfo} with fields a=1 (and nothing else). */ | |
| private static StarlarkInfo instantiateWithA1(StarlarkCallable provider) throws Exception { | |
| try (Mutability mu = Mutability.create()) { | |
| StarlarkThread thread = new StarlarkThread(mu, StarlarkSemantics.DEFAULT); | |
| Object result = | |
| Starlark.call( | |
| thread, | |
| provider, | |
| /*args=*/ ImmutableList.of(), | |
| /*kwargs=*/ ImmutableMap.of("a", StarlarkInt.of(1))); | |
| assertThat(result).isInstanceOf(StarlarkInfo.class); | |
| return (StarlarkInfo) result; | |
| } | |
| } | |
| /** Instantiates a {@link StarlarkInfo} with fields a=1, b=2, c=3 (and nothing else). */ | |
| private static StarlarkInfo instantiateWithA1B2C3(StarlarkCallable provider) throws Exception { | |
| try (Mutability mu = Mutability.create()) { | |
| StarlarkThread thread = new StarlarkThread(mu, StarlarkSemantics.DEFAULT); | |
| Object result = | |
| Starlark.call( | |
| thread, | |
| provider, | |
| /*args=*/ ImmutableList.of(), | |
| /*kwargs=*/ ImmutableMap.of( | |
| "a", StarlarkInt.of(1), "b", StarlarkInt.of(2), "c", StarlarkInt.of(3))); | |
| assertThat(result).isInstanceOf(StarlarkInfo.class); | |
| return (StarlarkInfo) result; | |
| } | |
| } | |
| /** Asserts that a {@link StarlarkInfo} has field a=1 (and nothing else). */ | |
| private static void assertHasExactlyValuesA1(StarlarkInfo info) throws Exception { | |
| assertThat(info.getFieldNames()).containsExactly("a"); | |
| assertThat(info.getValue("a")).isEqualTo(StarlarkInt.of(1)); | |
| } | |
| /** Asserts that a {@link StarlarkInfo} has fields a=1, b=2, c=3 (and nothing else). */ | |
| private static void assertHasExactlyValuesA1B2C3(StarlarkInfo info) throws Exception { | |
| assertThat(info.getFieldNames()).containsExactly("a", "b", "c"); | |
| assertThat(info.getValue("a")).isEqualTo(StarlarkInt.of(1)); | |
| assertThat(info.getValue("b")).isEqualTo(StarlarkInt.of(2)); | |
| assertThat(info.getValue("c")).isEqualTo(StarlarkInt.of(3)); | |
| } | |
| } |