• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package android.platform.test.longevity;
17 
18 import static java.util.stream.Collectors.joining;
19 import static java.util.stream.Collectors.toList;
20 import static java.util.stream.Collectors.toMap;
21 
22 import android.content.res.AssetManager;
23 import android.os.Bundle;
24 import android.os.SystemClock;
25 import android.platform.test.longevity.proto.Configuration;
26 import android.platform.test.longevity.proto.Configuration.Scenario;
27 import android.platform.test.longevity.proto.Configuration.Schedule;
28 import android.util.Log;
29 
30 import androidx.annotation.VisibleForTesting;
31 import androidx.test.InstrumentationRegistry;
32 
33 import org.junit.runner.Description;
34 import org.junit.runner.Runner;
35 import org.junit.runner.notification.RunListener;
36 
37 import java.io.File;
38 import java.io.FileInputStream;
39 import java.io.IOException;
40 import java.io.InputStream;
41 import java.text.ParseException;
42 import java.text.SimpleDateFormat;
43 import java.util.ArrayList;
44 import java.util.Collections;
45 import java.util.Comparator;
46 import java.util.List;
47 import java.util.Map;
48 import java.util.TimeZone;
49 import java.util.function.Function;
50 
51 /** A profile composer for device-side testing. */
52 public class Profile extends RunListener {
53     @VisibleForTesting static final String PROFILE_OPTION_NAME = "profile";
54 
55     protected static final String PROFILE_EXTENSION = ".pb";
56 
57     private static final String LOG_TAG = Profile.class.getSimpleName();
58 
59     // Parser for parsing "at" timestamps in profiles.
60     private static final SimpleDateFormat TIMESTAMP_FORMATTER = new SimpleDateFormat("HH:mm:ss");
61 
62     // Keeps track of the current scenario being run; updated at the end of a scenario.
63     private int mScenarioIndex = 0;
64     // A list of scenarios in the order that they will be run.
65     private List<Scenario> mOrderedScenariosList;
66     // Timestamp when the test run starts, defaults to time when the ProfileBase object is
67     // constructed. Can be overridden by {@link setTestRunStartTimeMs}.
68     private long mRunStartTimeMs = SystemClock.elapsedRealtime();
69     // The profile configuration.
70     private final Configuration mConfiguration;
71     // The timestamp of the first scenario in milliseconds. All scenarios will be scheduled relative
72     // to this timestamp.
73     private long mFirstScenarioTimestampMs = 0;
74 
75     // Comparator for sorting timestamped CUJs.
76     private static class ScenarioTimestampComparator implements Comparator<Scenario> {
77         @Override
compare(Scenario s1, Scenario s2)78         public int compare(Scenario s1, Scenario s2) {
79             if (!(s1.hasAt() && s2.hasAt())) {
80                 throw new IllegalArgumentException(
81                       "Scenarios in scheduled profiles must have timestamps.");
82             }
83             return s1.getAt().compareTo(s2.getAt());
84         }
85     }
86 
87     // Comparator for sorting indexed CUJs.
88     private static class ScenarioIndexedComparator implements Comparator<Scenario> {
89         @Override
compare(Scenario s1, Scenario s2)90         public int compare(Scenario s1, Scenario s2) {
91             if (!(s1.hasIndex() && s2.hasIndex())) {
92                 throw new IllegalArgumentException(
93                         "Scenarios in indexed profiles must have indexes.");
94             }
95             return Integer.compare(s1.getIndex(), s2.getIndex());
96         }
97     }
98 
Profile(Bundle args)99     public Profile(Bundle args) {
100         super();
101         // Set the timestamp parser to UTC to get test timestamps as "time elapsed since zero".
102         TIMESTAMP_FORMATTER.setTimeZone(TimeZone.getTimeZone("UTC"));
103 
104         // Load configuration from arguments and stored the list of scenarios sorted according to
105         // their timestamps.
106         mConfiguration = getConfigurationArgument(args);
107         // When no configuration is supplied, behaves the same way as LongevitySuite but without
108         // support for shuffle, iterate etc.
109         if (mConfiguration == null) {
110             return;
111         }
112         final List<Scenario> orderedScenarios = new ArrayList<>(mConfiguration.getScenariosList());
113         if (orderedScenarios.isEmpty()) {
114             throw new IllegalArgumentException("Profile must have at least one scenario.");
115         }
116         if (mConfiguration.getSchedule().equals(Schedule.TIMESTAMPED)) {
117             if (mConfiguration.getRepetitions() != 1) {
118                 throw new IllegalArgumentException(
119                         "Repetitions param not supported for TIMESTAMPED scheduler");
120             }
121 
122             Collections.sort(orderedScenarios, new ScenarioTimestampComparator());
123             try {
124                 mFirstScenarioTimestampMs =
125                         TIMESTAMP_FORMATTER.parse(orderedScenarios.get(0).getAt()).getTime();
126             } catch (ParseException e) {
127                 throw new IllegalArgumentException(
128                         "Cannot parse the timestamp of the first scenario.", e);
129             }
130         } else if (mConfiguration.getSchedule().equals(Schedule.INDEXED)) {
131             Collections.sort(orderedScenarios, new ScenarioIndexedComparator());
132         } else if (mConfiguration.getSchedule().equals(Schedule.SEQUENTIAL)) {
133             // Do nothing. Rely on the natural ordering specified in the profile.
134         } else {
135             throw new UnsupportedOperationException(
136                     "Only scheduled profiles are currently supported.");
137         }
138 
139         mOrderedScenariosList = new ArrayList<>();
140         for (int i = 0; i < mConfiguration.getRepetitions(); i++) {
141             mOrderedScenariosList.addAll(orderedScenarios);
142         }
143     }
144 
getRunnerSequence(List<Runner> input)145     public List<Runner> getRunnerSequence(List<Runner> input) {
146         if (mConfiguration == null) {
147             return input;
148         }
149         return getTestSequenceFromConfiguration(mConfiguration, input);
150     }
151 
getTestSequenceFromConfiguration( Configuration config, List<Runner> input)152     protected List<Runner> getTestSequenceFromConfiguration(
153             Configuration config, List<Runner> input) {
154         Map<String, Runner> nameToRunner =
155                 input.stream()
156                         .collect(
157                                 toMap(
158                                         r -> r.getDescription().getDisplayName(),
159                                         Function.identity()));
160         Log.i(
161                 LOG_TAG,
162                 String.format(
163                         "Available journeys: %s",
164                         nameToRunner.keySet().stream().collect(joining(", "))));
165         List<Runner> result =
166                 mOrderedScenariosList
167                         .stream()
168                         .map(Configuration.Scenario::getJourney)
169                         .map(
170                                 journeyName -> {
171                                     if (nameToRunner.containsKey(journeyName)) {
172                                         return nameToRunner.get(journeyName);
173                                     } else {
174                                         // Write error message here to trick the auto-formatter.
175                                         String errorFmtMessage =
176                                                 "Journey %s in profile not found. "
177                                                 + "Check logcat to see available journeys.";
178                                         throw new IllegalArgumentException(
179                                                 String.format(errorFmtMessage, journeyName));
180                                     }
181                                 })
182                         .collect(toList());
183         Log.i(
184                 LOG_TAG,
185                 String.format(
186                         "Returned runners: %s",
187                         result.stream()
188                                 .map(Runner::getDescription)
189                                 .map(Description::getDisplayName)
190                                 .collect(toList())));
191         return result;
192     }
193 
194     @Override
testRunStarted(Description description)195     public void testRunStarted(Description description) {
196         mRunStartTimeMs = SystemClock.elapsedRealtime();
197     }
198 
199     @Override
testFinished(Description description)200     public void testFinished(Description description) {
201         // Increments the index to move onto the next scenario.
202         mScenarioIndex += 1;
203     }
204 
205     @Override
testIgnored(Description description)206     public void testIgnored(Description description) {
207         // Increments the index to move onto the next scenario.
208         mScenarioIndex += 1;
209     }
210 
211     /**
212      * Returns true if there is a next scheduled scenario to run. If no profile is supplied, returns
213      * false.
214      */
hasNextScheduledScenario()215     public boolean hasNextScheduledScenario() {
216         return (mOrderedScenariosList != null)
217                 && (mScenarioIndex < mOrderedScenariosList.size() - 1);
218     }
219 
220     /** Returns time in milliseconds until the next scenario. */
getTimeUntilNextScenarioMs()221     public long getTimeUntilNextScenarioMs() {
222         Scenario nextScenario = mOrderedScenariosList.get(mScenarioIndex + 1);
223         if (nextScenario.hasAt()) {
224             try {
225                 // Calibrate the start time against the first scenario's timestamp.
226                 long startTimeMs =
227                         TIMESTAMP_FORMATTER.parse(nextScenario.getAt()).getTime()
228                                 - mFirstScenarioTimestampMs;
229                 // Time in milliseconds from the start of the test run to the current point in time.
230                 long currentTimeMs = getTimeSinceRunStartedMs();
231                 // If the next test should not start yet, sleep until its start time. Otherwise,
232                 // start it immediately.
233                 if (startTimeMs > currentTimeMs) {
234                     return startTimeMs - currentTimeMs;
235                 }
236             } catch (ParseException e) {
237                 throw new IllegalArgumentException(
238                         String.format(
239                                 "Timestamp %s from scenario %s could not be parsed",
240                                 nextScenario.getAt(), nextScenario.getJourney()));
241             }
242         }
243         // For non-scheduled profiles (not a priority at this point), simply return 0.
244         return 0L;
245     }
246 
247     /** Return time in milliseconds since the test run started. */
getTimeSinceRunStartedMs()248     public long getTimeSinceRunStartedMs() {
249         return SystemClock.elapsedRealtime() - mRunStartTimeMs;
250     }
251 
252     /** Returns the Scenario object for the current scenario. */
getCurrentScenario()253     public Scenario getCurrentScenario() {
254         return mOrderedScenariosList.get(mScenarioIndex);
255     }
256 
257     /** Returns the profile configuration. */
getConfiguration()258     public Configuration getConfiguration() {
259         return mConfiguration;
260     }
261 
262     /*
263      * Parses the arguments, reads the configuration file and returns the Configuration object.
264      *
265      * If no profile option is found in the arguments, function should return null, in which case
266      * the input sequence is returned without modification. Otherwise, function should parse the
267      * profile according to the supplied argument and return the Configuration object or throw an
268      * exception if the file is not available or cannot be parsed.
269      *
270      * The configuration should be passed as either the name of a configuration bundled into the APK
271      * or a path to the configuration file.
272      *
273      * TODO(harrytczhang@): Write tests for this logic.
274      */
getConfigurationArgument(Bundle args)275     protected Configuration getConfigurationArgument(Bundle args) {
276         // profileValue is either the name of a profile bundled with an APK or a path to a
277         // profile configuration file.
278         String profileValue = args.getString(PROFILE_OPTION_NAME, "");
279         if (profileValue.isEmpty()) {
280             return null;
281         }
282         // Look inside the APK assets for the profile; if this fails, try
283         // using the profile argument as a path to a configuration file.
284         InputStream configStream;
285         try {
286             AssetManager manager = InstrumentationRegistry.getContext().getAssets();
287             String profileName = profileValue + PROFILE_EXTENSION;
288             configStream = manager.open(profileName);
289         } catch (IOException e) {
290             // Try using the profile argument it as a path to a configuration file.
291             try {
292                 File configFile = new File(profileValue);
293                 if (!configFile.exists()) {
294                     throw new IllegalArgumentException(String.format(
295                             "Profile %s does not exist.", profileValue));
296                 }
297                 configStream = new FileInputStream(configFile);
298             } catch (IOException f) {
299                 throw new IllegalArgumentException(String.format(
300                         "Profile %s cannot be opened.", profileValue));
301             }
302         }
303         try {
304             // Parse the configuration from its input stream and return it.
305             return Configuration.parseFrom(configStream);
306         } catch (IOException e) {
307             throw new IllegalArgumentException(String.format(
308                     "Cannot parse profile %s.", profileValue));
309         } finally {
310             try {
311                 configStream.close();
312             } catch (IOException e) {
313                 throw new RuntimeException(e);
314             }
315         }
316     }
317 }
318