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