| // Copyright 2016 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 net.starlark.java.eval; | |
| import static com.google.common.truth.Truth.assertThat; | |
| import static org.junit.Assert.assertThrows; | |
| import com.google.auto.value.AutoValue; | |
| import java.lang.reflect.Method; | |
| import net.starlark.java.annot.StarlarkAnnotations; | |
| import net.starlark.java.annot.StarlarkBuiltin; | |
| import net.starlark.java.annot.StarlarkMethod; | |
| import org.junit.Test; | |
| import org.junit.runner.RunWith; | |
| import org.junit.runners.JUnit4; | |
| /** Test Starlark annotations and utilities. */ | |
| @RunWith(JUnit4.class) | |
| public class StarlarkAnnotationsTest { | |
| /** MockClassA */ | |
| @StarlarkBuiltin(name = "MockClassA", doc = "MockClassA") | |
| public static class MockClassA implements StarlarkValue { | |
| @StarlarkMethod(name = "foo", doc = "MockClassA#foo") | |
| public void foo() {} | |
| @StarlarkMethod(name = "bar", doc = "MockClassA#bar") | |
| public void bar() {} | |
| public void baz() {} | |
| } | |
| /** MockInterfaceB1 */ | |
| @StarlarkBuiltin(name = "MockInterfaceB1", doc = "MockInterfaceB1") | |
| public static interface MockInterfaceB1 extends StarlarkValue { | |
| @StarlarkMethod(name = "foo", doc = "MockInterfaceB1#foo") | |
| void foo(); | |
| @StarlarkMethod(name = "bar", doc = "MockInterfaceB1#bar") | |
| void bar(); | |
| @StarlarkMethod(name = "baz", doc = "MockInterfaceB1#baz") | |
| void baz(); | |
| } | |
| /** MockInterfaceB2 */ | |
| @StarlarkBuiltin(name = "MockInterfaceB2", doc = "MockInterfaceB2") | |
| public static interface MockInterfaceB2 extends StarlarkValue { | |
| @StarlarkMethod(name = "baz", doc = "MockInterfaceB2#baz") | |
| void baz(); | |
| @StarlarkMethod(name = "qux", doc = "MockInterfaceB2#qux") | |
| void qux(); | |
| } | |
| /** MockClassC */ | |
| @StarlarkBuiltin(name = "MockClassC", doc = "MockClassC") | |
| public static class MockClassC extends MockClassA implements MockInterfaceB1, MockInterfaceB2 { | |
| @Override | |
| @StarlarkMethod(name = "foo", doc = "MockClassC#foo") | |
| public void foo() {} | |
| @Override | |
| public void bar() {} | |
| @Override | |
| public void baz() {} | |
| @Override | |
| public void qux() {} | |
| } | |
| /** MockClassD */ | |
| public static class MockClassD extends MockClassC {} | |
| /** | |
| * A mock class that implements two unrelated module interfaces. This is invalid as the Starlark | |
| * type of such an object is ambiguous. | |
| */ | |
| public static class ImplementsTwoUnrelatedInterfaceModules | |
| implements MockInterfaceB1, MockInterfaceB2 { | |
| @Override | |
| public void foo() {} | |
| @Override | |
| public void bar() {} | |
| @Override | |
| public void baz() {} | |
| @Override | |
| public void qux() {} | |
| } | |
| /** ClassAModule test class */ | |
| @StarlarkBuiltin(name = "ClassAModule", doc = "ClassAModule") | |
| public static class ClassAModule implements StarlarkValue {} | |
| /** ExtendsClassA test class */ | |
| public static class ExtendsClassA extends ClassAModule {} | |
| /** InterfaceBModule test interface */ | |
| @StarlarkBuiltin(name = "InterfaceBModule", doc = "InterfaceBModule") | |
| public static interface InterfaceBModule extends StarlarkValue {} | |
| /** ExtendsInterfaceB test interface */ | |
| public static interface ExtendsInterfaceB extends InterfaceBModule {} | |
| /** | |
| * A mock class which has two transitive superclasses ({@link ClassAModule} and {@link | |
| * InterfaceBModule})) which are unrelated modules. This is invalid as the Starlark type of such | |
| * an object is ambiguous. | |
| * | |
| * <p>In other words: AmbiguousClass -> ClassAModule AmbiguousClass -> InterfaceBModule ... but | |
| * ClassAModule and InterfaceBModule have no relation. | |
| */ | |
| public static class AmbiguousClass extends ExtendsClassA implements ExtendsInterfaceB {} | |
| /** SubclassOfBoth test interface */ | |
| @StarlarkBuiltin(name = "SubclassOfBoth", doc = "SubclassOfBoth") | |
| public static class SubclassOfBoth extends ExtendsClassA implements ExtendsInterfaceB {} | |
| /** | |
| * A mock class similar to {@link AmbiugousClass} in that it has two separate superclass-paths to | |
| * Starlark modules, but is resolvable. | |
| * | |
| * <p>Concretely: UnambiguousClass -> SubclassOfBoth UnambiguousClass -> InterfaceBModule | |
| * SubclassOfBoth -> InterfaceBModule | |
| * | |
| * <p>... so UnambiguousClass is of type SubclassOfBoth. | |
| */ | |
| public static class UnambiguousClass extends SubclassOfBoth implements ExtendsInterfaceB {} | |
| /** MockClassZ */ | |
| public static class MockClassZ { | |
| } | |
| @StarlarkBuiltin(name = "MockStruct", doc = "MockStruct", isStructType = true) | |
| public static interface MockStructInterface extends StarlarkValue {} | |
| // Extends MockStructInterface but not itself marked as assignableToStructType | |
| @StarlarkBuiltin(name = "MockStructChild", doc = "MockStructChild") | |
| public static interface MockStructChildInterface extends MockStructInterface {} | |
| public static class MockStructImpl implements MockStructInterface {} | |
| public static class MockStructChildImpl implements MockStructChildInterface {} | |
| // First ancestor (MockStructChildInterface) is not itself marked as assignableToStructType | |
| public static class MockStructChildAndMockStructImpl | |
| implements MockStructChildInterface, MockStructInterface {} | |
| // The tests for getStarlarkBuiltin() double as tests for getParentWithStarlarkBuiltin(), | |
| // since they share an implementation. | |
| @Test | |
| public void testGetStarlarkBuiltinBasic() throws Exception { | |
| // Normal case. | |
| StarlarkBuiltin ann = StarlarkAnnotations.getStarlarkBuiltin(MockClassA.class); | |
| Class<?> cls = StarlarkAnnotations.getParentWithStarlarkBuiltin(MockClassA.class); | |
| assertThat(ann).isNotNull(); | |
| assertThat(ann.doc()).isEqualTo("MockClassA"); | |
| assertThat(cls).isNotNull(); | |
| assertThat(cls).isEqualTo(MockClassA.class); | |
| } | |
| @Test | |
| public void testGetStarlarkBuiltinSubclass() throws Exception { | |
| // Subclass's annotation is used. | |
| StarlarkBuiltin ann = StarlarkAnnotations.getStarlarkBuiltin(MockClassC.class); | |
| Class<?> cls = StarlarkAnnotations.getParentWithStarlarkBuiltin(MockClassC.class); | |
| assertThat(ann).isNotNull(); | |
| assertThat(ann.doc()).isEqualTo("MockClassC"); | |
| assertThat(cls).isNotNull(); | |
| assertThat(cls).isEqualTo(MockClassC.class); | |
| } | |
| @Test | |
| public void testGetStarlarkBuiltinSubclassNoSubannotation() throws Exception { | |
| // Falls back on superclass's annotation. | |
| StarlarkBuiltin ann = StarlarkAnnotations.getStarlarkBuiltin(MockClassD.class); | |
| Class<?> cls = StarlarkAnnotations.getParentWithStarlarkBuiltin(MockClassD.class); | |
| assertThat(ann).isNotNull(); | |
| assertThat(ann.doc()).isEqualTo("MockClassC"); | |
| assertThat(cls).isNotNull(); | |
| assertThat(cls).isEqualTo(MockClassC.class); | |
| } | |
| @Test | |
| public void testGetStarlarkBuiltinNotFound() throws Exception { | |
| // Doesn't exist. | |
| StarlarkBuiltin ann = StarlarkAnnotations.getStarlarkBuiltin(MockClassZ.class); | |
| Class<?> cls = StarlarkAnnotations.getParentWithStarlarkBuiltin(MockClassZ.class); | |
| assertThat(ann).isNull(); | |
| assertThat(cls).isNull(); | |
| } | |
| @Test | |
| public void testGetStarlarkBuiltinAmbiguous() throws Exception { | |
| assertThrows( | |
| IllegalArgumentException.class, | |
| () -> StarlarkAnnotations.getStarlarkBuiltin(ImplementsTwoUnrelatedInterfaceModules.class)); | |
| } | |
| @Test | |
| public void testGetStarlarkBuiltinTransitivelyAmbiguous() throws Exception { | |
| assertThrows( | |
| IllegalArgumentException.class, | |
| () -> StarlarkAnnotations.getStarlarkBuiltin(AmbiguousClass.class)); | |
| } | |
| @Test | |
| public void testGetStarlarkBuiltinUnambiguousComplex() throws Exception { | |
| assertThat(StarlarkAnnotations.getStarlarkBuiltin(SubclassOfBoth.class)) | |
| .isEqualTo(SubclassOfBoth.class.getAnnotation(StarlarkBuiltin.class)); | |
| assertThat(StarlarkAnnotations.getStarlarkBuiltin(UnambiguousClass.class)) | |
| .isEqualTo(SubclassOfBoth.class.getAnnotation(StarlarkBuiltin.class)); | |
| } | |
| @Test | |
| public void testIsAssignableToStructType() throws Exception { | |
| assertThat(StarlarkAnnotations.isAssignableToStructType(MockStructInterface.class)).isTrue(); | |
| assertThat(StarlarkAnnotations.isAssignableToStructType(MockStructChildInterface.class)) | |
| .isTrue(); | |
| assertThat(StarlarkAnnotations.isAssignableToStructType(MockStructImpl.class)).isTrue(); | |
| assertThat(StarlarkAnnotations.isAssignableToStructType(MockStructChildImpl.class)).isTrue(); | |
| assertThat(StarlarkAnnotations.isAssignableToStructType(MockStructChildAndMockStructImpl.class)) | |
| .isTrue(); | |
| // Doesn't implement MockStructInterface. | |
| assertThat(StarlarkAnnotations.isAssignableToStructType(MockClassA.class)).isFalse(); | |
| } | |
| @Test | |
| public void testGetStarlarkCallableBasic() throws Exception { | |
| // Normal case. Ensure two-arg form is consistent with one-arg form. | |
| Method method = MockClassA.class.getMethod("foo"); | |
| StarlarkMethod ann = StarlarkAnnotations.getStarlarkMethod(method); | |
| assertThat(ann).isNotNull(); | |
| assertThat(ann.doc()).isEqualTo("MockClassA#foo"); | |
| StarlarkMethod ann2 = StarlarkAnnotations.getStarlarkMethod(MockClassA.class, method); | |
| assertThat(ann2).isEqualTo(ann); | |
| } | |
| @Test | |
| public void testGetStarlarkCallableSubclass() throws Exception { | |
| // Subclass's annotation is used. | |
| Method method = MockClassC.class.getMethod("foo"); | |
| StarlarkMethod ann = StarlarkAnnotations.getStarlarkMethod(method); | |
| assertThat(ann).isNotNull(); | |
| assertThat(ann.doc()).isEqualTo("MockClassC#foo"); | |
| } | |
| @Test | |
| public void testGetStarlarkCallableSubclassNoSubannotation() throws Exception { | |
| // Falls back on superclass's annotation. Superclass takes precedence over interface. | |
| Method method = MockClassC.class.getMethod("bar"); | |
| StarlarkMethod ann = StarlarkAnnotations.getStarlarkMethod(method); | |
| assertThat(ann).isNotNull(); | |
| assertThat(ann.doc()).isEqualTo("MockClassA#bar"); | |
| } | |
| @Test | |
| public void testGetStarlarkCallableTwoargForm() throws Exception { | |
| // Ensure that when passing superclass in directly, we bypass subclass's annotation. | |
| Method method = MockClassC.class.getMethod("foo"); | |
| StarlarkMethod ann = StarlarkAnnotations.getStarlarkMethod(MockClassA.class, method); | |
| assertThat(ann).isNotNull(); | |
| assertThat(ann.doc()).isEqualTo("MockClassA#foo"); | |
| } | |
| @Test | |
| public void testGetStarlarkCallableNotFound() throws Exception { | |
| // Null result when no annotation present... | |
| Method method = MockClassA.class.getMethod("baz"); | |
| StarlarkMethod ann = StarlarkAnnotations.getStarlarkMethod(method); | |
| assertThat(ann).isNull(); | |
| // ... including when it's only present in a subclass that was bypassed... | |
| method = MockClassC.class.getMethod("baz"); | |
| ann = StarlarkAnnotations.getStarlarkMethod(MockClassA.class, method); | |
| assertThat(ann).isNull(); | |
| // ... or when the method itself is only in the subclass that was bypassed. | |
| method = MockClassC.class.getMethod("qux"); | |
| ann = StarlarkAnnotations.getStarlarkMethod(MockClassA.class, method); | |
| assertThat(ann).isNull(); | |
| } | |
| @Test | |
| public void testGetStarlarkCallableInterface() throws Exception { | |
| // Search through parent interfaces. First interface takes priority. | |
| Method method = MockClassC.class.getMethod("baz"); | |
| StarlarkMethod ann = StarlarkAnnotations.getStarlarkMethod(method); | |
| assertThat(ann).isNotNull(); | |
| assertThat(ann.doc()).isEqualTo("MockInterfaceB1#baz"); | |
| // Make sure both are still traversed. | |
| method = MockClassC.class.getMethod("qux"); | |
| ann = StarlarkAnnotations.getStarlarkMethod(method); | |
| assertThat(ann).isNotNull(); | |
| assertThat(ann.doc()).isEqualTo("MockInterfaceB2#qux"); | |
| } | |
| @StarlarkBuiltin(name = "MockAbstractClass", doc = "MockAbstractClass") | |
| public abstract static class MockAbstractClass implements StarlarkValue { | |
| @StarlarkMethod(name = "abstract_method", doc = "abstract_method") | |
| public abstract String abstractMethod(); | |
| } | |
| @AutoValue | |
| public abstract static class MockAutoValueClass extends MockAbstractClass { | |
| public static MockAutoValueClass create() { | |
| return new AutoValue_StarlarkAnnotationsTest_MockAutoValueClass("abstractMethodVal"); | |
| } | |
| } | |
| @Test | |
| public void testAutoValueAnnotations() throws Exception { | |
| MockAutoValueClass val = MockAutoValueClass.create(); | |
| Class<?> clazz = val.getClass(); | |
| // The generated class itself should be recognized as a Starlark type, | |
| // getting the annotation from its superclass. | |
| StarlarkBuiltin builtin = StarlarkAnnotations.getStarlarkBuiltin(clazz); | |
| assertThat(builtin).isNotNull(); | |
| assertThat(builtin.name()).isEqualTo("MockAbstractClass"); | |
| // We should be able to retrieve the StarlarkMethod from the generated class's method. | |
| Method method = clazz.getMethod("abstractMethod"); | |
| StarlarkMethod methodAnnot = StarlarkAnnotations.getStarlarkMethod(method); | |
| assertThat(methodAnnot).isNotNull(); | |
| assertThat(methodAnnot.name()).isEqualTo("abstract_method"); | |
| } | |
| } |