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