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