1 /* 2 * Copyright (C) 2023 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.adservices.test.longevity.concurrent; 18 19 import android.adservices.test.longevity.concurrent.proto.Configuration.Scenario; 20 import android.adservices.test.longevity.concurrent.proto.Configuration.Scenario.Journey; 21 import android.util.Log; 22 23 import com.android.adservices.shared.testing.junit.EasilyExtensibleBlockJUnit4ClassRunner; 24 import com.android.adservices.shared.testing.junit.RuleContainer; 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.Rule; 31 import org.junit.Test; 32 import org.junit.internal.runners.statements.RunAfters; 33 import org.junit.internal.runners.statements.RunBefores; 34 import org.junit.rules.TestRule; 35 import org.junit.runner.Description; 36 import org.junit.runner.RunWith; 37 import org.junit.runners.JUnit4; 38 import org.junit.runners.model.FrameworkMethod; 39 import org.junit.runners.model.InitializationError; 40 import org.junit.runners.model.MultipleFailureException; 41 import org.junit.runners.model.Statement; 42 import org.junit.runners.model.TestClass; 43 44 import java.util.ArrayList; 45 import java.util.List; 46 47 /** 48 * This class runs a particular scenario which has multiple tests present and this tests are run 49 * concurrently. 50 * 51 * <p>Overrides the methodBlock method which changes the execution of {@link Before}, {@link After}, 52 * {@link Test}, {@link Rule} execution. 53 */ 54 public class ConcurrentRunner extends EasilyExtensibleBlockJUnit4ClassRunner { 55 56 private static final String TAG = ConcurrentRunner.class.getSimpleName(); 57 private static final ThreadLocal<RuleContainer> sCurrentRuleContainer = new ThreadLocal<>(); 58 59 protected final Scenario mScenario; 60 private final String mScheduleIdx; 61 private final List<TestClass> mTestClasses = new ArrayList<>(); 62 private final List<Object> mJourneyClasses = new ArrayList<>(); 63 private final List<FrameworkMethod> mFrameworkMethods = new ArrayList<>(); 64 private Description mDescription = null; 65 ConcurrentRunner(Scenario scenario, String scheduleIdx)66 public ConcurrentRunner(Scenario scenario, String scheduleIdx) throws InitializationError { 67 // Using {@link DummyTestClass} class here as parent class of ConcurrentRunner invokes 68 // ParentRunner<> 69 // which validates test class has a single constructor and annotations. This does not 70 // have any functional impact. 71 super(DummyTestClass.class); 72 mScenario = scenario; 73 mScheduleIdx = scheduleIdx; 74 initialize(); 75 } 76 initialize()77 private void initialize() { 78 for (Journey journey : mScenario.getJourneysList()) { 79 try { 80 String journeyStr = journey.getJourney(); 81 TestClass testClass = new TestClass(Class.forName(journeyStr)); 82 mTestClasses.add(testClass); 83 mFrameworkMethods.add(getFrameworkMethod(journey, testClass)); 84 mJourneyClasses.add(testClass.getJavaClass().getConstructor().newInstance()); 85 } catch (Exception e) { 86 Log.e(TAG, "Failed to initialize test classes and methods"); 87 throw new IllegalArgumentException(e); 88 } 89 } 90 } 91 92 /** 93 * {@inheritDoc} 94 * 95 * <p>Overrides the test class description and describes which all tests we are running in 96 * parallel for a particular scenario. 97 */ 98 @Override describeChild(FrameworkMethod method)99 protected Description describeChild(FrameworkMethod method) { 100 if (mDescription != null) { 101 return mDescription; 102 } 103 StringBuilder builder = new StringBuilder(); 104 builder.append(mScheduleIdx + ": "); 105 for (int i = 0; i < mTestClasses.size(); i++) { 106 builder.append( 107 mTestClasses.get(i).getName() 108 + "#" 109 + mJourneyClasses.get(i).getClass().getSimpleName() 110 + ", "); 111 } 112 mDescription = Description.createTestDescription(builder.toString(), /* arg = */ ""); 113 return mDescription; 114 } 115 116 /** 117 * Returns a Statement that, when executed, either returns normally if {@code method} passes, or 118 * throws an exception if {@code method} fails. 119 * 120 * <p>Here is an outline of the implementation: 121 * 122 * <ul> 123 * <li>Invoke each {@code journey} and {@code method} present in {@code mJourneyClasses} and 124 * {@code mFrameworkMethods} respectively on a separate thread and runs it concurrently. 125 * Throws any exceptions thrown by either operation. 126 * <li>ALWAYS run all non-overridden {@code @Before} methods on this class and superclasses 127 * before any of the previous steps; if any throws an Exception, stop execution and pass 128 * the exception on. Combines all {@link Before} annotation of all the journeys present in 129 * a scenario to run it. 130 * <li>ALWAYS run all non-overridden {@code @After} methods on this class and superclasses 131 * after any of the previous steps; all After methods are always executed: exceptions 132 * thrown by previous steps are combined, if necessary, with exceptions from After methods 133 * into a {@link MultipleFailureException}. Combines all {@link After} annotation of all 134 * the journeys present in a scenario to run it serially. 135 * <li>ALWAYS allow {@code @Rule} fields to modify the execution of the above steps. A {@code 136 * Rule} may prevent all execution of the above steps, or add additional behavior before 137 * and after, or modify thrown exceptions. For more information, see {@link TestRule} 138 * </ul> 139 * 140 * This can be overridden in subclasses, either by overriding this method, or the 141 * implementations creating each sub-statement. 142 * 143 * <p>For example, lets say a scenario has 2 tests and we want to execute this in parallel. 144 * TestClass1 with methods (rule1, beforeClass1, before1, testMethod1, after1, afterClass1). 145 * TestClass2 with methods (rule2, beforeClass2, before2, testMethod2, after2, afterClass2). 146 * 147 * <p>These methods below will run serially. rule1 -> rule2 ->beforeClass1 -> before1 148 * ->beforeClass2 -> before2 149 * 150 * <p>testMethod1 and testMethod2 will run in parallel. 151 * 152 * <p>Once both the test methods finishes, we will execute remaining methods serially. 153 * 154 * <p>after1 -> afterClass1 -> after2 -> afterClass2 -> rule2 (remaining code after base 155 * .evaluate()) -> rule1 (remaining code after base.evaluate()). 156 */ 157 @Override methodBlock(final FrameworkMethod method)158 protected Statement methodBlock(final FrameworkMethod method) { 159 // Runs the test present in the scenario concurrently. 160 Statement statement = new ConcurrentScenariosStatement(mJourneyClasses, mFrameworkMethods); 161 statement = withBefores(method, /* target = */ null, statement); 162 statement = withAfters(method, /* target = */ null, statement); 163 statement = withAllRules(statement); 164 statement = withInterruptIsolation(statement); 165 return statement; 166 } 167 168 /** 169 * Runs the {@link AfterClass} methods after running all the {@link After} methods of the test 170 * class. 171 * 172 * <p>Combines all {@link After} annotation of all the journeys present in a scenario to run it 173 * serially. 174 */ 175 @Override withAfters(FrameworkMethod method, Object target, Statement statement)176 protected Statement withAfters(FrameworkMethod method, Object target, Statement statement) { 177 for (int i = 0; i < mTestClasses.size(); i++) { 178 TestClass testClass = mTestClasses.get(i); 179 final List<FrameworkMethod> afterMethods = new ArrayList<>(); 180 afterMethods.addAll(testClass.getAnnotatedMethods(After.class)); 181 afterMethods.addAll(testClass.getAnnotatedMethods(AfterClass.class)); 182 if (!afterMethods.isEmpty()) { 183 statement = addRunAfters(statement, afterMethods, mJourneyClasses.get(i)); 184 } 185 } 186 return statement; 187 } 188 189 /** 190 * Runs the {@link BeforeClass} methods before running all the {@link Before} methods of the 191 * test class. 192 * 193 * <p>Combines all {@link Before} annotation of all the journeys present in a scenario to run it 194 * serially. 195 */ 196 @Override withBefores(FrameworkMethod method, Object target, Statement statement)197 protected Statement withBefores(FrameworkMethod method, Object target, Statement statement) { 198 for (int i = 0; i < mTestClasses.size(); i++) { 199 TestClass testClass = mTestClasses.get(i); 200 List<FrameworkMethod> allBeforeMethods = new ArrayList<>(); 201 allBeforeMethods.addAll(testClass.getAnnotatedMethods(BeforeClass.class)); 202 allBeforeMethods.addAll(testClass.getAnnotatedMethods(Before.class)); 203 if (!allBeforeMethods.isEmpty()) { 204 statement = addRunBefores(statement, allBeforeMethods, mJourneyClasses.get(i)); 205 } 206 } 207 return statement; 208 } 209 210 /** 211 * Override the parent {@code withBeforeClasses} method to be a no-op. 212 * 213 * <p>The {@link BeforeClass} methods will be included later as {@link Before} methods. 214 */ 215 @Override withBeforeClasses(Statement statement)216 protected Statement withBeforeClasses(Statement statement) { 217 return statement; 218 } 219 220 /** 221 * Override the parent {@code withAfterClasses} method to be a no-op. 222 * 223 * <p>The {@link AfterClass} methods will be included later as {@link After} methods. 224 */ 225 @Override withAfterClasses(Statement statement)226 protected Statement withAfterClasses(Statement statement) { 227 return statement; 228 } 229 addRunBefores( Statement statement, List<FrameworkMethod> befores, Object target)230 protected RunBefores addRunBefores( 231 Statement statement, List<FrameworkMethod> befores, Object target) { 232 return new RunBefores(statement, befores, target); 233 } 234 addRunAfters( Statement statement, List<FrameworkMethod> afters, Object target)235 protected RunAfters addRunAfters( 236 Statement statement, List<FrameworkMethod> afters, Object target) { 237 return new RunAfters(statement, afters, target); 238 } 239 getFrameworkMethod(Journey journey, TestClass testClass)240 private FrameworkMethod getFrameworkMethod(Journey journey, TestClass testClass) 241 throws Exception { 242 if (!journey.hasMethodName()) { 243 return testClass.getAnnotatedMethods(Test.class).get(0); 244 } 245 for (FrameworkMethod method : testClass.getAnnotatedMethods(Test.class)) { 246 if (method.getName().equals(journey.getMethodName())) { 247 return method; 248 } 249 } 250 throw new Exception( 251 String.format( 252 "Method name: %s not found in the testclass", journey.getMethodName())); 253 } 254 withAllRules(Statement statement)255 private Statement withAllRules(Statement statement) { 256 for (int i = 0; i < mTestClasses.size(); i++) { 257 statement = 258 withRules( 259 mFrameworkMethods.get(i), 260 mTestClasses.get(i), 261 mJourneyClasses.get(i), 262 statement); 263 } 264 return statement; 265 } 266 267 /* 268 This class is used when we instantiate the {@link ConcurrentRunner}. Parent class of 269 ConcurrentRunner invoke ParentRunner<> which validates test class has a single constructor 270 and annotations. This does not have any functional impact on the overall code. 271 ConcurrentRunner override various methods such as methodBlock which explicitly invokes the 272 method on a particular test class. 273 */ 274 @android.platform.test.scenario.annotation.Scenario 275 @RunWith(JUnit4.class) 276 public static class DummyTestClass { 277 @Test testDummyClass()278 public void testDummyClass() throws Exception {} 279 } 280 } 281