blob: 14e8f374913fe0c9ad7a6d8e462891a28dc7bc2c [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.android.desugar;
import static com.google.common.truth.Truth.assertThat;
import static java.lang.reflect.Modifier.isFinal;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.Assert.fail;
import com.google.common.collect.ImmutableList;
import com.google.common.io.ByteStreams;
import com.google.devtools.build.android.desugar.testdata.CaptureLambda;
import com.google.devtools.build.android.desugar.testdata.ConcreteFunction;
import com.google.devtools.build.android.desugar.testdata.ConstructorReference;
import com.google.devtools.build.android.desugar.testdata.GuavaLambda;
import com.google.devtools.build.android.desugar.testdata.InnerClassLambda;
import com.google.devtools.build.android.desugar.testdata.InterfaceWithLambda;
import com.google.devtools.build.android.desugar.testdata.Lambda;
import com.google.devtools.build.android.desugar.testdata.LambdaInOverride;
import com.google.devtools.build.android.desugar.testdata.MethodReference;
import com.google.devtools.build.android.desugar.testdata.MethodReferenceInSubclass;
import com.google.devtools.build.android.desugar.testdata.MethodReferenceSuperclass;
import com.google.devtools.build.android.desugar.testdata.OuterReferenceLambda;
import com.google.devtools.build.android.desugar.testdata.SpecializedFunction;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.concurrent.Callable;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/**
* Test that exercises classes in the {@code testdata} package. This is meant to be run against a
* desugared version of those classes, which in turn exercise various desugaring features.
*/
@RunWith(JUnit4.class)
public class DesugarFunctionalTest {
private final int expectedBridgesFromSameTarget;
private final int expectedBridgesFromSeparateTarget;
private final boolean expectLambdaMethodsInInterfaces;
public DesugarFunctionalTest() {
this(3, 1, false);
}
/** Constructor for testing desugar while allowing default and static interface methods. */
protected DesugarFunctionalTest(
boolean expectBridgesFromSeparateTarget, boolean expectDefaultMethods) {
this(
expectDefaultMethods ? 0 : 3,
expectBridgesFromSeparateTarget ? 1 : 0,
expectDefaultMethods);
}
private DesugarFunctionalTest(int bridgesFromSameTarget, int bridgesFromSeparateTarget,
boolean lambdaMethodsInInterfaces) {
this.expectedBridgesFromSameTarget = bridgesFromSameTarget;
this.expectedBridgesFromSeparateTarget = bridgesFromSeparateTarget;
this.expectLambdaMethodsInInterfaces = lambdaMethodsInInterfaces;
}
@Test
public void testGuavaLambda() {
GuavaLambda lambdaUse = new GuavaLambda(ImmutableList.of("Sergey", "Larry", "Alex"));
assertThat(lambdaUse.as()).containsExactly("Alex");
}
@Test
public void testJavaLambda() {
Lambda lambdaUse = new Lambda(ImmutableList.of("Sergey", "Larry", "Alex"));
assertThat(lambdaUse.as()).containsExactly("Alex");
}
@Test
public void testLambdaForIntersectionType() throws Exception {
assertThat(Lambda.hello().call()).isEqualTo("hello");
}
@Test
public void testCapturingLambda() {
CaptureLambda lambdaUse = new CaptureLambda(ImmutableList.of("Sergey", "Larry", "Alex"));
assertThat(lambdaUse.prefixed("L")).containsExactly("Larry");
}
@Test
public void testOuterReferenceLambda() throws Exception {
OuterReferenceLambda lambdaUse = new OuterReferenceLambda(ImmutableList.of("Sergey", "Larry"));
assertThat(lambdaUse.filter(ImmutableList.of("Larry", "Alex"))).containsExactly("Larry");
assertThat(
isFinal(
OuterReferenceLambda.class
.getDeclaredMethod("lambda$filter$0$OuterReferenceLambda", String.class)
.getModifiers()))
.isTrue();
}
/**
* Tests a lambda in a subclass whose generated lambda$ method has the same name and signature
* as a lambda$ method generated by Javac in a superclass and both of these methods are used
* in the implementation of the subclass (by calling super). Naively this leads to wrong
* behavior (in this case, return a non-empty list) because the lambda$ in the superclass is never
* used once its made non-private during desugaring.
*/
@Test
public void testOuterReferenceLambdaInOverride() throws Exception {
OuterReferenceLambda lambdaUse = new LambdaInOverride(ImmutableList.of("Sergey", "Larry"));
assertThat(lambdaUse.filter(ImmutableList.of("Larry", "Alex"))).isEmpty();
assertThat(
isFinal(
LambdaInOverride.class
.getDeclaredMethod("lambda$filter$0$LambdaInOverride", String.class)
.getModifiers()))
.isTrue();
}
@Test
public void testLambdaInAnonymousClassReferencesSurroundingMethodParameter() throws Exception {
assertThat(Lambda.mult(21).apply(2).call()).isEqualTo(42);
}
/** Tests a lambda that accesses a method parameter across 2 nested anonymous classes. */
@Test
public void testLambdaInNestedAnonymousClass() throws Exception {
InnerClassLambda lambdaUse = new InnerClassLambda(ImmutableList.of("Sergey", "Larry"));
assertThat(lambdaUse.prefixFilter("L").apply(ImmutableList.of("Lois", "Larry")).call())
.containsExactly("Larry");
}
@Test
public void testClassMethodReference() {
MethodReference methodrefUse = new MethodReference(ImmutableList.of("Sergey", "Larry", "Alex"));
StringBuilder dest = new StringBuilder();
methodrefUse.appendAll(dest);
assertThat(dest.toString()).isEqualTo("SergeyLarryAlex");
}
// Regression test for b/33378312
@Test
public void testHiddenMethodReference() {
MethodReference methodrefUse = new MethodReference(ImmutableList.of("Sergey", "Larry", "Alex"));
assertThat(methodrefUse.intersect(ImmutableList.of("Alex", "Sundar"))).containsExactly("Alex");
}
// Regression test for b/33378312
@Test
public void testHiddenStaticMethodReference() {
MethodReference methodrefUse =
new MethodReference(ImmutableList.of("Sergey", "Larry", "Sundar"));
assertThat(methodrefUse.some()).containsExactly("Sergey", "Sundar");
}
@Test
public void testDuplicateHiddenMethodReference() {
MethodReference methodrefUse = new MethodReference(ImmutableList.of("Sergey", "Larry", "Alex"));
assertThat(methodrefUse.onlyIn(ImmutableList.of("Alex", "Sundar"))).containsExactly("Sundar");
}
// Regression test for b/36201257
@Test
public void testMethodReferenceThatNeedsBridgeInSubclass() {
MethodReferenceInSubclass methodrefUse =
new MethodReferenceInSubclass(ImmutableList.of("Sergey", "Larry", "Alex"));
assertThat(methodrefUse.containsE()).containsExactly("Sergey", "Alex");
assertThat(methodrefUse.startsWithL()).containsExactly("Larry");
// Test sanity: make sure sub- and superclass have bridge methods with matching descriptors but
// different names
Method superclassBridge = findOnlyBridge(MethodReferenceSuperclass.class);
Method subclassBridge = findOnlyBridge(MethodReferenceInSubclass.class);
assertThat(superclassBridge.getName()).isNotEqualTo(subclassBridge.getName());
assertThat(superclassBridge.getParameterTypes()).isEqualTo(subclassBridge.getParameterTypes());
}
private Method findOnlyBridge(Class<?> clazz) {
Method result = null;
for (Method m : clazz.getDeclaredMethods()) {
if (m.getName().startsWith("bridge$")) {
assertThat(result).named(m.getName()).isNull();
result = m;
}
}
assertThat(result).named(clazz.getSimpleName()).isNotNull();
return result;
}
// Regression test for b/33378312
@Test
public void testThrowingPrivateMethodReference() throws Exception {
MethodReference methodrefUse = new MethodReference(ImmutableList.of("Sergey", "Larry"));
Callable<?> stringer = methodrefUse.stringer();
try {
stringer.call();
fail("IOException expected");
} catch (IOException expected) {
assertThat(expected).hasMessage("SergeyLarry");
} catch (Exception e) {
throw e;
}
}
// Regression test for b/33304582
@Test
public void testInterfaceMethodReference() {
MethodReference methodrefUse = new MethodReference(ImmutableList.of("Sergey", "Larry", "Alex"));
MethodReference.Transformer<String> transform = new MethodReference.Transformer<String>() {
@Override
public String transform(String input) {
return input.substring(1);
}
};
assertThat(methodrefUse.transform(transform)).containsExactly("ergey", "arry", "lex");
}
@Test
public void testConstructorReference() {
ConstructorReference initRefUse = new ConstructorReference(ImmutableList.of("1", "2", "42"));
assertThat(initRefUse.toInt()).containsExactly(1, 2, 42);
}
// Regression test for b/33304582
@Test
public void testPrivateConstructorReference() {
ConstructorReference initRefUse = ConstructorReference.singleton().apply("17");
assertThat(initRefUse.toInt()).containsExactly(17);
}
// This test is similar to testPrivateConstructorReference but the private constructor of an inner
// class is used as a method reference. That causes Javac to generate a bridge constructor and
// a lambda body method that calls it, so the desugaring step doesn't need to do anything to make
// the private constructor visible. This is mostly to double-check that we don't interfere with
// this "already-working" scenario.
@Test
public void testPrivateConstructorAccessedThroughJavacGeneratedBridge() {
try {
@SuppressWarnings("unused") // local is needed to make ErrorProne happy
ConstructorReference unused = ConstructorReference.emptyThroughJavacGeneratedBridge().get();
fail("RuntimeException expected");
} catch (RuntimeException expected) {
assertThat(expected).hasMessage("got it!");
}
}
@Test
public void testExpressionMethodReference() {
assertThat(
MethodReference.stringChars(new StringBuilder().append("Larry").append("Sergey"))
.apply(5))
.isEqualTo('S');
}
@Test
public void testFieldMethodReference() {
MethodReference methodrefUse = new MethodReference(ImmutableList.of("Sergey", "Larry", "Alex"));
assertThat(methodrefUse.toPredicate().test("Larry")).isTrue();
assertThat(methodrefUse.toPredicate().test("Sundar")).isFalse();
}
@Test
public void testConcreteFunctionWithInheritedBridgeMethods() {
assertThat(new ConcreteFunction().apply("1234567890987654321")).isEqualTo(1234567890987654321L);
assertThat(ConcreteFunction.parseAll(ImmutableList.of("5", "17"), new ConcreteFunction()))
.containsExactly(5L, 17L);
}
@Test
public void testLambdaWithInheritedBridgeMethods() throws Exception {
assertThat(ConcreteFunction.toInt().apply("123456789")).isEqualTo(123456789);
assertThat(ConcreteFunction.parseAll(ImmutableList.of("5", "17"), ConcreteFunction.toInt()))
.containsExactly(5, 17);
// Expect String apply(Number) and any expected bridges
assertThat(ConcreteFunction.toInt().getClass().getDeclaredMethods())
.hasLength(expectedBridgesFromSameTarget + 1);
// Sanity check that we only copied over methods, no fields, from the functional interface
try {
ConcreteFunction.toInt().getClass().getDeclaredField("DO_NOT_COPY_INTO_LAMBDA_CLASSES");
fail("NoSuchFieldException expected");
} catch (NoSuchFieldException expected) {}
assertThat(SpecializedFunction.class.getDeclaredField("DO_NOT_COPY_INTO_LAMBDA_CLASSES"))
.isNotNull(); // test sanity
}
/** Tests lambdas with bridge methods when the implemented interface is in a separate target.*/
@Test
public void testLambdaWithBridgeMethodsForInterfaceInSeparateTarget() {
assertThat(ConcreteFunction.isInt().test(123456789L)).isTrue();
assertThat(
ConcreteFunction.doFilter(
ImmutableList.of(123456789L, 1234567890987654321L),
ConcreteFunction.isInt()))
.containsExactly(123456789L);
// Expect test(Number) and any expected bridges
assertThat(ConcreteFunction.isInt().getClass().getDeclaredMethods())
.hasLength(expectedBridgesFromSeparateTarget + 1);
}
@Test
public void testLambdaInInterfaceStaticInitializer() {
assertThat(InterfaceWithLambda.DIGITS).containsExactly("0", "1").inOrder();
// <clinit> doesn't count but if there's a lambda method then Jacoco adds more methods
assertThat(InterfaceWithLambda.class.getDeclaredMethods().length != 0)
.isEqualTo(expectLambdaMethodsInInterfaces);
}
/**
* Sanity-checks that the resource file included in the original Jar is still there unchanged.
*/
@Test
public void testResourcePreserved() throws Exception {
try (InputStream content = Lambda.class.getResource("testresource.txt").openStream()) {
assertThat(new String(ByteStreams.toByteArray(content), UTF_8)).isEqualTo("test");
}
}
/**
* Test for b/62456849. After desugar, the method {@code lambda$mult$0} should still be in the
* class.
*/
@Test
public void testCallMethodWithLambdaNamingConvention()
throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
Method method = Lambda.class.getDeclaredMethod("lambda$mult$0");
Object value = method.invoke(null);
assertThat(value).isInstanceOf(Integer.class);
assertThat((Integer) value).isEqualTo(0);
}
}