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