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