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