1 /*
2  * Copyright (C) 2017 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 package android.platform.test.longevity;
17 
18 import android.app.Instrumentation;
19 import android.content.Context;
20 import android.content.Intent;
21 import android.content.IntentFilter;
22 import android.os.BatteryManager;
23 import android.os.Bundle;
24 import android.platform.test.composer.Iterate;
25 import android.platform.test.composer.Shuffle;
26 import android.platform.test.longevity.listener.BatteryTerminator;
27 import android.platform.test.longevity.listener.ErrorTerminator;
28 import android.platform.test.longevity.listener.TimeoutTerminator;
29 import android.util.Log;
30 
31 import androidx.annotation.VisibleForTesting;
32 import androidx.test.InstrumentationRegistry;
33 
34 import org.junit.internal.builders.IgnoredClassRunner;
35 import org.junit.internal.runners.ErrorReportingRunner;
36 import org.junit.runner.Description;
37 import org.junit.runner.Runner;
38 import org.junit.runner.notification.RunNotifier;
39 import org.junit.runners.BlockJUnit4ClassRunner;
40 import org.junit.runners.model.InitializationError;
41 import org.junit.runners.model.RunnerBuilder;
42 
43 import java.lang.reflect.Field;
44 import java.util.ArrayList;
45 import java.util.HashMap;
46 import java.util.List;
47 import java.util.Map;
48 import java.util.function.BiFunction;
49 
50 /**
51  * {@inheritDoc}
52  *
53  * This class is used for constructing longevity suites that run on an Android device.
54  */
55 public class LongevitySuite extends android.host.test.longevity.LongevitySuite {
56     private static final String LOG_TAG = LongevitySuite.class.getSimpleName();
57 
58     public static final String RENAME_ITERATION_OPTION = "rename-iterations";
59     private final boolean mRenameIterations;
60 
61     private Context mContext;
62 
63     // Cached {@link TimeoutTerminator} instance.
64     private TimeoutTerminator mTimeoutTerminator;
65 
66     private final Map<Description, Integer> mIterations = new HashMap<>();
67 
68     /**
69      * Takes a {@link Bundle} and maps all String K/V pairs into a {@link Map<String, String>}.
70      *
71      * @param bundle the input arguments to return in a {@link Map}
72      * @return a {@code Map<String, String>} of all key, value pairs in {@code bundle}.
73      */
toMap(Bundle bundle)74     protected static final Map<String, String> toMap(Bundle bundle) {
75         Map<String, String> result = new HashMap<>();
76         for (String key : bundle.keySet()) {
77             if (!bundle.containsKey(key)) {
78                 Log.w(LOG_TAG, String.format("Couldn't find value for option: %s", key));
79             } else {
80                 // Arguments are assumed String <-> String
81                 result.put(key, bundle.getString(key));
82             }
83         }
84         return result;
85     }
86 
87     /**
88      * Called reflectively on classes annotated with {@code @RunWith(LongevitySuite.class)}
89      */
LongevitySuite(Class<?> klass, RunnerBuilder builder)90     public LongevitySuite(Class<?> klass, RunnerBuilder builder)
91             throws InitializationError {
92         this(
93                 klass,
94                 builder,
95                 new ArrayList<Runner>(),
96                 InstrumentationRegistry.getInstrumentation(),
97                 InstrumentationRegistry.getContext(),
98                 InstrumentationRegistry.getArguments());
99     }
100 
101     /** Used to dynamically pass in test classes to run as part of the suite in subclasses. */
LongevitySuite(Class<?> klass, RunnerBuilder builder, List<Runner> additional)102     public LongevitySuite(Class<?> klass, RunnerBuilder builder, List<Runner> additional)
103             throws InitializationError {
104         this(
105                 klass,
106                 builder,
107                 additional,
108                 InstrumentationRegistry.getInstrumentation(),
109                 InstrumentationRegistry.getContext(),
110                 InstrumentationRegistry.getArguments());
111     }
112 
113     /**
114      * Enables subclasses, e.g.{@link ProfileSuite}, to construct a suite using its own list of
115      * Runners.
116      */
LongevitySuite(Class<?> klass, List<Runner> runners, Bundle args)117     protected LongevitySuite(Class<?> klass, List<Runner> runners, Bundle args)
118             throws InitializationError {
119         super(klass, runners, toMap(args));
120         mContext = InstrumentationRegistry.getContext();
121 
122         // Parse out additional options.
123         mRenameIterations = Boolean.parseBoolean(args.getString(RENAME_ITERATION_OPTION));
124     }
125 
126     /** Used to pass in mock-able Android features for testing. */
127     @VisibleForTesting
LongevitySuite( Class<?> klass, RunnerBuilder builder, List<Runner> additional, Instrumentation instrumentation, Context context, Bundle arguments)128     public LongevitySuite(
129             Class<?> klass,
130             RunnerBuilder builder,
131             List<Runner> additional,
132             Instrumentation instrumentation,
133             Context context,
134             Bundle arguments)
135             throws InitializationError {
136         this(klass, constructClassRunners(klass, additional, builder, arguments), arguments);
137         // Overwrite instrumentation and context here with the passed-in objects.
138         mContext = context;
139     }
140 
141     /** Constructs the sequence of {@link Runner}s using platform composers. */
constructClassRunners( Class<?> suite, List<Runner> additional, RunnerBuilder builder, Bundle args)142     protected static List<Runner> constructClassRunners(
143             Class<?> suite, List<Runner> additional, RunnerBuilder builder, Bundle args)
144             throws InitializationError {
145         // TODO(b/118340229): Refactor to share logic with base class. In the meanwhile, keep the
146         // logic here in sync with the base class.
147         // Retrieve annotated suite classes.
148         SuiteClasses annotation = suite.getAnnotation(SuiteClasses.class);
149         if (annotation == null) {
150             throw new InitializationError(String.format(
151                     "Longevity suite, '%s', must have a SuiteClasses annotation", suite.getName()));
152         }
153         // Validate that runnable scenarios are passed into the suite.
154         for (Class<?> scenario : annotation.value()) {
155             Runner runner = null;
156             try {
157                 runner = builder.runnerForClass(scenario);
158             } catch (Throwable t) {
159                 throw new InitializationError(t);
160             }
161             // If a scenario is an ErrorReportingRunner, an InitializationError has occurred when
162             // initializing the runner. Throw out a new error with the causes.
163             if (runner instanceof ErrorReportingRunner) {
164                 throw new InitializationError(getCauses((ErrorReportingRunner) runner));
165             }
166             // All scenarios must extend BlockJUnit4ClassRunner or be ignored.
167             if (!(runner instanceof BlockJUnit4ClassRunner) && !isIgnoredRunner(runner)) {
168                 throw new InitializationError(
169                         String.format(
170                                 "All runners must extend BlockJUnit4ClassRunner. %s:%s doesn't.",
171                                 runner.getClass(), runner.getDescription().getDisplayName()));
172             }
173         }
174         // Combine annotated runners and additional ones.
175         List<Runner> runners = builder.runners(suite, annotation.value());
176         runners.addAll(additional);
177         // Apply the modifiers to construct the full suite.
178         BiFunction<Bundle, List<Runner>, List<Runner>> modifier =
179                 new Iterate<Runner>().andThen(new Shuffle<Runner>());
180         return modifier.apply(args, runners);
181     }
182 
183     @Override
run(final RunNotifier notifier)184     public void run(final RunNotifier notifier) {
185         // Register the battery terminator available only on the platform library, if present.
186         if (hasBattery()) {
187             notifier.addListener(new BatteryTerminator(notifier, mArguments, mContext));
188         }
189         // Register other listeners and continue with standard longevity run.
190         super.run(notifier);
191     }
192 
193     @Override
runChild(Runner runner, final RunNotifier notifier)194     protected void runChild(Runner runner, final RunNotifier notifier) {
195         // Update iterations.
196         mIterations.computeIfPresent(runner.getDescription(), (k, v) -> v + 1);
197         mIterations.computeIfAbsent(runner.getDescription(), k -> 1);
198 
199         if (isIgnoredRunner(runner)) {
200             runner.run(notifier);
201             return;
202         }
203 
204         LongevityClassRunner suiteRunner = getSuiteRunner(runner);
205         if (mRenameIterations) {
206             suiteRunner.setIteration(mIterations.get(runner.getDescription()));
207         }
208         super.runChild(suiteRunner, notifier);
209     }
210 
211     /** Returns the platform-specific {@link ErrorTerminator} for an Android device. */
212     @Override
getErrorTerminator( final RunNotifier notifier)213     public android.host.test.longevity.listener.ErrorTerminator getErrorTerminator(
214             final RunNotifier notifier) {
215         return new ErrorTerminator(notifier);
216     }
217 
218     /**
219      * Returns the platform-specific {@link TimeoutTerminator} for an Android device.
220      *
221      * <p>This method will always return the same {@link TimeoutTerminator} instance.
222      */
223     @Override
getTimeoutTerminator( final RunNotifier notifier)224     public android.host.test.longevity.listener.TimeoutTerminator getTimeoutTerminator(
225             final RunNotifier notifier) {
226         if (mTimeoutTerminator == null) {
227             mTimeoutTerminator = new TimeoutTerminator(notifier, mArguments);
228         }
229         return mTimeoutTerminator;
230     }
231 
232     /** Returns the timeout set on the suite in milliseconds. */
getSuiteTimeoutMs()233     public long getSuiteTimeoutMs() {
234         if (mTimeoutTerminator == null) {
235             throw new IllegalStateException("No suite timeout is set. This should never happen.");
236         }
237         return mTimeoutTerminator.getTotalSuiteTimeoutMs();
238     }
239 
240     /**
241      * Returns a {@link Runner} specific for the suite, if any. Can be overriden by subclasses to
242      * supply different runner implementations.
243      */
getSuiteRunner(Runner runner)244     protected LongevityClassRunner getSuiteRunner(Runner runner) {
245         try {
246             // Cast is safe as we verified the runner is BlockJUnit4Runner at initialization.
247             return new LongevityClassRunner(
248                     ((BlockJUnit4ClassRunner) runner).getTestClass().getJavaClass());
249         } catch (InitializationError e) {
250             throw new RuntimeException(
251                     String.format(
252                             "Unable to run scenario %s with a longevity-specific runner.",
253                             runner.getDescription().getDisplayName()),
254                     e);
255         }
256     }
257 
258     /**
259      * Determines if the device has a battery attached.
260      */
hasBattery()261     private boolean hasBattery () {
262         final Intent batteryInfo =
263                 mContext.registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
264         return batteryInfo.getBooleanExtra(BatteryManager.EXTRA_PRESENT, true);
265     }
266 
isIgnoredRunner(Runner runner)267     protected static boolean isIgnoredRunner(Runner runner) {
268         return runner instanceof IgnoredClassRunner;
269     }
270 
271     /** Gets the first cause out of a {@link ErrorReportingRunner}. Also logs the rest. */
getCauses(ErrorReportingRunner runner)272     private static List<Throwable> getCauses(ErrorReportingRunner runner) {
273         // Reflection is used for this operation as the runner itself does not allow the errors
274         // to be read directly, and masks everything as an InitializationError in its description,
275         // which is not very useful.
276         // It is ok to throw RuntimeException here as we have already entered a failure state. It is
277         // helpful to know that a ErrorReportingRunner has occurred even if we can't decipher it.
278         try {
279             Field causesField = runner.getClass().getDeclaredField("causes");
280             causesField.setAccessible(true);
281             return (List<Throwable>) causesField.get(runner);
282         } catch (NoSuchFieldException e) {
283             throw new RuntimeException(
284                     String.format(
285                             "Unable to find a \"causes\" field in the ErrorReportingRunner %s.",
286                             runner.getDescription()),
287                     e);
288         } catch (IllegalAccessException e) {
289             throw new RuntimeException(
290                     String.format(
291                             "Unable to access the \"causes\" field in the ErrorReportingRunner %s.",
292                             runner.getDescription()),
293                     e);
294         }
295     }
296 }
297