blob: 575c1893de8c4be22d505e3660a365563618e524 [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 com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.argThat;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
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 android.os.Bundle;
import android.platform.test.rule.TestWatcher;
import java.util.ArrayList;
import java.util.List;
import org.junit.Assert;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.Description;
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.FrameworkMethod;
import org.junit.runners.model.Statement;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.exceptions.base.MockitoAssertionError;
/** Unit tests for the {@link LongevityClassRunner}. */
public class LongevityClassRunnerTest {
// Used to test things that are generated reflectively, e.g. dynamic rules.
private static final List<String> sLogs = new ArrayList<>();
public static class LoggingRule extends TestWatcher {
private final String mName;
public LoggingRule(String name) {
mName = name;
}
@Override
public void starting(Description description) {
sLogs.add(String.format("%s starting", mName));
}
@Override
public void finished(Description description) {
sLogs.add(String.format("%s finished", mName));
}
}
public static class InjectedRule1 extends LoggingRule {
public InjectedRule1() {
super("Injected rule 1");
}
}
public static class InjectedRule2 extends LoggingRule {
public InjectedRule2() {
super("Injected rule 2");
}
}
// We use two @Test methods here to distinguish between testing for class
// rules vs. test rules. This is for testing only; we still advocate for
// one @Test method per class.
@RunWith(JUnit4.class)
public static class LoggingTestWithRules {
@ClassRule public static LoggingRule classRule = new LoggingRule("Hardcoded class rule");
@Rule public LoggingRule testRule = new LoggingRule("Hardcoded test rule");
@Test
public void test1() {
sLogs.add("Test 1 execution");
}
@Test
public void test2() {
sLogs.add("Test 2 execution");
}
}
// A sample test class to test the runner with.
@RunWith(JUnit4.class)
public static class NoOpTest {
@BeforeClass
public static void beforeClassMethod() {}
@Before
public void beforeMethod() {}
@Test
public void testMethod() {}
@After
public void afterMethod() {}
@AfterClass
public static void afterClassMethod() {}
}
@RunWith(JUnit4.class)
public static class FailingTest extends NoOpTest {
@Test
public void testMethod() {
throw new RuntimeException("I failed.");
}
}
@Mock private RunNotifier mRunNotifier;
private LongevityClassRunner mRunner;
private static final Statement PASSING_STATEMENT =
new Statement() {
public void evaluate() throws Throwable {
// No-op.
}
};
private static final Statement FAILING_STATEMENT =
new Statement() {
public void evaluate() throws Throwable {
throw new RuntimeException("I failed.");
}
};
// A failure message for assertion calls in Mockito stubs. These assertion failures will cause
// the runner under test to fail but will not trigger a test failure directly. This message is
// used to filter failures reported by the mocked RunNotifier and re-throw the ones injected in
// the spy.
private static final String ASSERTION_FAILURE_MESSAGE = "Test assertions failed";
@Before
public void setUp() {
initMocks(this);
sLogs.clear();
}
/**
* Test that the {@link BeforeClass} methods are added to the test statement as {@link Before}
* methods.
*/
@Test
public void testBeforeClassMethodsAddedAsBeforeMethods() throws Throwable {
mRunner = spy(new LongevityClassRunner(NoOpTest.class));
// Spy the withBeforeClasses() method to check that the method does not make changes to the
// statement despite the presence of a @BeforeClass method.
doAnswer(
invocation -> {
Statement returnedStatement = (Statement) invocation.callRealMethod();
// If this assertion fails, mRunNofitier will fire a test failure.
Assert.assertEquals(
ASSERTION_FAILURE_MESSAGE,
returnedStatement,
invocation.getArgument(0));
return returnedStatement;
})
.when(mRunner)
.withBeforeClasses(any(Statement.class));
// Spy the addRunBefores() method to check that the @BeforeClass method is added to the
// @Before methods.
doAnswer(
invocation -> {
List<FrameworkMethod> methodList =
(List<FrameworkMethod>) invocation.getArgument(1);
// If any of these assertions fail, mRunNofitier will fire a test
// failure.
// There should be two methods.
Assert.assertEquals(ASSERTION_FAILURE_MESSAGE, methodList.size(), 2);
// The first one should be the @BeforeClass one.
Assert.assertEquals(
ASSERTION_FAILURE_MESSAGE,
methodList.get(0).getName(),
"beforeClassMethod");
// The second one should be the @Before one.
Assert.assertEquals(
ASSERTION_FAILURE_MESSAGE,
methodList.get(1).getName(),
"beforeMethod");
return invocation.callRealMethod();
})
.when(mRunner)
.addRunBefores(any(Statement.class), any(List.class), any(Object.class));
// Run the runner.
mRunner.run(mRunNotifier);
verifyForAssertionFailures(mRunNotifier);
// Verify that the stubbed methods are indeed called.
verify(mRunner, times(1)).withBeforeClasses(any(Statement.class));
verify(mRunner, times(1))
.addRunBefores(any(Statement.class), any(List.class), any(Object.class));
}
/**
* Test that the {@link AfterClass} methods are added to the test statement as potential {@link
* After} methods.
*/
@Test
public void testAfterClassMethodsAddedAsAfterMethods() throws Throwable {
mRunner = spy(new LongevityClassRunner(NoOpTest.class));
// Spy the withAfterClasses() method to check that the method returns an instance of
// LongevityClassRunner.RunAfterClassMethodsOnTestFailure.
doAnswer(
invocation -> {
Statement returnedStatement = (Statement) invocation.callRealMethod();
// If this assertion fails, mRunNofitier will fire a test failure.
Assert.assertTrue(
ASSERTION_FAILURE_MESSAGE,
returnedStatement
instanceof
LongevityClassRunner.RunAfterClassMethodsOnTestFailure);
return returnedStatement;
})
.when(mRunner)
.withAfterClasses(any(Statement.class));
// Spy the addRunAfters() method to check that the method returns an instance of
// LongevityClassRunner.RunAfterMethods.
doAnswer(
invocation -> {
Statement returnedStatement = (Statement) invocation.callRealMethod();
// If any of these assertions fail, mRunNotifier will fire a test
// failure.
Assert.assertTrue(
ASSERTION_FAILURE_MESSAGE,
returnedStatement
instanceof LongevityClassRunner.RunAfterMethods);
// The second argument should only contain the @After method.
List<FrameworkMethod> afterMethodList =
(List<FrameworkMethod>) invocation.getArgument(1);
Assert.assertEquals(
ASSERTION_FAILURE_MESSAGE, afterMethodList.size(), 1);
Assert.assertEquals(
ASSERTION_FAILURE_MESSAGE,
afterMethodList.get(0).getName(),
"afterMethod");
// The third argument should only contain the @AfterClass method.
List<FrameworkMethod> afterClassMethodList =
(List<FrameworkMethod>) invocation.getArgument(2);
Assert.assertEquals(
ASSERTION_FAILURE_MESSAGE, afterClassMethodList.size(), 1);
Assert.assertEquals(
ASSERTION_FAILURE_MESSAGE,
afterClassMethodList.get(0).getName(),
"afterClassMethod");
return returnedStatement;
})
.when(mRunner)
.addRunAfters(
any(Statement.class), any(List.class), any(List.class), any(Object.class));
// Run the runner.
mRunner.run(mRunNotifier);
verifyForAssertionFailures(mRunNotifier);
// Verify that the stubbed methods are indeed called.
verify(mRunner, times(1)).withAfterClasses(any(Statement.class));
verify(mRunner, times(1))
.addRunAfters(
any(Statement.class), any(List.class), any(List.class), any(Object.class));
}
/**
* Test that {@link LongevityClassRunner.RunAfterMethods} marks the test as failed for a failed
* test.
*/
@Test
public void testAfterClassMethodsHandling_marksFailure() throws Throwable {
// Initialization parameter does not matter as this test does not concern the tested class.
mRunner = spy(new LongevityClassRunner(NoOpTest.class));
try {
mRunner.hasTestFailed();
Assert.fail("Test status should not be able to be checked before it's run.");
} catch (Throwable e) {
Assert.assertTrue(e.getMessage().contains("should not be checked"));
}
// Create and partially run the runner with a failing statement.
Statement statement =
mRunner.withAfters(
null,
new NoOpTest(),
new Statement() {
public void evaluate() throws Throwable {
throw new RuntimeException("I failed.");
}
});
try {
statement.evaluate();
} catch (Throwable e) {
// Expected and no action needed.
}
Assert.assertTrue(mRunner.hasTestFailed());
}
/**
* Test that {@link LongevityClassRunner.RunAfterMethods} marks the test as passed for a passed
* test.
*/
@Test
public void testRunAfterClassMethodsHandling_marksPassed() throws Throwable {
// Initialization parameter does not matter as this test does not concern the tested class.
mRunner = spy(new LongevityClassRunner(NoOpTest.class));
// Checking test status before the statements are run should throw.
try {
mRunner.hasTestFailed();
Assert.fail("Test status should not be able to be checked before it's run.");
} catch (Throwable e) {
Assert.assertTrue(e.getMessage().contains("should not be checked"));
}
// Create and partially run the runner with a passing statement.
Statement statement =
mRunner.withAfters(
null,
new NoOpTest(),
new Statement() {
public void evaluate() throws Throwable {
// Does nothing and thus passes.
}
});
statement.evaluate();
Assert.assertFalse(mRunner.hasTestFailed());
}
/** Test that {@link AfterClass} methods are run as {@link After} methods for a passing test. */
@Test
public void testAfterClass_runAsAfterForPassingTest() throws Throwable {
// Initialization parameter does not matter as this test does not concern the tested class.
mRunner = spy(new LongevityClassRunner(NoOpTest.class));
// For a passing test, the AfterClass method should be run in the statement returned from
// withAfters().
Statement statement =
mRunner.withAfters(
null, // Passing null as parameter in the interface is not actually used.
new NoOpTest(),
PASSING_STATEMENT);
statement.evaluate();
// Check that the @AfterClass method is called.
ArgumentCaptor<List> methodsCaptor = ArgumentCaptor.forClass(List.class);
verify(mRunner, times(1)).invokeAndCollectErrors(methodsCaptor.capture(), any());
Assert.assertEquals(methodsCaptor.getValue().size(), 1);
Assert.assertTrue(
((FrameworkMethod) methodsCaptor.getValue().get(0))
.getName()
.contains("afterClassMethod"));
}
/**
* Test that {@link AfterClass} methods are run as {@link AfterClass} method for a failing test.
*/
@Test
public void testAfterClass_runAsAfterClassForFailingTest() throws Throwable {
// Initialization parameter does not matter as this test does not concern the tested class.
mRunner = spy(new LongevityClassRunner(NoOpTest.class));
// For a failing test, the AfterClass method should be run in the statement returned from
// withAfterClass().
doReturn(true).when(mRunner).hasTestFailed();
Statement statement = mRunner.withAfterClasses(FAILING_STATEMENT);
try {
statement.evaluate();
} catch (Throwable e) {
// Expected and no action needed.
}
// Check that the @AfterClass method is called.
ArgumentCaptor<List> methodsCaptor = ArgumentCaptor.forClass(List.class);
verify(mRunner, times(1)).invokeAndCollectErrors(methodsCaptor.capture(), any());
Assert.assertEquals(methodsCaptor.getValue().size(), 1);
Assert.assertTrue(
((FrameworkMethod) methodsCaptor.getValue().get(0))
.getName()
.contains("afterClassMethod"));
}
/** Test that {@link AfterClass} methods are only executed once for a passing test. */
@Test
public void testAfterClassRunOnlyOnce_passingTest() throws Throwable {
mRunner = spy(new LongevityClassRunner(NoOpTest.class));
mRunner.run(mRunNotifier);
verify(mRunner, times(1))
.invokeAndCollectErrors(getMethodNameMatcher("afterClassMethod"), any());
}
/** Test that {@link AfterClass} methods are only executed once for a failing test. */
@Test
public void testAfterClassRunOnlyOnce_failingTest() throws Throwable {
mRunner = spy(new LongevityClassRunner(FailingTest.class));
mRunner.run(mRunNotifier);
verify(mRunner, times(1))
.invokeAndCollectErrors(getMethodNameMatcher("afterClassMethod"), any());
}
/** Test that excluded classes are not executed. */
@Test
public void testIgnore_excludedClasses() throws Throwable {
RunNotifier notifier = spy(new RunNotifier());
RunListener listener = mock(RunListener.class);
notifier.addListener(listener);
Bundle ignores = new Bundle();
ignores.putString(LongevityClassRunner.FILTER_OPTION, FailingTest.class.getCanonicalName());
mRunner = spy(new LongevityClassRunner(FailingTest.class, ignores));
mRunner.run(notifier);
verify(listener, times(1)).testIgnored(any());
}
/** Test that the runner does not report iteration when iteration is not set. */
@Test
public void testReportIteration_noIterationSet() throws Throwable {
ArgumentCaptor<Description> captor = ArgumentCaptor.forClass(Description.class);
RunNotifier notifier = mock(RunNotifier.class);
mRunner = spy(new LongevityClassRunner(NoOpTest.class));
mRunner.run(notifier);
verify(notifier).fireTestStarted(captor.capture());
Assert.assertFalse(
"Description class name should not contain the iteration number.",
captor.getValue()
.getClassName()
.matches(
String.join(
LongevityClassRunner.ITERATION_SEP_DEFAULT,
"^.*",
"[0-9]+$")));
}
/** Test that the runner reports iteration when set and the default separator was used. */
@Test
public void testReportIteration_withIteration_withDefaultSeparator() throws Throwable {
ArgumentCaptor<Description> captor = ArgumentCaptor.forClass(Description.class);
RunNotifier notifier = mock(RunNotifier.class);
mRunner = spy(new LongevityClassRunner(NoOpTest.class));
mRunner.setIteration(7);
mRunner.run(notifier);
verify(notifier).fireTestStarted(captor.capture());
Assert.assertTrue(
"Description class name should contain the iteration number.",
captor.getValue()
.getClassName()
.matches(
String.join(
LongevityClassRunner.ITERATION_SEP_DEFAULT, "^.*", "7$")));
}
/** Test that the runner reports iteration when set and a custom separator was supplied. */
@Test
public void testReportIteration_withIteration_withCustomSeparator() throws Throwable {
String sep = "--";
Bundle args = new Bundle();
args.putString(LongevityClassRunner.ITERATION_SEP_OPTION, sep);
ArgumentCaptor<Description> captor = ArgumentCaptor.forClass(Description.class);
RunNotifier notifier = mock(RunNotifier.class);
mRunner = spy(new LongevityClassRunner(NoOpTest.class, args));
mRunner.setIteration(7);
mRunner.run(notifier);
verify(notifier).fireTestStarted(captor.capture());
Assert.assertTrue(
"Description class name should contain the iteration number.",
captor.getValue().getClassName().matches(String.join(sep, "^.*", "7$")));
}
@Test
public void testDynamicClassRules() throws Throwable {
Bundle args = new Bundle();
args.putString(
LongevityClassRunner.DYNAMIC_OUTER_CLASS_RULES_OPTION,
InjectedRule1.class.getName());
args.putString(
LongevityClassRunner.DYNAMIC_INNER_CLASS_RULES_OPTION,
InjectedRule2.class.getName());
RunNotifier notifier = mock(RunNotifier.class);
mRunner = new LongevityClassRunner(LoggingTestWithRules.class, args);
mRunner.run(notifier);
verifyForAssertionFailures(notifier);
assertThat(sLogs)
.containsExactly(
"Injected rule 1 starting",
"Hardcoded class rule starting",
"Injected rule 2 starting",
"Hardcoded test rule starting",
"Test 1 execution",
"Hardcoded test rule finished",
"Hardcoded test rule starting",
"Test 2 execution",
"Hardcoded test rule finished",
"Injected rule 2 finished",
"Hardcoded class rule finished",
"Injected rule 1 finished");
}
@Test
public void testDynamicTestRules() throws Throwable {
Bundle args = new Bundle();
args.putString(
LongevityClassRunner.DYNAMIC_OUTER_TEST_RULES_OPTION,
InjectedRule1.class.getName());
args.putString(
LongevityClassRunner.DYNAMIC_INNER_TEST_RULES_OPTION,
InjectedRule2.class.getName());
RunNotifier notifier = mock(RunNotifier.class);
mRunner = new LongevityClassRunner(LoggingTestWithRules.class, args);
mRunner.run(notifier);
verifyForAssertionFailures(notifier);
assertThat(sLogs)
.containsExactly(
"Hardcoded class rule starting",
"Injected rule 1 starting",
"Hardcoded test rule starting",
"Injected rule 2 starting",
"Test 1 execution",
"Injected rule 2 finished",
"Hardcoded test rule finished",
"Injected rule 1 finished",
"Injected rule 1 starting",
"Hardcoded test rule starting",
"Injected rule 2 starting",
"Test 2 execution",
"Injected rule 2 finished",
"Hardcoded test rule finished",
"Injected rule 1 finished",
"Hardcoded class rule finished");
}
private List<FrameworkMethod> getMethodNameMatcher(String methodName) {
return argThat(
l ->
l.stream()
.anyMatch(
f -> ((FrameworkMethod) f).getName().contains(methodName)));
}
/**
* 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();
}
}
}