blob: 7afec4674fe87b6e4a939dfb6ac2bfcc2ee21eb5 [file] [log] [blame]
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.platform.test.longevity;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.longThat;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.MockitoAnnotations.initMocks;
import static java.lang.Math.abs;
import android.os.Bundle;
import android.platform.test.longevity.proto.Configuration.Scenario;
import android.platform.test.longevity.proto.Configuration.Scenario.AfterTest;
import android.platform.test.longevity.proto.Configuration.Scenario.ExtraArg;
import android.platform.test.longevity.samples.testing.SampleProfileSuite;
import androidx.test.InstrumentationRegistry;
import org.junit.Assert;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runner.notification.Failure;
import org.junit.runner.notification.RunListener;
import org.junit.runner.notification.RunNotifier;
import org.junit.runners.JUnit4;
import org.junit.runners.model.InitializationError;
import org.junit.runners.model.TestTimedOutException;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.exceptions.base.MockitoAssertionError;
import java.util.HashSet;
import java.util.List;
import java.util.concurrent.TimeUnit;
/** Unit tests for the {@link ScheduledScenarioRunner} runner. */
@RunWith(JUnit4.class)
public class ScheduledScenarioRunnerTest {
@Mock private RunNotifier mRunNotifier;
private static final String ASSERTION_FAILURE_MESSAGE = "Test assertion failed";
public static class ArgumentTest {
public static final String TEST_ARG = "test-arg-test-only";
public static final String TEST_ARG_DEFAULT = "default";
public static final String TEST_ARG_OVERRIDE = "not default";
@Before
public void setUp() {
// The actual argument testing happens here as this is where instrumentation args are
// parsed in the CUJs.
String argValue =
InstrumentationRegistry.getArguments().getString(TEST_ARG, TEST_ARG_DEFAULT);
Assert.assertEquals(ASSERTION_FAILURE_MESSAGE, argValue, TEST_ARG_OVERRIDE);
}
@Test
public void dummyTest() {
// Does nothing; always passes.
}
}
// Threshold above which missing a schedule is considered a failure.
private static final long TIMEOUT_ERROR_MARGIN_MS = 500;
// Holds the state of the instrumentation args before each test for restoring after, as one test
// might affect the state of another otherwise.
// TODO(b/124239142): Avoid manipulating the instrumentation args here.
private Bundle mArgumentsBeforeTest;
@Before
public void setUpSuite() throws InitializationError {
initMocks(this);
mArgumentsBeforeTest = InstrumentationRegistry.getArguments();
}
@After
public void restoreSuite() {
InstrumentationRegistry.registerInstance(
InstrumentationRegistry.getInstrumentation(), mArgumentsBeforeTest);
}
/**
* Test that an over time test causes a JUnit TestTimedOutException with the correct exception
* timeout.
*/
@Test
public void testOverTimeTest_throwsTestTimedOutException() throws InitializationError {
ArgumentCaptor<Failure> failureCaptor = ArgumentCaptor.forClass(Failure.class);
// Set a over time test with a 6-second window that will idle until the end of the window is
// reached.
long timeoutMs = TimeUnit.SECONDS.toMillis(6);
Scenario testScenario =
Scenario.newBuilder()
.setAt("00:00:00")
.setJourney(SampleProfileSuite.LongIdleTest.class.getName())
.setAfterTest(AfterTest.STAY_IN_APP)
.build();
ScheduledScenarioRunner runner =
spy(
new ScheduledScenarioRunner(
SampleProfileSuite.LongIdleTest.class,
testScenario,
timeoutMs,
true));
runner.run(mRunNotifier);
// Verify that a TestTimedOutException is fired and that the timeout is correct.
verify(mRunNotifier, atLeastOnce()).fireTestFailure(failureCaptor.capture());
List<Failure> failures = failureCaptor.getAllValues();
boolean correctTestTimedOutExceptionFired =
failures.stream()
.anyMatch(
f -> {
if (!(f.getException() instanceof TestTimedOutException)) {
return false;
}
TestTimedOutException exception =
(TestTimedOutException) f.getException();
long exceptionTimeout =
exception
.getTimeUnit()
.toMillis(exception.getTimeout());
long expectedTimeout =
timeoutMs - ScheduledScenarioRunner.TEARDOWN_LEEWAY_MS;
return abs(exceptionTimeout - expectedTimeout)
<= TIMEOUT_ERROR_MARGIN_MS;
});
Assert.assertTrue(correctTestTimedOutExceptionFired);
}
/** Test that an over time test does not idle before teardown. */
@Test
public void testOverTimeTest_doesNotIdleBeforeTeardown() throws InitializationError {
// Set a over time test with a 6-second window that will idle until the end of the window is
// reached.
Scenario testScenario =
Scenario.newBuilder()
.setAt("00:00:00")
.setJourney(SampleProfileSuite.LongIdleTest.class.getName())
.setAfterTest(AfterTest.STAY_IN_APP)
.build();
ScheduledScenarioRunner runner =
spy(
new ScheduledScenarioRunner(
SampleProfileSuite.LongIdleTest.class,
testScenario,
TimeUnit.SECONDS.toMillis(6),
true));
runner.run(mRunNotifier);
// There should not be idle before teardown as the test should not have left itself enough
// time for that.
verify(runner, never()).performIdleBeforeTeardown(anyLong());
}
/** Test that an over time test still idles until tne next scenario is supposed to begin. */
@Test
public void testOverTimeTest_idlesAfterTeardownUntilNextScenario() throws InitializationError {
// Set a over time test with a 6-second window that will idle until the end of the window is
// reached.
Scenario testScenario =
Scenario.newBuilder()
.setAt("00:00:00")
.setJourney(SampleProfileSuite.LongIdleTest.class.getName())
.setAfterTest(AfterTest.STAY_IN_APP)
.build();
ScheduledScenarioRunner runner =
spy(
new ScheduledScenarioRunner(
SampleProfileSuite.LongIdleTest.class,
testScenario,
TimeUnit.SECONDS.toMillis(6),
true));
runner.run(mRunNotifier);
// Verify that it still idles until the next scenario; duration should be roughly equal to
// the leeway set in @{link ScheduledScenarioRunner}.
verify(runner, times(1))
.performIdleBeforeNextScenario(
getWithinMarginMatcher(
ScheduledScenarioRunner.TEARDOWN_LEEWAY_MS,
TIMEOUT_ERROR_MARGIN_MS));
}
/** Test that a test set to stay in the app after the test idles after its @Test method. */
@Test
public void testRespectsAfterTestPolicy_stayInApp() throws InitializationError {
// Set a passing test with a 6-second timeout that will idle after its @Test method and
// idle until the end of the timeout is reached.
long timeoutMs = TimeUnit.SECONDS.toMillis(6);
Scenario testScenario =
Scenario.newBuilder()
.setAt("00:00:00")
.setJourney(SampleProfileSuite.PassingTest.class.getName())
.setAfterTest(AfterTest.STAY_IN_APP)
.build();
ScheduledScenarioRunner runner =
spy(
new ScheduledScenarioRunner(
SampleProfileSuite.PassingTest.class,
testScenario,
timeoutMs,
true));
runner.run(mRunNotifier);
// Idles before teardown; duration should be roughly equal to the timeout minus two times
// the leeway set in {@link ScheduledScenarioRunner}. Please see that class to see why two
// times the leeway is expected here.
verify(runner, times(1))
.performIdleBeforeTeardown(
getWithinMarginMatcher(
timeoutMs - 2 * ScheduledScenarioRunner.TEARDOWN_LEEWAY_MS,
TIMEOUT_ERROR_MARGIN_MS));
// Test should have passed.
verify(mRunNotifier, never()).fireTestFailure(any(Failure.class));
}
/** Test that a test set to exit the app after the test does not idle after its @Test method. */
@Test
public void testRespectsAfterTestPolicy_exit() throws InitializationError {
// Set a passing test with a 6-second timeout that does not idle after its @Test method and
// will idle until the end of the timeout is reached.
long timeoutMs = TimeUnit.SECONDS.toMillis(6);
Scenario testScenario =
Scenario.newBuilder()
.setAt("00:00:00")
.setJourney(SampleProfileSuite.PassingTest.class.getName())
.setAfterTest(AfterTest.EXIT)
.build();
ScheduledScenarioRunner runner =
spy(
new ScheduledScenarioRunner(
SampleProfileSuite.PassingTest.class,
testScenario,
timeoutMs,
true));
runner.run(mRunNotifier);
// There should not be idle before teardown.
verify(runner, never()).performIdleBeforeTeardown(anyLong());
// Idles before the next scenario; duration should be roughly equal to the timeout.
verify(runner, times(1))
.performIdleBeforeNextScenario(
getWithinMarginMatcher(timeoutMs, TIMEOUT_ERROR_MARGIN_MS));
// Test should have passed.
verify(mRunNotifier, never()).fireTestFailure(any(Failure.class));
}
/** Test that an ignored scenario still includes the timeout dictated in a profile. */
@Test
public void testIgnoredScenario_doesIdle() throws InitializationError, Exception {
long timeoutMs = TimeUnit.SECONDS.toMillis(6);
Scenario testScenario =
Scenario.newBuilder()
.setAt("00:00:00")
.setJourney(SampleProfileSuite.PassingTest.class.getName())
.setAfterTest(AfterTest.EXIT)
.build();
Bundle ignores = new Bundle();
ignores.putString(
LongevityClassRunner.FILTER_OPTION,
SampleProfileSuite.PassingTest.class.getCanonicalName());
ScheduledScenarioRunner runner =
spy(
new ScheduledScenarioRunner(
SampleProfileSuite.PassingTest.class,
testScenario,
timeoutMs,
true,
ignores));
RunNotifier notifier = spy(new RunNotifier());
RunListener listener = mock(RunListener.class);
notifier.addListener(listener);
runner.run(notifier);
// There should not be idle before teardown.
verify(runner, never()).performIdleBeforeTeardown(anyLong());
// Ensure the test was ignored via listener.
verify(listener, times(1)).testIgnored(any());
// Idles before the next scenario; duration should be roughly equal to the timeout.
verify(runner, times(1))
.performIdleBeforeNextScenario(
getWithinMarginMatcher(timeoutMs, TIMEOUT_ERROR_MARGIN_MS));
}
/** Test that the last test does not have idle after it, regardless of its AfterTest policy. */
@Test
public void testLastScenarioDoesNotIdle() throws InitializationError {
// Set a passing test with a 6-second timeout that is set to idle after its @Test method and
// but should not idle as it will be the last test in practice.
Scenario testScenario =
Scenario.newBuilder()
.setAt("00:00:00")
.setJourney(SampleProfileSuite.PassingTest.class.getName())
.setAfterTest(AfterTest.STAY_IN_APP)
.build();
ScheduledScenarioRunner runner =
spy(
new ScheduledScenarioRunner(
SampleProfileSuite.PassingTest.class,
testScenario,
TimeUnit.SECONDS.toMillis(6),
false));
runner.run(mRunNotifier);
// There should not be idle of any form.
verify(runner, never()).performIdleBeforeTeardown(anyLong());
verify(runner, never()).performIdleBeforeNextScenario(anyLong());
}
/** Test that the "extras" in a scenario is properly registered before the test. */
@Test
public void testExtraArgs_registeredBeforeTest() throws Throwable {
Scenario testScenario =
Scenario.newBuilder()
.setAt("00:00:00")
.setJourney(ArgumentTest.class.getName())
.setAfterTest(AfterTest.STAY_IN_APP)
.addExtras(
ExtraArg.newBuilder()
.setKey(ArgumentTest.TEST_ARG)
.setValue(ArgumentTest.TEST_ARG_OVERRIDE))
.build();
ScheduledScenarioRunner runner =
spy(
new ScheduledScenarioRunner(
ArgumentTest.class,
testScenario,
TimeUnit.SECONDS.toMillis(6),
false));
runner.run(mRunNotifier);
verifyForAssertionFailures(mRunNotifier);
}
/** Test that the "extras" in a scenario is properly un-registered after the test. */
@Test
public void testExtraArgs_unregisteredAfterTest() throws Throwable {
Bundle argsBeforeTest = InstrumentationRegistry.getArguments();
Scenario testScenario =
Scenario.newBuilder()
.setAt("00:00:00")
.setJourney(ArgumentTest.class.getName())
.setAfterTest(AfterTest.STAY_IN_APP)
.addExtras(
ExtraArg.newBuilder()
.setKey(ArgumentTest.TEST_ARG)
.setValue(ArgumentTest.TEST_ARG_OVERRIDE))
.build();
ScheduledScenarioRunner runner =
new ScheduledScenarioRunner(
ArgumentTest.class, testScenario, TimeUnit.SECONDS.toMillis(6), false);
runner.run(mRunNotifier);
Bundle argsAfterTest = InstrumentationRegistry.getArguments();
Assert.assertTrue(bundlesContainSameStringKeyValuePairs(argsBeforeTest, argsAfterTest));
}
/**
* Helper method to get an argument matcher that checks whether the input value is equal to
* expected value within a margin.
*/
private long getWithinMarginMatcher(long expected, long margin) {
return longThat(duration -> abs(duration - expected) <= margin);
}
/**
* Verify that no test failure is fired because of an assertion failure in the stubbed methods.
* If the verfication fails, check whether it's due the injected assertions failing. If yes,
* throw that exception out; otherwise, throw the first exception.
*/
private void verifyForAssertionFailures(final RunNotifier notifier) throws Throwable {
try {
verify(notifier, never()).fireTestFailure(any());
} catch (MockitoAssertionError e) {
ArgumentCaptor<Failure> failureCaptor = ArgumentCaptor.forClass(Failure.class);
verify(notifier, atLeastOnce()).fireTestFailure(failureCaptor.capture());
List<Failure> failures = failureCaptor.getAllValues();
// Go through the failures, look for an known failure case from the above exceptions
// and throw the exception in the first one out if any.
for (Failure failure : failures) {
if (failure.getException().getMessage().contains(ASSERTION_FAILURE_MESSAGE)) {
throw failure.getException();
}
}
// Otherwise, throw the exception from the first failure reported.
throw failures.get(0).getException();
}
}
/**
* Helper method to check whether two {@link Bundle}s are equal since the built-in {@code
* equals} is not properly overriden.
*/
private boolean bundlesContainSameStringKeyValuePairs(Bundle b1, Bundle b2) {
if (b1.size() != b2.size()) {
return false;
}
HashSet<String> allKeys = new HashSet<String>(b1.keySet());
allKeys.addAll(b2.keySet());
for (String key : allKeys) {
if (b1.getString(key) != null) {
// If key is in b1 and corresponds to a string, check whether this key corresponds
// to the same value in b2.
if (!b1.getString(key).equals(b2.getString(key))) {
return false;
}
} else if (b2.getString(key) != null) {
// Otherwise if b2 has a string at this key, return false since we know that b1 does
// not have a string at this key.
return false;
}
}
return true;
}
}