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