Merge "Enable runtime test-level rule injection in microbenchmarks." into sc-dev
diff --git a/libraries/health/runners/microbenchmark/src/android/platform/test/microbenchmark/Microbenchmark.java b/libraries/health/runners/microbenchmark/src/android/platform/test/microbenchmark/Microbenchmark.java
index e82fd3e..b6c6f13 100644
--- a/libraries/health/runners/microbenchmark/src/android/platform/test/microbenchmark/Microbenchmark.java
+++ b/libraries/health/runners/microbenchmark/src/android/platform/test/microbenchmark/Microbenchmark.java
@@ -23,6 +23,7 @@
 import android.os.Bundle;
 import android.os.SystemClock;
 import android.platform.test.composer.Iterate;
+import android.platform.test.rule.DynamicRuleChain;
 import android.platform.test.rule.TracePointRule;
 import android.util.Log;
 import androidx.annotation.VisibleForTesting;
@@ -32,6 +33,7 @@
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.lang.annotation.Target;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.List;
@@ -71,6 +73,10 @@
             };
     @VisibleForTesting static final String MIN_BATTERY_LEVEL_OPTION = "min-battery";
     @VisibleForTesting static final String MAX_BATTERY_DRAIN_OPTION = "max-battery-drain";
+    // Use these options to inject rules at runtime via the command line. For details, please see
+    // documentation for DynamicRuleChain.
+    @VisibleForTesting static final String DYNAMIC_OUTER_RULES_OPTION = "outer-rules";
+    @VisibleForTesting static final String DYNAMIC_INNER_RULES_OPTION = "inner-rules";
 
     // Options for aligning with the battery charge (coulomb) counter for power tests. We want to
     // start microbenchmarks just after the coulomb counter has decremented to account for the
@@ -275,7 +281,11 @@
     /** Re-implement the private rules wrapper from {@link BlockJUnit4ClassRunner} in JUnit 4.12. */
     private Statement withRules(FrameworkMethod method, Object target, Statement statement) {
         Statement result = statement;
-        List<TestRule> testRules = getTestRules(target);
+        List<TestRule> testRules = new ArrayList<>();
+        // Inner dynamic rules should be included first because RunRules applies rules inside-out.
+        testRules.add(new DynamicRuleChain(DYNAMIC_INNER_RULES_OPTION, mArguments));
+        testRules.addAll(getTestRules(target));
+        testRules.add(new DynamicRuleChain(DYNAMIC_OUTER_RULES_OPTION, mArguments));
         // Apply legacy MethodRules, if they don't overlap with TestRules.
         for (org.junit.rules.MethodRule each : rules(target)) {
             if (!testRules.contains(each)) {
@@ -283,10 +293,7 @@
             }
         }
         // Apply modern, method-level TestRules in outer statements.
-        result =
-                testRules.isEmpty()
-                        ? statement
-                        : new RunRules(result, testRules, describeChild(method));
+        result = new RunRules(result, testRules, describeChild(method));
         return result;
     }
 
diff --git a/libraries/health/runners/microbenchmark/tests/src/android/platform/test/microbenchmark/MicrobenchmarkTest.java b/libraries/health/runners/microbenchmark/tests/src/android/platform/test/microbenchmark/MicrobenchmarkTest.java
index 8d4cd82..0b44d1a 100644
--- a/libraries/health/runners/microbenchmark/tests/src/android/platform/test/microbenchmark/MicrobenchmarkTest.java
+++ b/libraries/health/runners/microbenchmark/tests/src/android/platform/test/microbenchmark/MicrobenchmarkTest.java
@@ -25,10 +25,12 @@
 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.Rule;
 import org.junit.Test;
 import org.junit.rules.TestRule;
 import org.junit.runner.Description;
@@ -51,6 +53,15 @@
  */
 @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.
      */
@@ -408,6 +419,57 @@
     }
 
     /**
+     * Test successive iteration will be executed when the terminate on test fail option is
+     * disabled.
+     */
+    @Test
+    public void testDynamicRuleInjection() 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_RULES_OPTION, LoggingRule1.class.getName());
+        args.putString(Microbenchmark.DYNAMIC_OUTER_RULES_OPTION, LoggingRule2.class.getName());
+        LoggingMicrobenchmark loggingRunner =
+                new LoggingMicrobenchmark(LoggingTestWithRule.class, args);
+        loggingRunner.setOperationLog(sLogs);
+        new JUnitCore().run(loggingRunner);
+        assertThat(sLogs)
+                .containsExactly(
+                        "logging rule 2 starting",
+                        "hardcoded rule starting",
+                        "logging rule 1 starting",
+                        "before",
+                        "tight before",
+                        "begin: testMethod("
+                                + "android.platform.test.microbenchmark.MicrobenchmarkTest"
+                                + "$LoggingTestWithRule)",
+                        "test",
+                        "end",
+                        "tight after",
+                        "after",
+                        "logging rule 1 finished",
+                        "hardcoded rule finished",
+                        "logging rule 2 finished",
+                        "logging rule 2 starting",
+                        "hardcoded rule starting",
+                        "logging rule 1 starting",
+                        "before",
+                        "tight before",
+                        "begin: testMethod("
+                                + "android.platform.test.microbenchmark.MicrobenchmarkTest"
+                                + "$LoggingTestWithRule)",
+                        "test",
+                        "end",
+                        "tight after",
+                        "after",
+                        "logging rule 1 finished",
+                        "hardcoded 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
@@ -503,10 +565,50 @@
         }
     }
 
+    public static class LoggingTestWithRule extends LoggingTest {
+        @Rule
+        public TestRule hardCodedRule =
+                new TestWatcher() {
+                    @Override
+                    public void starting(Description description) {
+                        sLogs.add("hardcoded rule starting");
+                    }
+
+                    @Override
+                    public void finished(Description description) {
+                        sLogs.add("hardcoded 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");
+        }
+    }
 }