| /* |
| * 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"); |
| } |
| } |
| } |