blob: 3d186b75262bcee4a593715b18d518a245f460f3 [file] [log] [blame]
// 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 com.google.devtools.build.lib.syntax;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import com.google.devtools.build.lib.skylarkinterface.SkylarkCallable;
import com.google.devtools.build.lib.skylarkinterface.SkylarkInterfaceUtils;
import com.google.devtools.build.lib.skylarkinterface.SkylarkModule;
import java.lang.reflect.Method;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/** Test Skylark interface annotations and utilities. */
@RunWith(JUnit4.class)
public class SkylarkInterfaceUtilsTest {
/** MockClassA */
@SkylarkModule(name = "MockClassA", doc = "MockClassA")
public static class MockClassA implements StarlarkValue {
@SkylarkCallable(name = "foo", doc = "MockClassA#foo")
public void foo() {}
@SkylarkCallable(name = "bar", doc = "MockClassA#bar")
public void bar() {}
public void baz() {}
}
/** MockInterfaceB1 */
@SkylarkModule(name = "MockInterfaceB1", doc = "MockInterfaceB1")
public static interface MockInterfaceB1 extends StarlarkValue {
@SkylarkCallable(name = "foo", doc = "MockInterfaceB1#foo")
void foo();
@SkylarkCallable(name = "bar", doc = "MockInterfaceB1#bar")
void bar();
@SkylarkCallable(name = "baz", doc = "MockInterfaceB1#baz")
void baz();
}
/** MockInterfaceB2 */
@SkylarkModule(name = "MockInterfaceB2", doc = "MockInterfaceB2")
public static interface MockInterfaceB2 extends StarlarkValue {
@SkylarkCallable(name = "baz", doc = "MockInterfaceB2#baz")
void baz();
@SkylarkCallable(name = "qux", doc = "MockInterfaceB2#qux")
void qux();
}
/** MockClassC */
@SkylarkModule(name = "MockClassC", doc = "MockClassC")
public static class MockClassC extends MockClassA implements MockInterfaceB1, MockInterfaceB2 {
@Override
@SkylarkCallable(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 {
@Override
@SkylarkCallable(name = "foo", doc = "MockClassD#foo")
public void foo() {}
}
/**
* A mock class that implements two unrelated module interfaces. This is invalid as the skylark
* 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 */
@SkylarkModule(name = "ClassAModule", doc = "ClassAModule")
public static class ClassAModule implements StarlarkValue {}
/** ExtendsClassA test class */
public static class ExtendsClassA extends ClassAModule {}
/** InterfaceBModule test interface */
@SkylarkModule(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 skylark type
* of such an object is ambiguous.
*
* 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 */
@SkylarkModule(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 skylark modules, but is resolvable.
*
* Concretely:
* UnambiguousClass -> SubclassOfBoth
* UnambiguousClass -> InterfaceBModule
* SubclassOfBoth -> InterfaceBModule
*
* ... so UnambiguousClass is of type SubclassOfBoth.
*/
public static class UnambiguousClass extends SubclassOfBoth implements ExtendsInterfaceB {}
/** MockClassZ */
public static class MockClassZ {
}
// The tests for getSkylarkModule() double as tests for getParentWithSkylarkModule(),
// since they share an implementation.
@Test
public void testGetSkylarkModuleBasic() throws Exception {
// Normal case.
SkylarkModule ann = SkylarkInterfaceUtils.getSkylarkModule(MockClassA.class);
Class<?> cls = SkylarkInterfaceUtils.getParentWithSkylarkModule(MockClassA.class);
assertThat(ann).isNotNull();
assertThat(ann.doc()).isEqualTo("MockClassA");
assertThat(cls).isNotNull();
assertThat(cls).isEqualTo(MockClassA.class);
}
@Test
public void testGetSkylarkModuleSubclass() throws Exception {
// Subclass's annotation is used.
SkylarkModule ann = SkylarkInterfaceUtils.getSkylarkModule(MockClassC.class);
Class<?> cls = SkylarkInterfaceUtils.getParentWithSkylarkModule(MockClassC.class);
assertThat(ann).isNotNull();
assertThat(ann.doc()).isEqualTo("MockClassC");
assertThat(cls).isNotNull();
assertThat(cls).isEqualTo(MockClassC.class);
}
@Test
public void testGetSkylarkModuleSubclassNoSubannotation() throws Exception {
// Falls back on superclass's annotation.
SkylarkModule ann = SkylarkInterfaceUtils.getSkylarkModule(MockClassD.class);
Class<?> cls = SkylarkInterfaceUtils.getParentWithSkylarkModule(MockClassD.class);
assertThat(ann).isNotNull();
assertThat(ann.doc()).isEqualTo("MockClassC");
assertThat(cls).isNotNull();
assertThat(cls).isEqualTo(MockClassC.class);
}
@Test
public void testGetSkylarkModuleNotFound() throws Exception {
// Doesn't exist.
SkylarkModule ann = SkylarkInterfaceUtils.getSkylarkModule(MockClassZ.class);
Class<?> cls = SkylarkInterfaceUtils.getParentWithSkylarkModule(MockClassZ.class);
assertThat(ann).isNull();
assertThat(cls).isNull();
}
@Test
public void testGetSkylarkModuleAmbiguous() throws Exception {
assertThrows(IllegalArgumentException.class,
() -> SkylarkInterfaceUtils.getSkylarkModule(ImplementsTwoUnrelatedInterfaceModules.class));
}
@Test
public void testGetSkylarkModuleTransitivelyAmbiguous() throws Exception {
assertThrows(IllegalArgumentException.class,
() -> SkylarkInterfaceUtils.getSkylarkModule(AmbiguousClass.class));
}
@Test
public void testGetSkylarkModuleUnambiguousComplex() throws Exception {
assertThat(SkylarkInterfaceUtils.getSkylarkModule(SubclassOfBoth.class))
.isEqualTo(SubclassOfBoth.class.getAnnotation(SkylarkModule.class));
assertThat(SkylarkInterfaceUtils.getSkylarkModule(UnambiguousClass.class))
.isEqualTo(SubclassOfBoth.class.getAnnotation(SkylarkModule.class));
}
@Test
public void testGetSkylarkCallableBasic() throws Exception {
// Normal case. Ensure two-arg form is consistent with one-arg form.
Method method = MockClassA.class.getMethod("foo");
SkylarkCallable ann = SkylarkInterfaceUtils.getSkylarkCallable(method);
assertThat(ann).isNotNull();
assertThat(ann.doc()).isEqualTo("MockClassA#foo");
SkylarkCallable ann2 = SkylarkInterfaceUtils.getSkylarkCallable(MockClassA.class, method);
assertThat(ann2).isEqualTo(ann);
}
@Test
public void testGetSkylarkCallableSubclass() throws Exception {
// Subclass's annotation is used.
Method method = MockClassC.class.getMethod("foo");
SkylarkCallable ann = SkylarkInterfaceUtils.getSkylarkCallable(method);
assertThat(ann).isNotNull();
assertThat(ann.doc()).isEqualTo("MockClassC#foo");
}
@Test
public void testGetSkylarkCallableSubclassNoSubannotation() throws Exception {
// Falls back on superclass's annotation. Superclass takes precedence over interface.
Method method = MockClassC.class.getMethod("bar");
SkylarkCallable ann = SkylarkInterfaceUtils.getSkylarkCallable(method);
assertThat(ann).isNotNull();
assertThat(ann.doc()).isEqualTo("MockClassA#bar");
}
@Test
public void testGetSkylarkCallableTwoargForm() throws Exception {
// Ensure that when passing superclass in directly, we bypass subclass's annotation.
Method method = MockClassC.class.getMethod("foo");
SkylarkCallable ann = SkylarkInterfaceUtils.getSkylarkCallable(MockClassA.class, method);
assertThat(ann).isNotNull();
assertThat(ann.doc()).isEqualTo("MockClassA#foo");
}
@Test
public void testGetSkylarkCallableNotFound() throws Exception {
// Null result when no annotation present...
Method method = MockClassA.class.getMethod("baz");
SkylarkCallable ann = SkylarkInterfaceUtils.getSkylarkCallable(method);
assertThat(ann).isNull();
// ... including when it's only present in a subclass that was bypassed...
method = MockClassC.class.getMethod("baz");
ann = SkylarkInterfaceUtils.getSkylarkCallable(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 = SkylarkInterfaceUtils.getSkylarkCallable(MockClassA.class, method);
assertThat(ann).isNull();
}
@Test
public void testGetSkylarkCallableInterface() throws Exception {
// Search through parent interfaces. First interface takes priority.
Method method = MockClassC.class.getMethod("baz");
SkylarkCallable ann = SkylarkInterfaceUtils.getSkylarkCallable(method);
assertThat(ann).isNotNull();
assertThat(ann.doc()).isEqualTo("MockInterfaceB1#baz");
// Make sure both are still traversed.
method = MockClassC.class.getMethod("qux");
ann = SkylarkInterfaceUtils.getSkylarkCallable(method);
assertThat(ann).isNotNull();
assertThat(ann.doc()).isEqualTo("MockInterfaceB2#qux");
}
}