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