// 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 transform = new MethodReference.Transformer() { @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(); // 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); } }