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