1 /* 2 * Copyright (C) 2019 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 17 package android.platform.test.longevity; 18 19 import static java.lang.Math.max; 20 21 import android.os.Bundle; 22 import android.os.SystemClock; 23 import android.platform.test.longevity.proto.Configuration.Scenario; 24 import android.platform.test.longevity.proto.Configuration.Scenario.ExtraArg; 25 import androidx.annotation.VisibleForTesting; 26 import androidx.test.InstrumentationRegistry; 27 28 import java.util.List; 29 30 import org.junit.rules.TestRule; 31 import org.junit.rules.Timeout; 32 import org.junit.runner.notification.RunNotifier; 33 import org.junit.runners.BlockJUnit4ClassRunner; 34 import org.junit.runners.model.FrameworkMethod; 35 import org.junit.runners.model.InitializationError; 36 import org.junit.runners.model.Statement; 37 38 /** 39 * A {@link BlockJUnit4ClassRunner} that runs a test class with a specified timeout and optionally 40 * performs an idle before teardown (staying inside the app for Android CUJs). 41 */ 42 public class ScheduledScenarioRunner extends LongevityClassRunner { 43 @VisibleForTesting static final long ENDTIME_LEEWAY_MS = 3000; 44 45 private final Scenario mScenario; 46 private final long mTotalTimeoutMs; 47 private final boolean mShouldIdle; 48 private final Bundle mArguments; 49 50 private long mStartTimeMs; 51 ScheduledScenarioRunner( Class<?> klass, Scenario scenario, long timeout, boolean shouldIdle)52 public ScheduledScenarioRunner( 53 Class<?> klass, Scenario scenario, long timeout, boolean shouldIdle) 54 throws InitializationError { 55 this(klass, scenario, timeout, shouldIdle, InstrumentationRegistry.getArguments()); 56 } 57 58 @VisibleForTesting ScheduledScenarioRunner( Class<?> klass, Scenario scenario, long timeout, boolean shouldIdle, Bundle arguments)59 ScheduledScenarioRunner( 60 Class<?> klass, Scenario scenario, long timeout, boolean shouldIdle, Bundle arguments) 61 throws InitializationError { 62 super(klass, arguments); 63 mScenario = scenario; 64 // Ensure that the timeout is non-negative. 65 mTotalTimeoutMs = max(timeout, 0); 66 mShouldIdle = shouldIdle; 67 mArguments = arguments; 68 } 69 70 @Override getTestRules(Object target)71 protected List<TestRule> getTestRules(Object target) { 72 List<TestRule> rules = super.getTestRules(target); 73 // Ensure that the timeout rule has a non-negative timeout. 74 rules.add(0, Timeout.millis(max(mTotalTimeoutMs - ENDTIME_LEEWAY_MS, 0))); 75 return rules; 76 } 77 78 @Override withAfters(FrameworkMethod method, Object target, Statement statement)79 protected Statement withAfters(FrameworkMethod method, Object target, Statement statement) { 80 Statement withIdle = 81 new Statement() { 82 @Override 83 public void evaluate() throws Throwable { 84 try { 85 // Run the underlying test and report exceptions. 86 statement.evaluate(); 87 } finally { 88 // If there is time left for idling (i.e. more than ENDTIME_LEEWAY_MS), 89 // and the scenario is set to stay in app, idle for the remainder of 90 // its timeout window until ENDTIME_LEEWAY_MS before the start time of 91 // the next scenario, before executing the scenario's @After methods. 92 // The above does not apply if current scenario is the last one, in 93 // which case the idle is never performed regardless of its after_test 94 // policy. 95 if (mShouldIdle 96 && mScenario 97 .getAfterTest() 98 .equals(Scenario.AfterTest.STAY_IN_APP)) { 99 performIdleBeforeTeardown( 100 max(getTimeRemaining() - ENDTIME_LEEWAY_MS, 0)); 101 } 102 } 103 } 104 }; 105 return super.withAfters(method, target, withIdle); 106 } 107 108 @Override runChild(final FrameworkMethod method, RunNotifier notifier)109 protected void runChild(final FrameworkMethod method, RunNotifier notifier) { 110 mStartTimeMs = System.currentTimeMillis(); 111 // Keep a copy of the bundle arguments for restoring later. 112 Bundle modifiedArguments = mArguments.deepCopy(); 113 for (ExtraArg argPair : mScenario.getExtrasList()) { 114 if (argPair.getKey() == null || argPair.getValue() == null) { 115 throw new IllegalArgumentException( 116 String.format( 117 "Each extra arg entry in scenario must have both a key and a value," 118 + " but scenario is %s.", 119 mScenario.toString())); 120 } 121 modifiedArguments.putString(argPair.getKey(), argPair.getValue()); 122 } 123 InstrumentationRegistry.registerInstance( 124 InstrumentationRegistry.getInstrumentation(), modifiedArguments); 125 super.runChild(method, notifier); 126 // Restore the arguments to the state prior to the scenario. 127 InstrumentationRegistry.registerInstance( 128 InstrumentationRegistry.getInstrumentation(), mArguments); 129 // If there are remaining scenarios, idle until the next one starts. 130 if (mShouldIdle) { 131 performIdleBeforeNextScenario(getTimeRemaining()); 132 } 133 } 134 135 /** 136 * Get the duration to idle after the current scenario. If the current scenario is the last one 137 * in the profile, returns 0. 138 */ getTimeRemaining()139 private long getTimeRemaining() { 140 // The idle time is total time minus time elapsed since the current scenario started. 141 return max(mTotalTimeoutMs - (System.currentTimeMillis() - mStartTimeMs), 0); 142 } 143 144 @VisibleForTesting performIdleBeforeTeardown(long durationMs)145 protected void performIdleBeforeTeardown(long durationMs) { 146 idleWithSystemClockSleep(durationMs); 147 } 148 149 @VisibleForTesting performIdleBeforeNextScenario(long durationMs)150 protected void performIdleBeforeNextScenario(long durationMs) { 151 // TODO (b/119386011): Change this idle method to using a sleep test; for now, using the 152 // same idling logic as {@link performIdleBeforeTeardown}. 153 idleWithSystemClockSleep(durationMs); 154 } 155 idleWithSystemClockSleep(long durationMs)156 private void idleWithSystemClockSleep(long durationMs) { 157 if (durationMs <= 0) { 158 return; 159 } 160 SystemClock.sleep(durationMs); 161 } 162 } 163