• 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.util.function.BiFunction;
34 import java.util.HashMap;
35 import java.util.List;
36 import java.util.Map;
37 
38 import org.junit.runner.Description;
39 import org.junit.runner.Runner;
40 import org.junit.runner.notification.RunNotifier;
41 import org.junit.runners.BlockJUnit4ClassRunner;
42 import org.junit.runners.model.InitializationError;
43 import org.junit.runners.model.RunnerBuilder;
44 
45 /**
46  * {@inheritDoc}
47  *
48  * This class is used for constructing longevity suites that run on an Android device.
49  */
50 public class LongevitySuite extends android.host.test.longevity.LongevitySuite {
51     private static final String LOG_TAG = LongevitySuite.class.getSimpleName();
52 
53     public static final String RENAME_ITERATION_OPTION = "rename-iterations";
54     private boolean mRenameIterations;
55 
56     private Instrumentation mInstrumentation;
57     private Context mContext;
58 
59     // Cached {@link TimeoutTerminator} instance.
60     private TimeoutTerminator mTimeoutTerminator;
61 
62     private Map<Description, Integer> mIterations = new HashMap<>();
63 
64     /**
65      * Takes a {@link Bundle} and maps all String K/V pairs into a {@link Map<String, String>}.
66      *
67      * @param bundle the input arguments to return in a {@link Map}
68      * @return Map<String, String> all String-to-String key, value pairs in the {@link Bundle}
69      */
toMap(Bundle bundle)70     protected static final Map<String, String> toMap(Bundle bundle) {
71         Map<String, String> result = new HashMap<>();
72         for (String key : bundle.keySet()) {
73             if (!bundle.containsKey(key)) {
74                 Log.w(LOG_TAG, String.format("Couldn't find value for option: %s", key));
75             } else {
76                 // Arguments are assumed String <-> String
77                 result.put(key, bundle.getString(key));
78             }
79         }
80         return result;
81     }
82 
83     /**
84      * Called reflectively on classes annotated with {@code @RunWith(LongevitySuite.class)}
85      */
LongevitySuite(Class<?> klass, RunnerBuilder builder)86     public LongevitySuite(Class<?> klass, RunnerBuilder builder)
87             throws InitializationError {
88         this(klass, builder, InstrumentationRegistry.getInstrumentation(),
89                 InstrumentationRegistry.getContext(), InstrumentationRegistry.getArguments());
90     }
91 
92     /**
93      * Enables subclasses, e.g.{@link ProfileSuite}, to constuct a suite using its own list of
94      * Runners.
95      */
LongevitySuite(Class<?> klass, List<Runner> runners, Bundle args)96     protected LongevitySuite(Class<?> klass, List<Runner> runners, Bundle args)
97             throws InitializationError {
98         super(klass, runners, toMap(args));
99         mInstrumentation = InstrumentationRegistry.getInstrumentation();
100         mContext = InstrumentationRegistry.getContext();
101 
102         // Parse out additional options.
103         mRenameIterations = Boolean.valueOf(args.getString(RENAME_ITERATION_OPTION));
104     }
105 
106     /**
107      * Used to pass in mock-able Android features for testing.
108      */
109     @VisibleForTesting
LongevitySuite(Class<?> klass, RunnerBuilder builder, Instrumentation instrumentation, Context context, Bundle arguments)110     public LongevitySuite(Class<?> klass, RunnerBuilder builder,
111             Instrumentation instrumentation, Context context, Bundle arguments)
112             throws InitializationError {
113         this(klass, constructClassRunners(klass, builder, arguments), arguments);
114         // Overwrite instrumentation and context here with the passed-in objects.
115         mInstrumentation = instrumentation;
116         mContext = context;
117     }
118 
119     /**
120      * Constructs the sequence of {@link Runner}s using platform composers.
121      */
constructClassRunners( Class<?> suite, RunnerBuilder builder, Bundle args)122     private static List<Runner> constructClassRunners(
123                 Class<?> suite, RunnerBuilder builder, Bundle args)
124             throws InitializationError {
125         // TODO(b/118340229): Refactor to share logic with base class. In the meanwhile, keep the
126         // logic here in sync with the base class.
127         // Retrieve annotated suite classes.
128         SuiteClasses annotation = suite.getAnnotation(SuiteClasses.class);
129         if (annotation == null) {
130             throw new InitializationError(String.format(
131                     "Longevity suite, '%s', must have a SuiteClasses annotation", suite.getName()));
132         }
133         // Validate that runnable scenarios are passed into the suite.
134         for (Class<?> scenario : annotation.value()) {
135             Runner runner = null;
136             try {
137                 runner = builder.runnerForClass(scenario);
138             } catch (Throwable t) {
139                 throw new InitializationError(t);
140             }
141             // All scenarios must extend BlockJUnit4ClassRunner.
142             if (!(runner instanceof BlockJUnit4ClassRunner)) {
143                 throw new InitializationError(
144                         String.format(
145                                 "All runners must extend BlockJUnit4ClassRunner. %s:%s doesn't.",
146                                 runner.getClass(), runner.getDescription().getDisplayName()));
147             }
148         }
149         // Construct and store custom runners for the full suite.
150         BiFunction<Bundle, List<Runner>, List<Runner>> modifier =
151                 new Iterate<Runner>().andThen(new Shuffle<Runner>());
152         return modifier.apply(args, builder.runners(suite, annotation.value()));
153     }
154 
155     @Override
run(final RunNotifier notifier)156     public void run(final RunNotifier notifier) {
157         // Register the battery terminator available only on the platform library, if present.
158         if (hasBattery()) {
159             notifier.addListener(new BatteryTerminator(notifier, mArguments, mContext));
160         }
161         // Register other listeners and continue with standard longevity run.
162         super.run(notifier);
163     }
164 
165     @Override
runChild(Runner runner, final RunNotifier notifier)166     protected void runChild(Runner runner, final RunNotifier notifier) {
167         // Update iterations.
168         mIterations.computeIfPresent(runner.getDescription(), (k, v) -> v + 1);
169         mIterations.computeIfAbsent(runner.getDescription(), k -> 1);
170 
171         LongevityClassRunner suiteRunner = getSuiteRunner(runner);
172         if (mRenameIterations) {
173             suiteRunner.setIteration(mIterations.get(runner.getDescription()));
174         }
175         super.runChild(suiteRunner, notifier);
176     }
177 
178     /**
179      * Returns the platform-specific {@link TimeoutTerminator} for Android devices.
180      */
181     @Override
getErrorTerminator( final RunNotifier notifier)182     public android.host.test.longevity.listener.ErrorTerminator getErrorTerminator(
183             final RunNotifier notifier) {
184         return new ErrorTerminator(notifier);
185     }
186 
187     /**
188      * Returns the platform-specific {@link TimeoutTerminator} for Android devices.
189      *
190      * <p>This method will always return the same {@link TimeoutTerminator} instance.
191      */
192     @Override
getTimeoutTerminator( final RunNotifier notifier)193     public android.host.test.longevity.listener.TimeoutTerminator getTimeoutTerminator(
194             final RunNotifier notifier) {
195         if (mTimeoutTerminator == null) {
196             mTimeoutTerminator = new TimeoutTerminator(notifier, mArguments);
197         }
198         return mTimeoutTerminator;
199     }
200 
201     /** Returns the timeout set on the suite in milliseconds. */
getSuiteTimeoutMs()202     public long getSuiteTimeoutMs() {
203         if (mTimeoutTerminator == null) {
204             throw new IllegalStateException("No suite timeout is set. This should never happen.");
205         }
206         return mTimeoutTerminator.getTotalSuiteTimeoutMs();
207     }
208 
209     /**
210      * Returns a {@link Runner} specific for the suite, if any. Can be overriden by subclasses to
211      * supply different runner implementations.
212      */
getSuiteRunner(Runner runner)213     protected LongevityClassRunner getSuiteRunner(Runner runner) {
214         try {
215             // Cast is safe as we verified the runner is BlockJUnit4Runner at initialization.
216             return new LongevityClassRunner(
217                     ((BlockJUnit4ClassRunner) runner).getTestClass().getJavaClass());
218         } catch (InitializationError e) {
219             throw new RuntimeException(
220                     String.format(
221                             "Unable to run scenario %s with a longevity-specific runner.",
222                             runner.getDescription().getDisplayName()),
223                     e);
224         }
225     }
226 
227     /**
228      * Determines if the device has a battery attached.
229      */
hasBattery()230     private boolean hasBattery () {
231         final Intent batteryInfo =
232                 mContext.registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
233         return batteryInfo.getBooleanExtra(BatteryManager.EXTRA_PRESENT, true);
234     }
235 }
236