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