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