Make it possible to specify repeat parameter for INDEXED and SEQUENTIAL schedules

Bug: 209063673
Test: atest platform_testing/libraries/health/runners/longevity/platform/tests and by local run
Change-Id: Idaafe10ad0d823df884c4513a2f54f5b88d3ab6c
diff --git a/libraries/health/runners/longevity/platform/src/android/platform/test/longevity/Profile.java b/libraries/health/runners/longevity/platform/src/android/platform/test/longevity/Profile.java
index 6b35160..b16f231 100644
--- a/libraries/health/runners/longevity/platform/src/android/platform/test/longevity/Profile.java
+++ b/libraries/health/runners/longevity/platform/src/android/platform/test/longevity/Profile.java
@@ -108,27 +108,36 @@
         if (mConfiguration == null) {
             return;
         }
-        mOrderedScenariosList = new ArrayList<>(mConfiguration.getScenariosList());
-        if (mOrderedScenariosList.isEmpty()) {
+        final List<Scenario> orderedScenarios = new ArrayList<>(mConfiguration.getScenariosList());
+        if (orderedScenarios.isEmpty()) {
             throw new IllegalArgumentException("Profile must have at least one scenario.");
         }
         if (mConfiguration.getSchedule().equals(Schedule.TIMESTAMPED)) {
-            Collections.sort(mOrderedScenariosList, new ScenarioTimestampComparator());
+            if (mConfiguration.getRepetitions() != 1) {
+                throw new IllegalArgumentException("Repetitions param not supported for TIMESTAMPED scheduler");
+            }
+
+            Collections.sort(orderedScenarios, new ScenarioTimestampComparator());
             try {
                 mFirstScenarioTimestampMs =
-                        TIMESTAMP_FORMATTER.parse(mOrderedScenariosList.get(0).getAt()).getTime();
+                        TIMESTAMP_FORMATTER.parse(orderedScenarios.get(0).getAt()).getTime();
             } catch (ParseException e) {
                 throw new IllegalArgumentException(
                         "Cannot parse the timestamp of the first scenario.", e);
             }
         } else if (mConfiguration.getSchedule().equals(Schedule.INDEXED)) {
-            Collections.sort(mOrderedScenariosList, new ScenarioIndexedComparator());
+            Collections.sort(orderedScenarios, new ScenarioIndexedComparator());
         } else if (mConfiguration.getSchedule().equals(Schedule.SEQUENTIAL)) {
             // Do nothing. Rely on the natural ordering specified in the profile.
         } else {
             throw new UnsupportedOperationException(
                     "Only scheduled profiles are currently supported.");
         }
+
+        mOrderedScenariosList = new ArrayList<>();
+        for (int i = 0; i < mConfiguration.getRepetitions(); i++) {
+            mOrderedScenariosList.addAll(orderedScenarios);
+        }
     }
 
     public List<Runner> getRunnerSequence(List<Runner> input) {
diff --git a/libraries/health/runners/longevity/platform/src/android/platform/test/longevity/profile.proto b/libraries/health/runners/longevity/platform/src/android/platform/test/longevity/profile.proto
index 1949adb..b2e799c 100644
--- a/libraries/health/runners/longevity/platform/src/android/platform/test/longevity/profile.proto
+++ b/libraries/health/runners/longevity/platform/src/android/platform/test/longevity/profile.proto
@@ -50,4 +50,9 @@
         optional AfterTest after_test = 5 [default = EXIT];
     }
     repeated Scenario scenarios = 2;
+
+    // Amount of times the whole scenarios will be executed.
+    // For example, scenarios A->B->C with repetitions = 2 will be executed twice in a row:
+    // A->B->C->A->B->C.
+    optional int32 repetitions = 3 [default = 1];
 }
diff --git a/libraries/health/runners/longevity/platform/tests/src/android/platform/test/longevity/ProfileTest.java b/libraries/health/runners/longevity/platform/tests/src/android/platform/test/longevity/ProfileTest.java
index aacee7c..94df8b8 100644
--- a/libraries/health/runners/longevity/platform/tests/src/android/platform/test/longevity/ProfileTest.java
+++ b/libraries/health/runners/longevity/platform/tests/src/android/platform/test/longevity/ProfileTest.java
@@ -69,6 +69,7 @@
                                     .setJourney(
                                             "android.platform.test.scenario.calendar.FlingWeekPage"))
                     .build();
+
     private static final String CONFIG_WITH_INVALID_JOURNEY_KEY = "config_with_invalid_journey";
     protected static final Configuration CONFIG_WITH_INVALID_JOURNEY =
             Configuration.newBuilder()
@@ -80,11 +81,38 @@
                                             "android.platform.test.scenario.calendar.FlingWeekPage"))
                     .addScenarios(Scenario.newBuilder().setAt("00:02:00").setJourney("invalid"))
                     .build();
+
+    private static final String CONFIG_WITH_REPEATED_TIMESTAMPED_JOURNEY_KEY =
+            "config_with_repeated_timestamped_journey";
+    protected static final Configuration CONFIG_WITH_REPEATED_TIMESTAMPED_JOURNEY =
+            Configuration.newBuilder()
+                    .setRepetitions(2)
+                    .setSchedule(Schedule.TIMESTAMPED)
+                    .addScenarios(
+                            Scenario.newBuilder().setJourney(
+                                    "android.platform.test.scenario.calendar.FlingWeekPage"))
+                    .build();
+
+    private static final String CONFIG_WITH_REPEATED_JOURNEY_KEY = "config_with_repeated_journey";
+    protected static final Configuration CONFIG_WITH_REPEATED_JOURNEY =
+            Configuration.newBuilder()
+                    .setRepetitions(2)
+                    .setSchedule(Schedule.SEQUENTIAL)
+                    .addScenarios(
+                            Scenario.newBuilder().setJourney(
+                                    "android.platform.test.scenario.calendar.FlingWeekPage"))
+                    .addScenarios(
+                            Scenario.newBuilder().setJourney(
+                                    "android.platform.test.scenario.calendar.FlingDayPage"))
+                    .build();
+
     private static final String CONFIG_WITH_MISSING_TIMESTAMPS_KEY =
             "config_with_missing_timestamps";
     protected static final ImmutableMap<String, Configuration> TEST_CONFIGS= ImmutableMap.of(
             VALID_CONFIG_KEY, VALID_CONFIG,
-            CONFIG_WITH_INVALID_JOURNEY_KEY, CONFIG_WITH_INVALID_JOURNEY);
+            CONFIG_WITH_INVALID_JOURNEY_KEY, CONFIG_WITH_INVALID_JOURNEY,
+            CONFIG_WITH_REPEATED_TIMESTAMPED_JOURNEY_KEY, CONFIG_WITH_REPEATED_TIMESTAMPED_JOURNEY,
+            CONFIG_WITH_REPEATED_JOURNEY_KEY, CONFIG_WITH_REPEATED_JOURNEY);
     private static final ImmutableList<String> AVAILABLE_JOURNEYS = ImmutableList.of(
             "android.platform.test.scenario.calendar.FlingWeekPage",
             "android.platform.test.scenario.calendar.FlingDayPage",
@@ -129,6 +157,37 @@
     }
 
     /**
+     * Tests that the returned runners are ordered according to their scheduled timestamps.
+     */
+    @Test
+    public void testProfileRepeatRespected() {
+        ImmutableList<String> expectedJourneyOrder = ImmutableList.of(
+            "android.platform.test.scenario.calendar.FlingWeekPage",
+            "android.platform.test.scenario.calendar.FlingDayPage",
+            "android.platform.test.scenario.calendar.FlingWeekPage",
+            "android.platform.test.scenario.calendar.FlingDayPage");
+
+        List<Runner> output = getProfile(getArguments(CONFIG_WITH_REPEATED_JOURNEY_KEY))
+                .getRunnerSequence(mMockInput);
+        List<String> outputDescriptions = output.stream().map(r ->
+                r.getDescription().getDisplayName()).collect(Collectors.toList());
+        boolean respected = outputDescriptions.equals(expectedJourneyOrder);
+        assertThat(respected).isTrue();
+    }
+
+    /**
+     * Tests that the returned runners are ordered according to their scheduled timestamps.
+     */
+    @Test
+    public void testTimestampedProfileWithRepeatThrows() {
+        exceptionThrown.expect(IllegalArgumentException.class);
+        exceptionThrown.expectMessage("Repetitions param not supported for TIMESTAMPED scheduler");
+
+        List<Runner> output = getProfile(getArguments(CONFIG_WITH_REPEATED_TIMESTAMPED_JOURNEY_KEY))
+                .getRunnerSequence(mMockInput);
+    }
+
+    /**
      * Tests that an exception is thrown for profiles with invalid scenario names.
      */
     @Test