blob: fa8e64a24e9b71daf3e9394b207fda9d1aec9180 [file] [log] [blame]
/*
* Copyright (C) 2018 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.microbenchmark;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import android.os.Bundle;
import android.os.SystemClock;
import android.platform.test.microbenchmark.Microbenchmark.TerminateEarlyException;
import android.platform.test.rule.TestWatcher;
import android.platform.test.rule.TracePointRule;
import org.junit.After;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runner.JUnitCore;
import org.junit.runner.Result;
import org.junit.runner.RunWith;
import org.junit.runner.notification.Failure;
import org.junit.runner.notification.RunNotifier;
import org.junit.runners.JUnit4;
import org.junit.runners.model.InitializationError;
import org.junit.runners.model.Statement;
import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;
import java.util.ArrayList;
import java.util.List;
/**
* Unit tests for the {@link Microbenchmark} runner.
*/
@RunWith(JUnit4.class)
public final class MicrobenchmarkTest {
// Static logs are needed to validate dynamic rules, which are instantiated reflectively and
// cannot access non-static variables.
private static List<String> sLogs = new ArrayList<>();
@Before
public void setUp() {
sLogs.clear();
}
/**
* Tests that iterations are respected for microbenchmark tests.
*/
@Test
public void testIterationCount() throws InitializationError {
Bundle args = new Bundle();
args.putString("iterations", "10");
Microbenchmark microbench = new Microbenchmark(BasicTest.class, args);
assertThat(microbench.testCount()).isEqualTo(10);
}
public static class BasicTest {
@Test
public void doNothingTest() { }
}
/**
* Tests that {@link TracePointRule} and {@link TightMethodRule}s are properly ordered.
*
* Before --> TightBefore --> Trace (begin) --> Test --> Trace(end) --> TightAfter --> After
*/
@Test
public void testFeatureExecutionOrder() throws InitializationError {
LoggingMicrobenchmark loggingRunner = new LoggingMicrobenchmark(LoggingTest.class);
loggingRunner.setOperationLog(new ArrayList<String>());
Result result = new JUnitCore().run(loggingRunner);
assertThat(result.wasSuccessful()).isTrue();
assertThat(loggingRunner.getOperationLog()).containsExactly(
"before",
"tight before",
"begin: testMethod("
+ "android.platform.test.microbenchmark.MicrobenchmarkTest$LoggingTest)",
"test",
"end",
"tight after",
"after")
.inOrder();
}
/**
* Test iterations number are added to the test name with default suffix.
*
* Before --> TightBefore --> Trace (begin) with suffix @1 --> Test --> Trace(end)
* --> TightAfter --> After --> Before --> TightBefore --> Trace (begin) with suffix @2
* --> Test --> Trace(end) --> TightAfter --> After
*/
@Test
public void testMultipleIterationsWithRename() throws InitializationError {
Bundle args = new Bundle();
args.putString("iterations", "2");
args.putString("rename-iterations", "true");
LoggingMicrobenchmark loggingRunner = new LoggingMicrobenchmark(LoggingTest.class, args);
loggingRunner.setOperationLog(new ArrayList<String>());
Result result = new JUnitCore().run(loggingRunner);
assertThat(result.wasSuccessful()).isTrue();
assertThat(loggingRunner.getOperationLog()).containsExactly(
"before",
"tight before",
"begin: testMethod("
+ "android.platform.test.microbenchmark.MicrobenchmarkTest$LoggingTest$1)",
"test",
"end",
"tight after",
"after",
"before",
"tight before",
"begin: testMethod("
+ "android.platform.test.microbenchmark.MicrobenchmarkTest$LoggingTest$2)",
"test",
"end",
"tight after",
"after")
.inOrder();
}
/**
* Test iterations number are added to the test name with custom suffix.
*
* Before --> TightBefore --> Trace (begin) with suffix --1 --> Test --> Trace(end)
* --> TightAfter --> After --> Before --> TightBefore --> Trace (begin) with suffix --2
* --> Test --> Trace(end) --> TightAfter --> After
*/
@Test
public void testMultipleIterationsWithDifferentSuffix() throws InitializationError {
Bundle args = new Bundle();
args.putString("iterations", "2");
args.putString("rename-iterations", "true");
args.putString("iteration-separator", "--");
LoggingMicrobenchmark loggingRunner = new LoggingMicrobenchmark(LoggingTest.class, args);
loggingRunner.setOperationLog(new ArrayList<String>());
Result result = new JUnitCore().run(loggingRunner);
assertThat(result.wasSuccessful()).isTrue();
assertThat(loggingRunner.getOperationLog()).containsExactly(
"before",
"tight before",
"begin: testMethod("
+ "android.platform.test.microbenchmark.MicrobenchmarkTest$LoggingTest--1)",
"test",
"end",
"tight after",
"after",
"before",
"tight before",
"begin: testMethod("
+ "android.platform.test.microbenchmark.MicrobenchmarkTest$LoggingTest--2)",
"test",
"end",
"tight after",
"after")
.inOrder();
}
/**
* Test iteration number are not added to the test name when explictly disabled.
*
* Before --> TightBefore --> Trace (begin) --> Test --> Trace(end) --> TightAfter
* --> After
*/
@Test
public void testMultipleIterationsWithoutRename() throws InitializationError {
Bundle args = new Bundle();
args.putString("iterations", "1");
args.putString("rename-iterations", "false");
LoggingMicrobenchmark loggingRunner = new LoggingMicrobenchmark(LoggingTest.class, args);
loggingRunner.setOperationLog(new ArrayList<String>());
Result result = new JUnitCore().run(loggingRunner);
assertThat(result.wasSuccessful()).isTrue();
assertThat(loggingRunner.getOperationLog())
.containsExactly(
"before",
"tight before",
"begin: testMethod("
+ "android.platform.test.microbenchmark.MicrobenchmarkTest"
+ "$LoggingTest)",
"test",
"end",
"tight after",
"after")
.inOrder();
}
/**
* Test method iteration will iterate the inner-most test method N times.
*
* <p>Before --> TightBefore --> Trace (begin) --> Test x N --> Trace(end) --> TightAfter -->
* After
*/
@Test
public void testMultipleMethodIterations() throws InitializationError {
Bundle args = new Bundle();
args.putString("iterations", "1");
args.putString("method-iterations", "10");
args.putString("rename-iterations", "false");
LoggingMicrobenchmark loggingRunner = new LoggingMicrobenchmark(LoggingTest.class, args);
loggingRunner.setOperationLog(new ArrayList<String>());
Result result = new JUnitCore().run(loggingRunner);
assertThat(result.wasSuccessful()).isTrue();
assertThat(loggingRunner.getOperationLog())
.containsExactly(
"before",
"tight before",
"begin: testMethod("
+ "android.platform.test.microbenchmark.MicrobenchmarkTest"
+ "$LoggingTest)",
"test",
"test",
"test",
"test",
"test",
"test",
"test",
"test",
"test",
"test",
"end",
"tight after",
"after")
.inOrder();
}
/** Test that the microbenchmark will terminate if the battery is too low. */
@Test
public void testStopsEarly_ifBatteryLevelIsBelowThreshold() throws InitializationError {
Bundle args = new Bundle();
args.putString(Microbenchmark.MIN_BATTERY_LEVEL_OPTION, "50");
args.putString(Microbenchmark.MAX_BATTERY_DRAIN_OPTION, "20");
Microbenchmark runner = Mockito.spy(new Microbenchmark(LoggingTest.class, args));
doReturn(49).when(runner).getBatteryLevel();
RunNotifier notifier = Mockito.mock(RunNotifier.class);
runner.run(notifier);
ArgumentCaptor<Failure> failureCaptor = ArgumentCaptor.forClass(Failure.class);
verify(notifier).fireTestFailure(failureCaptor.capture());
Failure failure = failureCaptor.getValue();
Throwable throwable = failure.getException();
assertTrue(
String.format(
"Exception was not a TerminateEarlyException. Instead, it was: %s",
throwable.getClass()),
throwable instanceof TerminateEarlyException);
assertThat(throwable)
.hasMessageThat()
.matches("Terminating early.*battery level.*threshold.");
}
/** Test that the microbenchmark will terminate if the battery is too low. */
@Test
public void testStopsEarly_ifBatteryDrainIsAboveThreshold() throws InitializationError {
Bundle args = new Bundle();
args.putString(Microbenchmark.MIN_BATTERY_LEVEL_OPTION, "40");
args.putString(Microbenchmark.MAX_BATTERY_DRAIN_OPTION, "20");
Microbenchmark runner = Mockito.spy(new Microbenchmark(LoggingTest.class, args));
doReturn(80).doReturn(50).when(runner).getBatteryLevel();
RunNotifier notifier = Mockito.mock(RunNotifier.class);
runner.run(notifier);
ArgumentCaptor<Failure> failureCaptor = ArgumentCaptor.forClass(Failure.class);
verify(notifier).fireTestFailure(failureCaptor.capture());
Failure failure = failureCaptor.getValue();
Throwable throwable = failure.getException();
assertTrue(
String.format(
"Exception was not a TerminateEarlyException. Instead, it was: %s",
throwable.getClass()),
throwable instanceof TerminateEarlyException);
assertThat(throwable)
.hasMessageThat()
.matches("Terminating early.*battery drain.*threshold.");
}
/** Test that the microbenchmark will align starting with the battery charge counter. */
@Test
public void testAlignWithBatteryChargeCounter() throws InitializationError {
Bundle args = new Bundle();
args.putString("align-with-charge-counter", "true");
args.putString("counter-decrement-timeout_ms", "5000");
Microbenchmark runner = Mockito.spy(new Microbenchmark(LoggingTest.class, args));
doReturn(99999)
.doReturn(99999)
.doReturn(99999)
.doReturn(88888)
.when(runner)
.getBatteryChargeCounter();
doReturn(10L).when(runner).getCounterPollingInterval();
RunNotifier notifier = Mockito.mock(RunNotifier.class);
Thread thread =
new Thread(
new Runnable() {
public void run() {
runner.run(notifier);
}
});
thread.start();
SystemClock.sleep(20);
verify(notifier, never()).fireTestStarted(any(Description.class));
SystemClock.sleep(20);
verify(notifier).fireTestStarted(any(Description.class));
}
/** Test that the microbenchmark counter alignment will time out if there's no change. */
@Test
public void testAlignWithBatteryChargeCounter_timesOut() throws InitializationError {
Bundle args = new Bundle();
args.putString("align-with-charge-counter", "true");
args.putString("counter-decrement-timeout_ms", "30");
Microbenchmark runner = Mockito.spy(new Microbenchmark(LoggingTest.class, args));
doReturn(99999).when(runner).getBatteryChargeCounter();
doReturn(10L).when(runner).getCounterPollingInterval();
RunNotifier notifier = Mockito.mock(RunNotifier.class);
Thread thread =
new Thread(
new Runnable() {
public void run() {
runner.run(notifier);
}
});
thread.start();
SystemClock.sleep(20);
verify(notifier, never()).fireTestStarted(any(Description.class));
SystemClock.sleep(30);
verify(notifier).fireTestStarted(any(Description.class));
}
/**
* Test successive iteration will not be executed when the terminate on test fail
* option is enabled.
*/
@Test
public void testTerminateOnTestFailOptionEnabled() throws InitializationError {
Bundle args = new Bundle();
args.putString("iterations", "2");
args.putString("rename-iterations", "false");
args.putString("terminate-on-test-fail", "true");
LoggingMicrobenchmark loggingRunner = new LoggingMicrobenchmark(
LoggingFailedTest.class, args);
loggingRunner.setOperationLog(new ArrayList<String>());
Result result = new JUnitCore().run(loggingRunner);
assertThat(result.wasSuccessful()).isFalse();
assertThat(loggingRunner.getOperationLog())
.containsExactly(
"before",
"tight before",
"begin: testMethod("
+ "android.platform.test.microbenchmark.MicrobenchmarkTest"
+ "$LoggingFailedTest)",
"end",
"after")
.inOrder();
}
/**
* Test successive iteration will be executed when the terminate on test fail
* option is disabled.
*/
@Test
public void testTerminateOnTestFailOptionDisabled() throws InitializationError {
Bundle args = new Bundle();
args.putString("iterations", "2");
args.putString("rename-iterations", "false");
args.putString("terminate-on-test-fail", "false");
LoggingMicrobenchmark loggingRunner = new LoggingMicrobenchmark(
LoggingFailedTest.class, args);
loggingRunner.setOperationLog(new ArrayList<String>());
Result result = new JUnitCore().run(loggingRunner);
assertThat(result.wasSuccessful()).isFalse();
assertThat(loggingRunner.getOperationLog())
.containsExactly(
"before",
"tight before",
"begin: testMethod("
+ "android.platform.test.microbenchmark.MicrobenchmarkTest"
+ "$LoggingFailedTest)",
"end",
"after",
"before",
"tight before",
"begin: testMethod("
+ "android.platform.test.microbenchmark.MicrobenchmarkTest"
+ "$LoggingFailedTest)",
"end",
"after")
.inOrder();
}
/** Test dynamic test rule injection. */
@Test
public void testDynamicTestRuleInjection() throws InitializationError {
Bundle args = new Bundle();
args.putString("iterations", "2");
args.putString("rename-iterations", "false");
args.putString("terminate-on-test-fail", "false");
args.putString(
Microbenchmark.DYNAMIC_INNER_TEST_RULES_OPTION, LoggingRule1.class.getName());
args.putString(
Microbenchmark.DYNAMIC_OUTER_TEST_RULES_OPTION, LoggingRule2.class.getName());
LoggingMicrobenchmark loggingRunner =
new LoggingMicrobenchmark(LoggingTestWithRules.class, args);
loggingRunner.setOperationLog(sLogs);
new JUnitCore().run(loggingRunner);
assertThat(sLogs)
.containsExactly(
"hardcoded class rule starting",
"logging rule 2 starting",
"hardcoded test rule starting",
"logging rule 1 starting",
"before",
"tight before",
"begin: testMethod("
+ "android.platform.test.microbenchmark.MicrobenchmarkTest"
+ "$LoggingTestWithRules)",
"test",
"end",
"tight after",
"after",
"logging rule 1 finished",
"hardcoded test rule finished",
"logging rule 2 finished",
"logging rule 2 starting",
"hardcoded test rule starting",
"logging rule 1 starting",
"before",
"tight before",
"begin: testMethod("
+ "android.platform.test.microbenchmark.MicrobenchmarkTest"
+ "$LoggingTestWithRules)",
"test",
"end",
"tight after",
"after",
"logging rule 1 finished",
"hardcoded test rule finished",
"logging rule 2 finished",
"hardcoded class rule finished")
.inOrder();
}
/** Test dynamic class rule injection. */
@Test
public void testDynamicClassRuleInjection() throws InitializationError {
Bundle args = new Bundle();
args.putString("iterations", "2");
args.putString("rename-iterations", "false");
args.putString("terminate-on-test-fail", "false");
args.putString(
Microbenchmark.DYNAMIC_INNER_CLASS_RULES_OPTION, LoggingRule1.class.getName());
args.putString(
Microbenchmark.DYNAMIC_OUTER_CLASS_RULES_OPTION, LoggingRule2.class.getName());
LoggingMicrobenchmark loggingRunner =
new LoggingMicrobenchmark(LoggingTestWithRules.class, args);
loggingRunner.setOperationLog(sLogs);
new JUnitCore().run(loggingRunner);
assertThat(sLogs)
.containsExactly(
"logging rule 2 starting",
"hardcoded class rule starting",
"logging rule 1 starting",
"hardcoded test rule starting",
"before",
"tight before",
"begin: testMethod("
+ "android.platform.test.microbenchmark.MicrobenchmarkTest"
+ "$LoggingTestWithRules)",
"test",
"end",
"tight after",
"after",
"hardcoded test rule finished",
"hardcoded test rule starting",
"before",
"tight before",
"begin: testMethod("
+ "android.platform.test.microbenchmark.MicrobenchmarkTest"
+ "$LoggingTestWithRules)",
"test",
"end",
"tight after",
"after",
"hardcoded test rule finished",
"logging rule 1 finished",
"hardcoded class rule finished",
"logging rule 2 finished")
.inOrder();
}
/**
* An extensions of the {@link Microbenchmark} runner that logs the start and end of collecting
* traces. It also passes the operation log to the provided test {@code Class}, if it is a
* {@link LoggingTest}. This is used for ensuring the proper order for evaluating test {@link
* Statement}s.
*/
public static class LoggingMicrobenchmark extends Microbenchmark {
private List<String> mOperationLog;
public LoggingMicrobenchmark(Class<?> klass) throws InitializationError {
super(klass);
}
LoggingMicrobenchmark(Class<?> klass, Bundle arguments) throws InitializationError {
super(klass, arguments);
}
protected Object createTest() throws Exception {
Object test = super.createTest();
if (test instanceof LoggingTest) {
((LoggingTest)test).setOperationLog(mOperationLog);
}
return test;
}
void setOperationLog(List<String> log) {
mOperationLog = log;
}
List<String> getOperationLog() {
return mOperationLog;
}
@Override
protected TracePointRule getTracePointRule() {
return new LoggingTracePointRule();
}
class LoggingTracePointRule extends TracePointRule {
@Override
protected void beginSection(String sectionTag) {
mOperationLog.add(String.format("begin: %s", sectionTag));
}
@Override
protected void endSection() {
mOperationLog.add("end");
}
}
}
/**
* A test that logs {@link Before}, {@link After}, {@link Test}, and the logging {@link
* TightMethodRule} included, used in conjunction with {@link LoggingMicrobenchmark} to
* determine all {@link Statement}s are evaluated in the proper order.
*/
public static class LoggingTest {
@Microbenchmark.TightMethodRule
public TightRule orderRule = new TightRule();
private List<String> mOperationLog;
void setOperationLog(List<String> log) {
mOperationLog = log;
}
@Before
public void beforeMethod() {
mOperationLog.add("before");
}
@Test
public void testMethod() {
mOperationLog.add("test");
}
@After
public void afterMethod() {
mOperationLog.add("after");
}
class TightRule implements TestRule {
@Override
public Statement apply(Statement base, Description description) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
mOperationLog.add("tight before");
base.evaluate();
mOperationLog.add("tight after");
}
};
}
}
}
public static class LoggingTestWithRules extends LoggingTest {
@ClassRule
public static TestRule hardCodedClassRule =
new TestWatcher() {
@Override
public void starting(Description description) {
sLogs.add("hardcoded class rule starting");
}
@Override
public void finished(Description description) {
sLogs.add("hardcoded class rule finished");
}
};
@Rule
public TestRule hardCodedRule =
new TestWatcher() {
@Override
public void starting(Description description) {
sLogs.add("hardcoded test rule starting");
}
@Override
public void finished(Description description) {
sLogs.add("hardcoded test rule finished");
}
};
}
public static class LoggingFailedTest extends LoggingTest {
@Test
public void testMethod() {
throw new RuntimeException("I failed.");
}
}
public static class LoggingRule1 extends TestWatcher {
@Override
public void starting(Description description) {
sLogs.add("logging rule 1 starting");
}
@Override
public void finished(Description description) {
sLogs.add("logging rule 1 finished");
}
}
public static class LoggingRule2 extends TestWatcher {
@Override
public void starting(Description description) {
sLogs.add("logging rule 2 starting");
}
@Override
public void finished(Description description) {
sLogs.add("logging rule 2 finished");
}
}
}