1// Copyright 2016 The Bazel Authors. All rights reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//    http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14package com.google.devtools.build.android.desugar;
15
16import static com.google.common.truth.Truth.assertThat;
17import static java.lang.reflect.Modifier.isFinal;
18import static java.nio.charset.StandardCharsets.UTF_8;
19import static org.junit.Assert.fail;
20
21import com.google.common.collect.ImmutableList;
22import com.google.common.io.ByteStreams;
23import com.google.devtools.build.android.desugar.testdata.CaptureLambda;
24import com.google.devtools.build.android.desugar.testdata.ConcreteFunction;
25import com.google.devtools.build.android.desugar.testdata.ConstructorReference;
26import com.google.devtools.build.android.desugar.testdata.GuavaLambda;
27import com.google.devtools.build.android.desugar.testdata.InnerClassLambda;
28import com.google.devtools.build.android.desugar.testdata.InterfaceWithLambda;
29import com.google.devtools.build.android.desugar.testdata.Lambda;
30import com.google.devtools.build.android.desugar.testdata.LambdaInOverride;
31import com.google.devtools.build.android.desugar.testdata.MethodReference;
32import com.google.devtools.build.android.desugar.testdata.MethodReferenceInSubclass;
33import com.google.devtools.build.android.desugar.testdata.MethodReferenceSuperclass;
34import com.google.devtools.build.android.desugar.testdata.OuterReferenceLambda;
35import com.google.devtools.build.android.desugar.testdata.SpecializedFunction;
36import java.io.IOException;
37import java.io.InputStream;
38import java.lang.reflect.InvocationTargetException;
39import java.lang.reflect.Method;
40import java.util.concurrent.Callable;
41import org.junit.Test;
42import org.junit.runner.RunWith;
43import org.junit.runners.JUnit4;
44
45/**
46 * Test that exercises classes in the {@code testdata} package.  This is meant to be run against a
47 * desugared version of those classes, which in turn exercise various desugaring features.
48 */
49@RunWith(JUnit4.class)
50public class DesugarFunctionalTest {
51
52  private final int expectedBridgesFromSameTarget;
53  private final int expectedBridgesFromSeparateTarget;
54  private final boolean expectLambdaMethodsInInterfaces;
55
56  public DesugarFunctionalTest() {
57    this(3, 1, false);
58  }
59
60  /** Constructor for testing desugar while allowing default and static interface methods. */
61  protected DesugarFunctionalTest(
62      boolean expectBridgesFromSeparateTarget, boolean expectDefaultMethods) {
63    this(
64        expectDefaultMethods ? 0 : 3,
65        expectBridgesFromSeparateTarget ? 1 : 0,
66        expectDefaultMethods);
67  }
68
69  private DesugarFunctionalTest(int bridgesFromSameTarget, int bridgesFromSeparateTarget,
70      boolean lambdaMethodsInInterfaces) {
71    this.expectedBridgesFromSameTarget = bridgesFromSameTarget;
72    this.expectedBridgesFromSeparateTarget = bridgesFromSeparateTarget;
73    this.expectLambdaMethodsInInterfaces = lambdaMethodsInInterfaces;
74  }
75
76  @Test
77  public void testGuavaLambda() {
78    GuavaLambda lambdaUse = new GuavaLambda(ImmutableList.of("Sergey", "Larry", "Alex"));
79    assertThat(lambdaUse.as()).containsExactly("Alex");
80  }
81
82  @Test
83  public void testJavaLambda() {
84    Lambda lambdaUse = new Lambda(ImmutableList.of("Sergey", "Larry", "Alex"));
85    assertThat(lambdaUse.as()).containsExactly("Alex");
86  }
87
88  @Test
89  public void testLambdaForIntersectionType() throws Exception {
90    assertThat(Lambda.hello().call()).isEqualTo("hello");
91  }
92
93  @Test
94  public void testCapturingLambda() {
95    CaptureLambda lambdaUse = new CaptureLambda(ImmutableList.of("Sergey", "Larry", "Alex"));
96    assertThat(lambdaUse.prefixed("L")).containsExactly("Larry");
97  }
98
99  @Test
100  public void testOuterReferenceLambda() throws Exception {
101    OuterReferenceLambda lambdaUse = new OuterReferenceLambda(ImmutableList.of("Sergey", "Larry"));
102    assertThat(lambdaUse.filter(ImmutableList.of("Larry", "Alex"))).containsExactly("Larry");
103    assertThat(
104        isFinal(
105            OuterReferenceLambda.class
106                .getDeclaredMethod("lambda$filter$0$OuterReferenceLambda", String.class)
107                .getModifiers()))
108        .isTrue();
109  }
110
111  /**
112   * Tests a lambda in a subclass whose generated lambda$ method has the same name and signature
113   * as a lambda$ method generated by Javac in a superclass and both of these methods are used
114   * in the implementation of the subclass (by calling super).  Naively this leads to wrong
115   * behavior (in this case, return a non-empty list) because the lambda$ in the superclass is never
116   * used once its made non-private during desugaring.
117   */
118  @Test
119  public void testOuterReferenceLambdaInOverride() throws Exception {
120    OuterReferenceLambda lambdaUse = new LambdaInOverride(ImmutableList.of("Sergey", "Larry"));
121    assertThat(lambdaUse.filter(ImmutableList.of("Larry", "Alex"))).isEmpty();
122    assertThat(
123        isFinal(
124            LambdaInOverride.class
125                .getDeclaredMethod("lambda$filter$0$LambdaInOverride", String.class)
126                .getModifiers()))
127        .isTrue();
128  }
129
130  @Test
131  public void testLambdaInAnonymousClassReferencesSurroundingMethodParameter() throws Exception {
132    assertThat(Lambda.mult(21).apply(2).call()).isEqualTo(42);
133  }
134
135  /** Tests a lambda that accesses a method parameter across 2 nested anonymous classes. */
136  @Test
137  public void testLambdaInNestedAnonymousClass() throws Exception {
138    InnerClassLambda lambdaUse = new InnerClassLambda(ImmutableList.of("Sergey", "Larry"));
139    assertThat(lambdaUse.prefixFilter("L").apply(ImmutableList.of("Lois", "Larry")).call())
140        .containsExactly("Larry");
141  }
142
143  @Test
144  public void testClassMethodReference() {
145    MethodReference methodrefUse = new MethodReference(ImmutableList.of("Sergey", "Larry", "Alex"));
146    StringBuilder dest = new StringBuilder();
147    methodrefUse.appendAll(dest);
148    assertThat(dest.toString()).isEqualTo("SergeyLarryAlex");
149  }
150
151  // Regression test for b/33378312
152  @Test
153  public void testHiddenMethodReference() {
154    MethodReference methodrefUse = new MethodReference(ImmutableList.of("Sergey", "Larry", "Alex"));
155    assertThat(methodrefUse.intersect(ImmutableList.of("Alex", "Sundar"))).containsExactly("Alex");
156  }
157
158  // Regression test for b/33378312
159  @Test
160  public void testHiddenStaticMethodReference() {
161    MethodReference methodrefUse =
162        new MethodReference(ImmutableList.of("Sergey", "Larry", "Sundar"));
163    assertThat(methodrefUse.some()).containsExactly("Sergey", "Sundar");
164  }
165
166  @Test
167  public void testDuplicateHiddenMethodReference() {
168    MethodReference methodrefUse = new MethodReference(ImmutableList.of("Sergey", "Larry", "Alex"));
169    assertThat(methodrefUse.onlyIn(ImmutableList.of("Alex", "Sundar"))).containsExactly("Sundar");
170  }
171
172  // Regression test for b/36201257
173  @Test
174  public void testMethodReferenceThatNeedsBridgeInSubclass() {
175    MethodReferenceInSubclass methodrefUse =
176        new MethodReferenceInSubclass(ImmutableList.of("Sergey", "Larry", "Alex"));
177    assertThat(methodrefUse.containsE()).containsExactly("Sergey", "Alex");
178    assertThat(methodrefUse.startsWithL()).containsExactly("Larry");
179    // Test sanity: make sure sub- and superclass have bridge methods with matching descriptors but
180    // different names
181    Method superclassBridge = findOnlyBridge(MethodReferenceSuperclass.class);
182    Method subclassBridge = findOnlyBridge(MethodReferenceInSubclass.class);
183    assertThat(superclassBridge.getName()).isNotEqualTo(subclassBridge.getName());
184    assertThat(superclassBridge.getParameterTypes()).isEqualTo(subclassBridge.getParameterTypes());
185  }
186
187  private Method findOnlyBridge(Class<?> clazz) {
188    Method result = null;
189    for (Method m : clazz.getDeclaredMethods()) {
190      if (m.getName().startsWith("bridge$")) {
191        assertThat(result).named(m.getName()).isNull();
192        result = m;
193      }
194    }
195    assertThat(result).named(clazz.getSimpleName()).isNotNull();
196    return result;
197  }
198
199  // Regression test for b/33378312
200  @Test
201  public void testThrowingPrivateMethodReference() throws Exception {
202    MethodReference methodrefUse = new MethodReference(ImmutableList.of("Sergey", "Larry"));
203    Callable<?> stringer = methodrefUse.stringer();
204    try {
205      stringer.call();
206      fail("IOException expected");
207    } catch (IOException expected) {
208      assertThat(expected).hasMessage("SergeyLarry");
209    } catch (Exception e) {
210      throw e;
211    }
212  }
213
214  // Regression test for b/33304582
215  @Test
216  public void testInterfaceMethodReference() {
217    MethodReference methodrefUse = new MethodReference(ImmutableList.of("Sergey", "Larry", "Alex"));
218    MethodReference.Transformer<String> transform = new MethodReference.Transformer<String>() {
219      @Override
220      public String transform(String input) {
221        return input.substring(1);
222      }
223    };
224    assertThat(methodrefUse.transform(transform)).containsExactly("ergey", "arry", "lex");
225  }
226
227  @Test
228  public void testConstructorReference() {
229    ConstructorReference initRefUse = new ConstructorReference(ImmutableList.of("1", "2", "42"));
230    assertThat(initRefUse.toInt()).containsExactly(1, 2, 42);
231  }
232
233  // Regression test for b/33304582
234  @Test
235  public void testPrivateConstructorReference() {
236    ConstructorReference initRefUse = ConstructorReference.singleton().apply("17");
237    assertThat(initRefUse.toInt()).containsExactly(17);
238  }
239
240  // This test is similar to testPrivateConstructorReference but the private constructor of an inner
241  // class is used as a method reference.  That causes Javac to generate a bridge constructor and
242  // a lambda body method that calls it, so the desugaring step doesn't need to do anything to make
243  // the private constructor visible.  This is mostly to double-check that we don't interfere with
244  // this "already-working" scenario.
245  @Test
246  public void testPrivateConstructorAccessedThroughJavacGeneratedBridge() {
247    try {
248      @SuppressWarnings("unused") // local is needed to make ErrorProne happy
249      ConstructorReference unused = ConstructorReference.emptyThroughJavacGeneratedBridge().get();
250      fail("RuntimeException expected");
251    } catch (RuntimeException expected) {
252      assertThat(expected).hasMessage("got it!");
253    }
254  }
255
256  @Test
257  public void testExpressionMethodReference() {
258    assertThat(
259            MethodReference.stringChars(new StringBuilder().append("Larry").append("Sergey"))
260                .apply(5))
261        .isEqualTo('S');
262  }
263
264  @Test
265  public void testFieldMethodReference() {
266    MethodReference methodrefUse = new MethodReference(ImmutableList.of("Sergey", "Larry", "Alex"));
267    assertThat(methodrefUse.toPredicate().test("Larry")).isTrue();
268    assertThat(methodrefUse.toPredicate().test("Sundar")).isFalse();
269  }
270
271  @Test
272  public void testConcreteFunctionWithInheritedBridgeMethods() {
273    assertThat(new ConcreteFunction().apply("1234567890987654321")).isEqualTo(1234567890987654321L);
274    assertThat(ConcreteFunction.parseAll(ImmutableList.of("5", "17"), new ConcreteFunction()))
275        .containsExactly(5L, 17L);
276  }
277
278  @Test
279  public void testLambdaWithInheritedBridgeMethods() throws Exception {
280    assertThat(ConcreteFunction.toInt().apply("123456789")).isEqualTo(123456789);
281    assertThat(ConcreteFunction.parseAll(ImmutableList.of("5", "17"), ConcreteFunction.toInt()))
282        .containsExactly(5, 17);
283    // Expect String apply(Number) and any expected bridges
284    assertThat(ConcreteFunction.toInt().getClass().getDeclaredMethods())
285        .hasLength(expectedBridgesFromSameTarget + 1);
286    // Sanity check that we only copied over methods, no fields, from the functional interface
287    try {
288      ConcreteFunction.toInt().getClass().getDeclaredField("DO_NOT_COPY_INTO_LAMBDA_CLASSES");
289      fail("NoSuchFieldException expected");
290    } catch (NoSuchFieldException expected) {}
291    assertThat(SpecializedFunction.class.getDeclaredField("DO_NOT_COPY_INTO_LAMBDA_CLASSES"))
292        .isNotNull(); // test sanity
293  }
294
295  /** Tests lambdas with bridge methods when the implemented interface is in a separate target.*/
296  @Test
297  public void testLambdaWithBridgeMethodsForInterfaceInSeparateTarget() {
298    assertThat(ConcreteFunction.isInt().test(123456789L)).isTrue();
299    assertThat(
300        ConcreteFunction.doFilter(
301            ImmutableList.of(123456789L, 1234567890987654321L),
302            ConcreteFunction.isInt()))
303        .containsExactly(123456789L);
304    // Expect test(Number) and any expected bridges
305    assertThat(ConcreteFunction.isInt().getClass().getDeclaredMethods())
306        .hasLength(expectedBridgesFromSeparateTarget + 1);
307  }
308
309  @Test
310  public void testLambdaInInterfaceStaticInitializer() {
311    assertThat(InterfaceWithLambda.DIGITS).containsExactly("0", "1").inOrder();
312    // <clinit> doesn't count but if there's a lambda method then Jacoco adds more methods
313    assertThat(InterfaceWithLambda.class.getDeclaredMethods().length != 0)
314        .isEqualTo(expectLambdaMethodsInInterfaces);
315  }
316
317  /**
318   * Sanity-checks that the resource file included in the original Jar is still there unchanged.
319   */
320  @Test
321  public void testResourcePreserved() throws Exception {
322    try (InputStream content = Lambda.class.getResource("testresource.txt").openStream()) {
323      assertThat(new String(ByteStreams.toByteArray(content), UTF_8)).isEqualTo("test");
324    }
325  }
326
327  /**
328   * Test for b/62456849. After desugar, the method {@code lambda$mult$0} should still be in the
329   * class.
330   */
331  @Test
332  public void testCallMethodWithLambdaNamingConvention()
333      throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
334    Method method = Lambda.class.getDeclaredMethod("lambda$mult$0");
335    Object value = method.invoke(null);
336    assertThat(value).isInstanceOf(Integer.class);
337    assertThat((Integer) value).isEqualTo(0);
338  }
339}
340