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