1 /*
2  * Copyright (C) 2019 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.platform.test.longevity;
18 
19 import static com.google.common.truth.Truth.assertThat;
20 
21 import static org.mockito.ArgumentMatchers.any;
22 import static org.mockito.Mockito.argThat;
23 import static org.mockito.Mockito.atLeastOnce;
24 import static org.mockito.Mockito.doAnswer;
25 import static org.mockito.Mockito.doReturn;
26 import static org.mockito.Mockito.mock;
27 import static org.mockito.Mockito.never;
28 import static org.mockito.Mockito.spy;
29 import static org.mockito.Mockito.times;
30 import static org.mockito.Mockito.verify;
31 import static org.mockito.MockitoAnnotations.initMocks;
32 
33 import android.os.Bundle;
34 import android.platform.test.rule.TestWatcher;
35 
36 import org.junit.After;
37 import org.junit.AfterClass;
38 import org.junit.Assert;
39 import org.junit.Before;
40 import org.junit.BeforeClass;
41 import org.junit.ClassRule;
42 import org.junit.Rule;
43 import org.junit.Test;
44 import org.junit.runner.Description;
45 import org.junit.runner.RunWith;
46 import org.junit.runner.notification.Failure;
47 import org.junit.runner.notification.RunListener;
48 import org.junit.runner.notification.RunNotifier;
49 import org.junit.runners.JUnit4;
50 import org.junit.runners.model.FrameworkMethod;
51 import org.junit.runners.model.Statement;
52 import org.mockito.ArgumentCaptor;
53 import org.mockito.Mock;
54 import org.mockito.exceptions.base.MockitoAssertionError;
55 
56 import java.util.ArrayList;
57 import java.util.List;
58 
59 /** Unit tests for the {@link LongevityClassRunner}. */
60 public class LongevityClassRunnerTest {
61     // Used to test things that are generated reflectively, e.g. dynamic rules.
62     private static final List<String> sLogs = new ArrayList<>();
63 
64     public static class LoggingRule extends TestWatcher {
65         private final String mName;
66 
LoggingRule(String name)67         public LoggingRule(String name) {
68             mName = name;
69         }
70 
71         @Override
starting(Description description)72         public void starting(Description description) {
73             sLogs.add(String.format("%s starting", mName));
74         }
75 
76         @Override
finished(Description description)77         public void finished(Description description) {
78             sLogs.add(String.format("%s finished", mName));
79         }
80     }
81 
82     public static class InjectedRule1 extends LoggingRule {
InjectedRule1()83         public InjectedRule1() {
84             super("Injected rule 1");
85         }
86     }
87 
88     public static class InjectedRule2 extends LoggingRule {
InjectedRule2()89         public InjectedRule2() {
90             super("Injected rule 2");
91         }
92     }
93 
94     // We use two @Test methods here to distinguish between testing for class
95     // rules vs. test rules. This is for testing only; we still advocate for
96     // one @Test method per class.
97     @RunWith(JUnit4.class)
98     public static class LoggingTestWithRules {
99         @ClassRule public static LoggingRule classRule = new LoggingRule("Hardcoded class rule");
100 
101         @Rule public LoggingRule testRule = new LoggingRule("Hardcoded test rule");
102 
103         @Test
test1()104         public void test1() {
105             sLogs.add("Test 1 execution");
106         }
107 
108         @Test
test2()109         public void test2() {
110             sLogs.add("Test 2 execution");
111         }
112     }
113 
114     // A sample test class to test the runner with.
115     @RunWith(JUnit4.class)
116     public static class NoOpTest {
117         @BeforeClass
beforeClassMethod()118         public static void beforeClassMethod() {}
119 
120         @Before
beforeMethod()121         public void beforeMethod() {}
122 
123         @Test
testMethod()124         public void testMethod() {}
125 
126         @After
afterMethod()127         public void afterMethod() {}
128 
129         @AfterClass
afterClassMethod()130         public static void afterClassMethod() {}
131     }
132 
133     @RunWith(JUnit4.class)
134     public static class FailingTest extends NoOpTest {
135         @Test
testMethod()136         public void testMethod() {
137             throw new RuntimeException("I failed.");
138         }
139     }
140 
141     @Mock private RunNotifier mRunNotifier;
142 
143     private LongevityClassRunner mRunner;
144 
145     private static final Statement PASSING_STATEMENT =
146             new Statement() {
147                 public void evaluate() throws Throwable {
148                     // No-op.
149                 }
150             };
151 
152     private static final Statement FAILING_STATEMENT =
153             new Statement() {
154                 public void evaluate() throws Throwable {
155                     throw new RuntimeException("I failed.");
156                 }
157             };
158 
159     // A failure message for assertion calls in Mockito stubs. These assertion failures will cause
160     // the runner under test to fail but will not trigger a test failure directly. This message is
161     // used to filter failures reported by the mocked RunNotifier and re-throw the ones injected in
162     // the spy.
163     private static final String ASSERTION_FAILURE_MESSAGE = "Test assertions failed";
164 
165     @Before
setUp()166     public void setUp() {
167         initMocks(this);
168         sLogs.clear();
169     }
170 
171     /**
172      * Test that the {@link BeforeClass} methods are added to the test statement as {@link Before}
173      * methods.
174      */
175     @Test
testBeforeClassMethodsAddedAsBeforeMethods()176     public void testBeforeClassMethodsAddedAsBeforeMethods() throws Throwable {
177         mRunner = spy(new LongevityClassRunner(NoOpTest.class));
178         // Spy the withBeforeClasses() method to check that the method does not make changes to the
179         // statement despite the presence of a @BeforeClass method.
180         doAnswer(
181                         invocation -> {
182                             Statement returnedStatement = (Statement) invocation.callRealMethod();
183                             // If this assertion fails, mRunNofitier will fire a test failure.
184                             Assert.assertEquals(
185                                     ASSERTION_FAILURE_MESSAGE,
186                                     returnedStatement,
187                                     invocation.getArgument(0));
188                             return returnedStatement;
189                         })
190                 .when(mRunner)
191                 .withBeforeClasses(any(Statement.class));
192         // Spy the addRunBefores() method to check that the @BeforeClass method is added to the
193         // @Before methods.
194         doAnswer(
195                         invocation -> {
196                             List<FrameworkMethod> methodList =
197                                     (List<FrameworkMethod>) invocation.getArgument(1);
198                             // If any of these assertions fail, mRunNofitier will fire a test
199                             // failure.
200                             // There should be two methods.
201                             Assert.assertEquals(ASSERTION_FAILURE_MESSAGE, methodList.size(), 2);
202                             // The first one should be the @BeforeClass one.
203                             Assert.assertEquals(
204                                     ASSERTION_FAILURE_MESSAGE,
205                                     methodList.get(0).getName(),
206                                     "beforeClassMethod");
207                             // The second one should be the @Before one.
208                             Assert.assertEquals(
209                                     ASSERTION_FAILURE_MESSAGE,
210                                     methodList.get(1).getName(),
211                                     "beforeMethod");
212                             return invocation.callRealMethod();
213                         })
214                 .when(mRunner)
215                 .addRunBefores(any(Statement.class), any(List.class), any(Object.class));
216         // Run the runner.
217         mRunner.run(mRunNotifier);
218         verifyForAssertionFailures(mRunNotifier);
219         // Verify that the stubbed methods are indeed called.
220         verify(mRunner, times(1)).withBeforeClasses(any(Statement.class));
221         verify(mRunner, times(1))
222                 .addRunBefores(any(Statement.class), any(List.class), any(Object.class));
223     }
224 
225     /**
226      * Test that the {@link AfterClass} methods are added to the test statement as potential {@link
227      * After} methods.
228      */
229     @Test
testAfterClassMethodsAddedAsAfterMethods()230     public void testAfterClassMethodsAddedAsAfterMethods() throws Throwable {
231         mRunner = spy(new LongevityClassRunner(NoOpTest.class));
232         // Spy the withAfterClasses() method to check that the method returns an instance of
233         // LongevityClassRunner.RunAfterClassMethodsOnTestFailure.
234         doAnswer(
235                         invocation -> {
236                             Statement returnedStatement = (Statement) invocation.callRealMethod();
237                             // If this assertion fails, mRunNofitier will fire a test failure.
238                             Assert.assertTrue(
239                                     ASSERTION_FAILURE_MESSAGE,
240                                     returnedStatement
241                                             instanceof
242                                             LongevityClassRunner.RunAfterClassMethodsOnTestFailure);
243                             return returnedStatement;
244                         })
245                 .when(mRunner)
246                 .withAfterClasses(any(Statement.class));
247         // Spy the addRunAfters() method to check that the method returns an instance of
248         // LongevityClassRunner.RunAfterMethods.
249         doAnswer(
250                         invocation -> {
251                             Statement returnedStatement = (Statement) invocation.callRealMethod();
252                             // If any of these assertions fail, mRunNotifier will fire a test
253                             // failure.
254                             Assert.assertTrue(
255                                     ASSERTION_FAILURE_MESSAGE,
256                                     returnedStatement
257                                             instanceof LongevityClassRunner.RunAfterMethods);
258                             // The second argument should only contain the @After method.
259                             List<FrameworkMethod> afterMethodList =
260                                     (List<FrameworkMethod>) invocation.getArgument(1);
261                             Assert.assertEquals(
262                                     ASSERTION_FAILURE_MESSAGE, afterMethodList.size(), 1);
263                             Assert.assertEquals(
264                                     ASSERTION_FAILURE_MESSAGE,
265                                     afterMethodList.get(0).getName(),
266                                     "afterMethod");
267                             // The third argument should only contain the @AfterClass method.
268                             List<FrameworkMethod> afterClassMethodList =
269                                     (List<FrameworkMethod>) invocation.getArgument(2);
270                             Assert.assertEquals(
271                                     ASSERTION_FAILURE_MESSAGE, afterClassMethodList.size(), 1);
272                             Assert.assertEquals(
273                                     ASSERTION_FAILURE_MESSAGE,
274                                     afterClassMethodList.get(0).getName(),
275                                     "afterClassMethod");
276                             return returnedStatement;
277                         })
278                 .when(mRunner)
279                 .addRunAfters(
280                         any(Statement.class), any(List.class), any(List.class), any(Object.class));
281         // Run the runner.
282         mRunner.run(mRunNotifier);
283         verifyForAssertionFailures(mRunNotifier);
284         // Verify that the stubbed methods are indeed called.
285         verify(mRunner, times(1)).withAfterClasses(any(Statement.class));
286         verify(mRunner, times(1))
287                 .addRunAfters(
288                         any(Statement.class), any(List.class), any(List.class), any(Object.class));
289     }
290 
291     /**
292      * Test that {@link LongevityClassRunner.RunAfterMethods} marks the test as failed for a failed
293      * test.
294      */
295     @Test
testAfterClassMethodsHandling_marksFailure()296     public void testAfterClassMethodsHandling_marksFailure() throws Throwable {
297         // Initialization parameter does not matter as this test does not concern the tested class.
298         mRunner = spy(new LongevityClassRunner(NoOpTest.class));
299         try {
300             mRunner.hasTestFailed();
301             Assert.fail("Test status should not be able to be checked before it's run.");
302         } catch (Throwable e) {
303             Assert.assertTrue(e.getMessage().contains("should not be checked"));
304         }
305         // Create and partially run the runner with a failing statement.
306         Statement statement =
307                 mRunner.withAfters(
308                         null,
309                         new NoOpTest(),
310                         new Statement() {
311                             public void evaluate() throws Throwable {
312                                 throw new RuntimeException("I failed.");
313                             }
314                         });
315         try {
316             statement.evaluate();
317         } catch (Throwable e) {
318             // Expected and no action needed.
319         }
320         Assert.assertTrue(mRunner.hasTestFailed());
321     }
322 
323     /**
324      * Test that {@link LongevityClassRunner.RunAfterMethods} marks the test as passed for a passed
325      * test.
326      */
327     @Test
testRunAfterClassMethodsHandling_marksPassed()328     public void testRunAfterClassMethodsHandling_marksPassed() throws Throwable {
329         // Initialization parameter does not matter as this test does not concern the tested class.
330         mRunner = spy(new LongevityClassRunner(NoOpTest.class));
331         // Checking test status before the statements are run should throw.
332         try {
333             mRunner.hasTestFailed();
334             Assert.fail("Test status should not be able to be checked before it's run.");
335         } catch (Throwable e) {
336             Assert.assertTrue(e.getMessage().contains("should not be checked"));
337         }
338         // Create and partially run the runner with a passing statement.
339         Statement statement =
340                 mRunner.withAfters(
341                         null,
342                         new NoOpTest(),
343                         new Statement() {
344                             public void evaluate() throws Throwable {
345                                 // Does nothing and thus passes.
346                             }
347                         });
348         statement.evaluate();
349         Assert.assertFalse(mRunner.hasTestFailed());
350     }
351 
352     /** Test that {@link AfterClass} methods are run as {@link After} methods for a passing test. */
353     @Test
testAfterClass_runAsAfterForPassingTest()354     public void testAfterClass_runAsAfterForPassingTest() throws Throwable {
355         // Initialization parameter does not matter as this test does not concern the tested class.
356         mRunner = spy(new LongevityClassRunner(NoOpTest.class));
357         // For a passing test, the AfterClass method should be run in the statement returned from
358         // withAfters().
359         Statement statement =
360                 mRunner.withAfters(
361                         null, // Passing null as parameter in the interface is not actually used.
362                         new NoOpTest(),
363                         PASSING_STATEMENT);
364         statement.evaluate();
365         // Check that the @AfterClass method is called.
366         ArgumentCaptor<List> methodsCaptor = ArgumentCaptor.forClass(List.class);
367         verify(mRunner, times(1)).invokeAndCollectErrors(methodsCaptor.capture(), any());
368         Assert.assertEquals(methodsCaptor.getValue().size(), 1);
369         Assert.assertTrue(
370                 ((FrameworkMethod) methodsCaptor.getValue().get(0))
371                         .getName()
372                         .contains("afterClassMethod"));
373     }
374 
375     /**
376      * Test that {@link AfterClass} methods are run as {@link AfterClass} method for a failing test.
377      */
378     @Test
testAfterClass_runAsAfterClassForFailingTest()379     public void testAfterClass_runAsAfterClassForFailingTest() throws Throwable {
380         // Initialization parameter does not matter as this test does not concern the tested class.
381         mRunner = spy(new LongevityClassRunner(NoOpTest.class));
382         // For a failing test, the AfterClass method should be run in the statement returned from
383         // withAfterClass().
384         doReturn(true).when(mRunner).hasTestFailed();
385         Statement statement = mRunner.withAfterClasses(FAILING_STATEMENT);
386         try {
387             statement.evaluate();
388         } catch (Throwable e) {
389             // Expected and no action needed.
390         }
391         // Check that the @AfterClass method is called.
392         ArgumentCaptor<List> methodsCaptor = ArgumentCaptor.forClass(List.class);
393         verify(mRunner, times(1)).invokeAndCollectErrors(methodsCaptor.capture(), any());
394         Assert.assertEquals(methodsCaptor.getValue().size(), 1);
395         Assert.assertTrue(
396                 ((FrameworkMethod) methodsCaptor.getValue().get(0))
397                         .getName()
398                         .contains("afterClassMethod"));
399     }
400 
401     /** Test that {@link AfterClass} methods are only executed once for a passing test. */
402     @Test
testAfterClassRunOnlyOnce_passingTest()403     public void testAfterClassRunOnlyOnce_passingTest() throws Throwable {
404         mRunner = spy(new LongevityClassRunner(NoOpTest.class));
405         mRunner.run(mRunNotifier);
406         verify(mRunner, times(1))
407                 .invokeAndCollectErrors(getMethodNameMatcher("afterClassMethod"), any());
408     }
409 
410     /** Test that {@link AfterClass} methods are only executed once for a failing test. */
411     @Test
testAfterClassRunOnlyOnce_failingTest()412     public void testAfterClassRunOnlyOnce_failingTest() throws Throwable {
413         mRunner = spy(new LongevityClassRunner(FailingTest.class));
414         mRunner.run(mRunNotifier);
415         verify(mRunner, times(1))
416                 .invokeAndCollectErrors(getMethodNameMatcher("afterClassMethod"), any());
417     }
418 
419     /** Test that excluded classes are not executed. */
420     @Test
testIgnore_excludedClasses()421     public void testIgnore_excludedClasses() throws Throwable {
422         RunNotifier notifier = spy(new RunNotifier());
423         RunListener listener = mock(RunListener.class);
424         notifier.addListener(listener);
425         Bundle ignores = new Bundle();
426         ignores.putString(LongevityClassRunner.FILTER_OPTION, FailingTest.class.getCanonicalName());
427         mRunner = spy(new LongevityClassRunner(FailingTest.class, ignores));
428         mRunner.run(notifier);
429         verify(listener, times(1)).testIgnored(any());
430     }
431 
432     /** Test that the runner does not report iteration when iteration is not set. */
433     @Test
testReportIteration_noIterationSet()434     public void testReportIteration_noIterationSet() throws Throwable {
435         ArgumentCaptor<Description> captor = ArgumentCaptor.forClass(Description.class);
436         RunNotifier notifier = mock(RunNotifier.class);
437         mRunner = spy(new LongevityClassRunner(NoOpTest.class));
438         mRunner.run(notifier);
439         verify(notifier).fireTestStarted(captor.capture());
440         Assert.assertFalse(
441                 "Description class name should not contain the iteration number.",
442                 captor.getValue()
443                         .getClassName()
444                         .matches(
445                                 String.join(
446                                         LongevityClassRunner.ITERATION_SEP_DEFAULT,
447                                         "^.*",
448                                         "[0-9]+$")));
449     }
450 
451     /** Test that the runner reports iteration when set and the default separator was used. */
452     @Test
testReportIteration_withIteration_withDefaultSeparator()453     public void testReportIteration_withIteration_withDefaultSeparator() throws Throwable {
454         ArgumentCaptor<Description> captor = ArgumentCaptor.forClass(Description.class);
455         RunNotifier notifier = mock(RunNotifier.class);
456         mRunner = spy(new LongevityClassRunner(NoOpTest.class));
457         mRunner.setIteration(7);
458         mRunner.run(notifier);
459         verify(notifier).fireTestStarted(captor.capture());
460         Assert.assertTrue(
461                 "Description class name should contain the iteration number.",
462                 captor.getValue()
463                         .getClassName()
464                         .matches(
465                                 String.join(
466                                         LongevityClassRunner.ITERATION_SEP_DEFAULT, "^.*", "7$")));
467     }
468 
469     /** Test that the runner reports iteration when set and a custom separator was supplied. */
470     @Test
testReportIteration_withIteration_withCustomSeparator()471     public void testReportIteration_withIteration_withCustomSeparator() throws Throwable {
472         String sep = "--";
473         Bundle args = new Bundle();
474         args.putString(LongevityClassRunner.ITERATION_SEP_OPTION, sep);
475 
476         ArgumentCaptor<Description> captor = ArgumentCaptor.forClass(Description.class);
477         RunNotifier notifier = mock(RunNotifier.class);
478         mRunner = spy(new LongevityClassRunner(NoOpTest.class, args));
479         mRunner.setIteration(7);
480         mRunner.run(notifier);
481         verify(notifier).fireTestStarted(captor.capture());
482         Assert.assertTrue(
483                 "Description class name should contain the iteration number.",
484                 captor.getValue().getClassName().matches(String.join(sep, "^.*", "7$")));
485     }
486 
487     @Test
testDynamicClassRules()488     public void testDynamicClassRules() throws Throwable {
489         Bundle args = new Bundle();
490         args.putString(
491                 LongevityClassRunner.DYNAMIC_OUTER_CLASS_RULES_OPTION,
492                 InjectedRule1.class.getName());
493         args.putString(
494                 LongevityClassRunner.DYNAMIC_INNER_CLASS_RULES_OPTION,
495                 InjectedRule2.class.getName());
496 
497         RunNotifier notifier = mock(RunNotifier.class);
498         mRunner = new LongevityClassRunner(LoggingTestWithRules.class, args);
499         mRunner.run(notifier);
500         verifyForAssertionFailures(notifier);
501         assertThat(sLogs)
502                 .containsExactly(
503                         "Injected rule 1 starting",
504                         "Hardcoded class rule starting",
505                         "Injected rule 2 starting",
506                         "Hardcoded test rule starting",
507                         "Test 1 execution",
508                         "Hardcoded test rule finished",
509                         "Hardcoded test rule starting",
510                         "Test 2 execution",
511                         "Hardcoded test rule finished",
512                         "Injected rule 2 finished",
513                         "Hardcoded class rule finished",
514                         "Injected rule 1 finished");
515     }
516 
517     @Test
testDynamicTestRules()518     public void testDynamicTestRules() throws Throwable {
519         Bundle args = new Bundle();
520         args.putString(
521                 LongevityClassRunner.DYNAMIC_OUTER_TEST_RULES_OPTION,
522                 InjectedRule1.class.getName());
523         args.putString(
524                 LongevityClassRunner.DYNAMIC_INNER_TEST_RULES_OPTION,
525                 InjectedRule2.class.getName());
526 
527         RunNotifier notifier = mock(RunNotifier.class);
528         mRunner = new LongevityClassRunner(LoggingTestWithRules.class, args);
529         mRunner.run(notifier);
530         verifyForAssertionFailures(notifier);
531         assertThat(sLogs)
532                 .containsExactly(
533                         "Hardcoded class rule starting",
534                         "Injected rule 1 starting",
535                         "Hardcoded test rule starting",
536                         "Injected rule 2 starting",
537                         "Test 1 execution",
538                         "Injected rule 2 finished",
539                         "Hardcoded test rule finished",
540                         "Injected rule 1 finished",
541                         "Injected rule 1 starting",
542                         "Hardcoded test rule starting",
543                         "Injected rule 2 starting",
544                         "Test 2 execution",
545                         "Injected rule 2 finished",
546                         "Hardcoded test rule finished",
547                         "Injected rule 1 finished",
548                         "Hardcoded class rule finished");
549     }
550 
getMethodNameMatcher(String methodName)551     private List<FrameworkMethod> getMethodNameMatcher(String methodName) {
552         return argThat(
553                 l ->
554                         l.stream()
555                                 .anyMatch(
556                                         f -> ((FrameworkMethod) f).getName().contains(methodName)));
557     }
558 
559     /**
560      * Verify that no test failure is fired because of an assertion failure in the stubbed methods.
561      * If the verfication fails, check whether it's due the injected assertions failing. If yes,
562      * throw that exception out; otherwise, throw the first exception.
563      */
verifyForAssertionFailures(final RunNotifier notifier)564     private void verifyForAssertionFailures(final RunNotifier notifier) throws Throwable {
565         try {
566             verify(notifier, never()).fireTestFailure(any());
567         } catch (MockitoAssertionError e) {
568             ArgumentCaptor<Failure> failureCaptor = ArgumentCaptor.forClass(Failure.class);
569             verify(notifier, atLeastOnce()).fireTestFailure(failureCaptor.capture());
570             List<Failure> failures = failureCaptor.getAllValues();
571             // Go through the failures, look for an known failure case from the above exceptions
572             // and throw the exception in the first one out if any.
573             for (Failure failure : failures) {
574                 if (failure.getException().getMessage().contains(ASSERTION_FAILURE_MESSAGE)) {
575                     throw failure.getException();
576                 }
577             }
578             // Otherwise, throw the exception from the first failure reported.
579             throw failures.get(0).getException();
580         }
581     }
582 }
583