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 android.os.Bundle;
20 import android.platform.test.microbenchmark.Microbenchmark;
21 import android.platform.test.rule.DynamicRuleChain;
22 
23 import androidx.annotation.VisibleForTesting;
24 import androidx.test.InstrumentationRegistry;
25 
26 import org.junit.After;
27 import org.junit.AfterClass;
28 import org.junit.Before;
29 import org.junit.BeforeClass;
30 import org.junit.internal.runners.statements.RunAfters;
31 import org.junit.internal.runners.statements.RunBefores;
32 import org.junit.rules.TestRule;
33 import org.junit.runner.Description;
34 import org.junit.runner.notification.StoppedByUserException;
35 import org.junit.runners.BlockJUnit4ClassRunner;
36 import org.junit.runners.model.FrameworkMethod;
37 import org.junit.runners.model.InitializationError;
38 import org.junit.runners.model.MultipleFailureException;
39 import org.junit.runners.model.Statement;
40 
41 import java.util.ArrayList;
42 import java.util.Arrays;
43 import java.util.List;
44 import java.util.regex.Matcher;
45 import java.util.regex.Pattern;
46 
47 /**
48  * A {@link BlockJUnit4ClassRunner} that runs the test class's {@link BeforeClass} methods as {@link
49  * Before} methods and {@link AfterClass} methods as {@link After} methods for metric collection in
50  * longevity tests.
51  */
52 public class LongevityClassRunner extends BlockJUnit4ClassRunner {
53     // Use these options to inject rules at runtime via the command line. For details, please see
54     // documentation for DynamicRuleChain.
55     @VisibleForTesting static final String DYNAMIC_OUTER_CLASS_RULES_OPTION = "outer-class-rules";
56     @VisibleForTesting static final String DYNAMIC_INNER_CLASS_RULES_OPTION = "inner-class-rules";
57     @VisibleForTesting static final String DYNAMIC_OUTER_TEST_RULES_OPTION = "outer-test-rules";
58     @VisibleForTesting static final String DYNAMIC_INNER_TEST_RULES_OPTION = "inner-test-rules";
59 
60     @VisibleForTesting static final String FILTER_OPTION = "exclude-class";
61     @VisibleForTesting static final String ITERATION_SEP_OPTION = "iteration-separator";
62     @VisibleForTesting static final String ITERATION_SEP_DEFAULT = "@";
63     // A constant to indicate that the iteration number is not set.
64     @VisibleForTesting static final int ITERATION_NOT_SET = -1;
65 
66     private final Bundle mArguments;
67     private final String[] mExcludedClasses;
68     private String mIterationSep = ITERATION_SEP_DEFAULT;
69 
70     private boolean mTestFailed = true;
71     private boolean mTestAttempted = false;
72     // Iteration number.
73     private int mIteration = ITERATION_NOT_SET;
74 
LongevityClassRunner(Class<?> klass)75     public LongevityClassRunner(Class<?> klass) throws InitializationError {
76         this(klass, InstrumentationRegistry.getArguments());
77     }
78 
79     @VisibleForTesting
LongevityClassRunner(Class<?> klass, Bundle args)80     LongevityClassRunner(Class<?> klass, Bundle args) throws InitializationError {
81         super(klass);
82         mArguments = args;
83         mExcludedClasses =
84                 args.containsKey(FILTER_OPTION)
85                         ? args.getString(FILTER_OPTION).split(",")
86                         : new String[] {};
87         mIterationSep =
88                 args.containsKey(ITERATION_SEP_OPTION)
89                         ? args.getString(ITERATION_SEP_OPTION)
90                         : mIterationSep;
91     }
92 
93     /** Set the iteration of the test that this runner is running. */
setIteration(int iteration)94     public void setIteration(int iteration) {
95         mIteration = iteration;
96     }
97 
98     /**
99      * Utilized by tests to check that the iteration is set, independent of the description logic.
100      */
101     @VisibleForTesting
getIteration()102     int getIteration() {
103         return mIteration;
104     }
105 
106     /** Add {@link DynamicRuleChain} to the existing class rules. */
107     @Override
classRules()108     protected List<TestRule> classRules() {
109         List<TestRule> classRules = new ArrayList<>();
110         // Inner dynamic class rules should be included first because RunRules applies rules inside
111         // -out.
112         classRules.add(new DynamicRuleChain(DYNAMIC_INNER_CLASS_RULES_OPTION, mArguments));
113         classRules.addAll(super.classRules());
114         classRules.add(new DynamicRuleChain(DYNAMIC_OUTER_CLASS_RULES_OPTION, mArguments));
115         return classRules;
116     }
117 
118     /** Add {@link DynamicRuleChain} to the existing test rules. */
getTestRules(Object target)119     protected List<TestRule> getTestRules(Object target) {
120         List<TestRule> testRules = new ArrayList<>();
121         // Inner dynamic rules should be included first because RunRules applies rules inside-out.
122         testRules.add(new DynamicRuleChain(DYNAMIC_INNER_TEST_RULES_OPTION, mArguments));
123         testRules.addAll(super.getTestRules(target));
124         testRules.add(new DynamicRuleChain(DYNAMIC_OUTER_TEST_RULES_OPTION, mArguments));
125         return testRules;
126     }
127 
128     /**
129      * Override the parent {@code withBeforeClasses} method to be a no-op.
130      *
131      * <p>The {@link BeforeClass} methods will be included later as {@link Before} methods.
132      */
133     @Override
withBeforeClasses(Statement statement)134     protected Statement withBeforeClasses(Statement statement) {
135         return statement;
136     }
137 
138     /**
139      * Override the parent {@code withAfterClasses} method to be a no-op.
140      *
141      * <p>The {@link AfterClass} methods will be included later as {@link After} methods.
142      */
143     @Override
withAfterClasses(Statement statement)144     protected Statement withAfterClasses(Statement statement) {
145         return new RunAfterClassMethodsOnTestFailure(
146                 statement, getTestClass().getAnnotatedMethods(AfterClass.class), null);
147     }
148 
149     /**
150      * Runs the {@link BeforeClass} methods before running all the {@link Before} methods of the
151      * test class.
152      */
153     @Override
withBefores(FrameworkMethod method, Object target, Statement statement)154     protected Statement withBefores(FrameworkMethod method, Object target, Statement statement) {
155         List<FrameworkMethod> allBeforeMethods = new ArrayList<>();
156         allBeforeMethods.addAll(getTestClass().getAnnotatedMethods(BeforeClass.class));
157         // Workaround to support @NoMetricBefore/@NoMetricAfter methods used in microbenchmark
158         // runner.
159         // TODO(b/205019000) TODO(b/148104702): these annotations seen as a temporary solutions
160         // and supposed to be eventually removed
161         allBeforeMethods.addAll(getTestClass().getAnnotatedMethods(Microbenchmark.NoMetricBefore.class));
162         allBeforeMethods.addAll(getTestClass().getAnnotatedMethods(Before.class));
163         return allBeforeMethods.isEmpty()
164                 ? statement
165                 : addRunBefores(statement, allBeforeMethods, target);
166     }
167 
168     /**
169      * Runs the {@link AfterClass} methods after running all the {@link After} methods of the test
170      * class.
171      */
172     @Override
withAfters(FrameworkMethod method, Object target, Statement statement)173     protected Statement withAfters(FrameworkMethod method, Object target, Statement statement) {
174         final List<FrameworkMethod> afterMethods = new ArrayList<>();
175         afterMethods.addAll(getTestClass().getAnnotatedMethods(After.class));
176         // Workaround to support @NoMetricBefore/@NoMetricAfter methods used in microbenchmark
177         // runner.
178         // TODO(b/205019000) TODO(b/148104702): these annotations seen as a temporary solutions
179         // and supposed to be eventually removed
180         afterMethods.addAll(getTestClass().getAnnotatedMethods(Microbenchmark.NoMetricAfter.class));
181 
182         return addRunAfters(
183                 statement,
184                 afterMethods,
185                 getTestClass().getAnnotatedMethods(AfterClass.class),
186                 target);
187     }
188 
189     /** Factory method to return the {@link RunBefores} object. Exposed for testing only. */
190     @VisibleForTesting
addRunBefores( Statement statement, List<FrameworkMethod> befores, Object target)191     protected RunBefores addRunBefores(
192             Statement statement, List<FrameworkMethod> befores, Object target) {
193         return new RunBefores(statement, befores, target);
194     }
195 
196     /**
197      * Factory method to return the {@link Statement} object for running "after" methods. Exposed
198      * for testing only.
199      */
200     @VisibleForTesting
addRunAfters( Statement statement, List<FrameworkMethod> afterMethods, List<FrameworkMethod> afterClassMethods, Object target)201     protected Statement addRunAfters(
202             Statement statement,
203             List<FrameworkMethod> afterMethods,
204             List<FrameworkMethod> afterClassMethods,
205             Object target) {
206         return new RunAfterMethods(statement, afterMethods, afterClassMethods, target);
207     }
208 
209     @VisibleForTesting
hasTestFailed()210     protected boolean hasTestFailed() {
211         if (!mTestAttempted) {
212             throw new IllegalStateException(
213                     "Test success status should not be checked before the test is attempted.");
214         }
215         return mTestFailed;
216     }
217 
218     @Override
isIgnored(FrameworkMethod child)219     protected boolean isIgnored(FrameworkMethod child) {
220         if (super.isIgnored(child)) return true;
221         // Check if this class has been filtered.
222         String name = getTestClass().getJavaClass().getCanonicalName();
223         return Arrays.stream(mExcludedClasses)
224                 .map(f -> Pattern.compile(f).matcher(name))
225                 .anyMatch(Matcher::matches);
226     }
227 
228     /**
229      * {@link Statement} to run the statement and the {@link After} methods. If the test does not
230      * fail, also runs the {@link AfterClass} method as {@link After} methods.
231      */
232     @VisibleForTesting
233     class RunAfterMethods extends Statement {
234         private final List<FrameworkMethod> mAfterMethods;
235         private final List<FrameworkMethod> mAfterClassMethods;
236         private final Statement mStatement;
237         private final Object mTarget;
238 
RunAfterMethods( Statement statement, List<FrameworkMethod> afterMethods, List<FrameworkMethod> afterClassMethods, Object target)239         public RunAfterMethods(
240                 Statement statement,
241                 List<FrameworkMethod> afterMethods,
242                 List<FrameworkMethod> afterClassMethods,
243                 Object target) {
244             mStatement = statement;
245             mAfterMethods = afterMethods;
246             mAfterClassMethods = afterClassMethods;
247             mTarget = target;
248         }
249 
250         @Override
evaluate()251         public void evaluate() throws Throwable {
252             Statement withAfters = new RunAfters(mStatement, mAfterMethods, mTarget);
253             LongevityClassRunner.this.mTestAttempted = true;
254             withAfters.evaluate();
255             // If the evaluation fails, the part from here on will not be executed, and
256             // RunAfterClassMethodsOnTestFailure will then know to run the @AfterClass methods.
257             LongevityClassRunner.this.mTestFailed = false;
258             invokeAndCollectErrors(mAfterClassMethods, mTarget);
259         }
260     }
261 
262     /**
263      * {@link Statement} to run the {@link AfterClass} methods only in the event that a test failed.
264      */
265     @VisibleForTesting
266     class RunAfterClassMethodsOnTestFailure extends Statement {
267         private final List<FrameworkMethod> mAfterClassMethods;
268         private final Statement mStatement;
269         private final Object mTarget;
270 
RunAfterClassMethodsOnTestFailure( Statement statement, List<FrameworkMethod> afterClassMethods, Object target)271         public RunAfterClassMethodsOnTestFailure(
272                 Statement statement, List<FrameworkMethod> afterClassMethods, Object target) {
273             mStatement = statement;
274             mAfterClassMethods = afterClassMethods;
275             mTarget = target;
276         }
277 
278         @Override
evaluate()279         public void evaluate() throws Throwable {
280             List<Throwable> errors = new ArrayList<>();
281             boolean stoppedByUser = false;
282             try {
283                 mStatement.evaluate();
284             } catch (Throwable e) {
285                 if (e instanceof StoppedByUserException) {
286                     stoppedByUser = true;
287                 }
288                 errors.add(e);
289             } finally {
290                 if (!stoppedByUser && LongevityClassRunner.this.hasTestFailed()) {
291                     errors.addAll(invokeAndCollectErrors(mAfterClassMethods, mTarget));
292                 }
293             }
294             MultipleFailureException.assertEmpty(errors);
295         }
296     }
297 
298     /** Invoke the list of methods and collect errors into a list. */
299     @VisibleForTesting
invokeAndCollectErrors(List<FrameworkMethod> methods, Object target)300     protected List<Throwable> invokeAndCollectErrors(List<FrameworkMethod> methods, Object target)
301             throws Throwable {
302         List<Throwable> errors = new ArrayList<>();
303         for (FrameworkMethod method : methods) {
304             try {
305                 method.invokeExplosively(target);
306             } catch (Throwable e) {
307                 errors.add(e);
308             }
309         }
310         return errors;
311     }
312 
313     /**
314      * Rename the child class name to add iterations if the renaming iteration option is enabled.
315      *
316      * <p>Renaming the class here is chosen over renaming the method name because
317      *
318      * <ul>
319      *   <li>Conceptually, the runner is running a class multiple times, as opposed to a method.
320      *   <li>When instrumenting a suite in command line, by default the instrumentation command
321      *       outputs the class name only. Renaming the class helps with interpretation in this case.
322      */
323     @Override
describeChild(FrameworkMethod method)324     protected Description describeChild(FrameworkMethod method) {
325         return addIterationIfEnabled(super.describeChild(method));
326     }
327 
328     /** Rename the class name to add iterations if the renaming iteration option is enabled. */
addIterationIfEnabled(Description input)329     protected Description addIterationIfEnabled(Description input) {
330         if (mIteration == ITERATION_NOT_SET) {
331             return input;
332         }
333         return Description.createTestDescription(
334                 String.join(mIterationSep, input.getClassName(), String.valueOf(mIteration)),
335                 input.getMethodName());
336     }
337 }
338