• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2017 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.device.collectors;
17 
18 import android.device.collectors.annotations.MetricOption;
19 import android.device.collectors.annotations.OptionClass;
20 import android.device.collectors.util.SendToInstrumentation;
21 import android.os.Bundle;
22 import android.os.Environment;
23 import android.os.ParcelFileDescriptor;
24 import androidx.annotation.VisibleForTesting;
25 import android.util.Log;
26 
27 import androidx.test.InstrumentationRegistry;
28 import androidx.test.internal.runner.listener.InstrumentationRunListener;
29 
30 import org.junit.runner.Description;
31 import org.junit.runner.Result;
32 import org.junit.runner.notification.Failure;
33 
34 import java.io.ByteArrayOutputStream;
35 import java.io.File;
36 import java.io.IOException;
37 import java.io.InputStream;
38 import java.io.PrintStream;
39 import java.util.ArrayList;
40 import java.util.Arrays;
41 import java.util.HashMap;
42 import java.util.HashSet;
43 import java.util.Map;
44 import java.util.List;
45 import java.util.Set;
46 
47 /**
48  * Base implementation of a device metric listener that will capture and output metrics for each
49  * test run or test cases. Collectors will have access to {@link DataRecord} objects where they
50  * can put results and the base class ensure these results will be send to the instrumentation.
51  *
52  * Any subclass that calls {@link #createAndEmptyDirectory(String)} needs external storage
53  * permission. So to use this class at runtime, your test need to
54  * <a href="{@docRoot}training/basics/data-storage/files.html#GetWritePermission">have storage
55  * permission enabled</a>, and preferably granted at install time (to avoid interrupting the test).
56  * For testing at desk, run adb install -r -g testpackage.apk
57  * "-g" grants all required permission at install time.
58  *
59  * Filtering:
60  * You can annotate any test method (@Test) with {@link MetricOption} and specify an arbitrary
61  * group name that the test will be part of. It is possible to trigger the collection only against
62  * test part of a group using '--include-filter-group [group name]' or to exclude a particular
63  * group using '--exclude-filter-group [group name]'.
64  * Several group name can be passed using a comma separated argument.
65  *
66  */
67 public class BaseMetricListener extends InstrumentationRunListener {
68 
69     public static final int BUFFER_SIZE = 1024;
70     // Default collect iteration interval.
71     private static final int DEFAULT_COLLECT_INTERVAL = 1;
72 
73     // Default skip metric until iteration count.
74     private static final int SKIP_UNTIL_DEFAULT_ITERATION = 0;
75 
76     /** Options keys that the collector can receive. */
77     // Filter groups, comma separated list of group name to be included or excluded
78     public static final String INCLUDE_FILTER_GROUP_KEY = "include-filter-group";
79     public static final String EXCLUDE_FILTER_GROUP_KEY = "exclude-filter-group";
80     // Argument passed to AndroidJUnitRunner to make it log-only, we shouldn't collect on log only.
81     public static final String ARGUMENT_LOG_ONLY = "log";
82     // Collect metric every nth iteration of a test with the same name.
83     public static final String COLLECT_ITERATION_INTERVAL = "collect_iteration_interval";
84 
85     // Skip metric collection until given n iteration. Uses 1 indexing here.
86     // For example if overall iteration is 10 and skip until iteration is set
87     // to 3. Metric will not be collected for 1st,2nd and 3rd iteration.
88     public static final String SKIP_METRIC_UNTIL_ITERATION = "skip_metric_until_iteration";
89 
90     private static final String NAMESPACE_SEPARATOR = ":";
91 
92     private DataRecord mRunData;
93     private DataRecord mTestData;
94 
95     private Bundle mArgsBundle = null;
96     private final List<String> mIncludeFilters;
97     private final List<String> mExcludeFilters;
98     private boolean mLogOnly = false;
99     // Store the method name and invocation count.
100     private Map<String, Integer> mTestIdInvocationCount = new HashMap<>();
101     private int mCollectIterationInterval = 1;
102     private int mSkipMetricUntilIteration = 0;
103 
104     // Whether to report the results as instrumentation results. Used by metric collector rules,
105     // which do not have the information to invoke InstrumentationRunFinished() to report metrics.
106     private boolean mReportAsInstrumentationResults = false;
107 
BaseMetricListener()108     public BaseMetricListener() {
109         mIncludeFilters = new ArrayList<>();
110         mExcludeFilters = new ArrayList<>();
111     }
112 
113     /**
114      * Constructor to simulate receiving the instrumentation arguments. Should not be used except
115      * for testing.
116      */
117     @VisibleForTesting
BaseMetricListener(Bundle argsBundle)118     protected BaseMetricListener(Bundle argsBundle) {
119         this();
120         mArgsBundle = argsBundle;
121     }
122 
123     @Override
testRunStarted(Description description)124     public final void testRunStarted(Description description) throws Exception {
125         setUp();
126         if (!mLogOnly) {
127             try {
128                 mRunData = createDataRecord();
129                 onTestRunStart(mRunData, description);
130             } catch (RuntimeException e) {
131                 // Prevent exception from reporting events.
132                 Log.e(getTag(), "Exception during onTestRunStart.", e);
133             }
134         }
135         super.testRunStarted(description);
136     }
137 
138     @Override
testRunFinished(Result result)139     public final void testRunFinished(Result result) throws Exception {
140         if (!mLogOnly) {
141             try {
142                 onTestRunEnd(mRunData, result);
143             } catch (RuntimeException e) {
144                 // Prevent exception from reporting events.
145                 Log.e(getTag(), "Exception during onTestRunEnd.", e);
146             }
147         }
148         cleanUp();
149         super.testRunFinished(result);
150     }
151 
152     @Override
testStarted(Description description)153     public final void testStarted(Description description) throws Exception {
154 
155         // Update the current invocation before proceeding with metric collection.
156         // mTestIdInvocationCount uses 1 indexing.
157         mTestIdInvocationCount.compute(description.toString(),
158                 (key, value) -> (value == null) ? 1 : value + 1);
159 
160         if (shouldRun(description)) {
161             try {
162                 mTestData = createDataRecord();
163                 onTestStart(mTestData, description);
164             } catch (RuntimeException e) {
165                 // Prevent exception from reporting events.
166                 Log.e(getTag(), "Exception during onTestStart.", e);
167             }
168         }
169         super.testStarted(description);
170     }
171 
172     @Override
testFailure(Failure failure)173     public final void testFailure(Failure failure) throws Exception {
174         Description description = failure.getDescription();
175         if (shouldRun(description)) {
176             try {
177                 onTestFail(mTestData, description, failure);
178             } catch (RuntimeException e) {
179                 // Prevent exception from reporting events.
180                 Log.e(getTag(), "Exception during onTestFail.", e);
181             }
182         }
183         super.testFailure(failure);
184     }
185 
186     @Override
testFinished(Description description)187     public final void testFinished(Description description) throws Exception {
188         if (shouldRun(description)) {
189             try {
190                 onTestEnd(mTestData, description);
191             } catch (RuntimeException e) {
192                 // Prevent exception from reporting events.
193                 Log.e(getTag(), "Exception during onTestEnd.", e);
194             }
195             if (mTestData.hasMetrics()) {
196                 // Only send the status progress if there are metrics
197                 if (mReportAsInstrumentationResults) {
198                     getInstrumentation().addResults(mTestData.createBundleFromMetrics());
199                 } else {
200                 SendToInstrumentation.sendBundle(getInstrumentation(),
201                         mTestData.createBundleFromMetrics());
202             }
203             }
204         }
205         super.testFinished(description);
206     }
207 
208     @Override
instrumentationRunFinished( PrintStream streamResult, Bundle resultBundle, Result junitResults)209     public void instrumentationRunFinished(
210             PrintStream streamResult, Bundle resultBundle, Result junitResults) {
211         // Test Run data goes into the INSTRUMENTATION_RESULT
212         if (mRunData != null) {
213             resultBundle.putAll(mRunData.createBundleFromMetrics());
214         }
215     }
216 
217     /**
218      * Set up the metric collector.
219      *
220      * <p>If another class is invoking the metric collector's callbacks directly, it should call
221      * this method to make sure that the metric collector is set up properly.
222      */
setUp()223     public final void setUp() {
224         parseArguments();
225         setupAdditionalArgs();
226         onSetUp();
227     }
228 
229     /**
230      * Clean up the metric collector.
231      *
232      * <p>If another class is invoking the metric collector's callbacks directly, it should call
233      * this method to make sure that the metric collector is cleaned up properly after collection.
234      */
cleanUp()235     public final void cleanUp() {
236         onCleanUp();
237     }
238 
239     /**
240      * Create a {@link DataRecord}. Exposed for testing.
241      */
242     @VisibleForTesting
createDataRecord()243     DataRecord createDataRecord() {
244         return new DataRecord();
245     }
246 
247     // ---------- Interfaces that can be implemented to set up and clean up metric collection.
248 
249     /** Called if custom set-up is needed for this metric collector. */
onSetUp()250     protected void onSetUp() {
251         // Does nothing by default.
252     }
253 
onCleanUp()254     protected void onCleanUp() {
255         // Does nothing by default.
256     }
257 
258     // ---------- Interfaces that can be implemented to take action on each test state.
259 
260     /**
261      * Called when {@link #testRunStarted(Description)} is called.
262      *
263      * @param runData structure where metrics can be put.
264      * @param description the {@link Description} for the run about to start.
265      */
onTestRunStart(DataRecord runData, Description description)266     public void onTestRunStart(DataRecord runData, Description description) {
267         // Does nothing
268     }
269 
270     /**
271      * Called when {@link #testRunFinished(Result result)} is called.
272      *
273      * @param runData structure where metrics can be put.
274      * @param result the {@link Result} for the run coming from the runner.
275      */
onTestRunEnd(DataRecord runData, Result result)276     public void onTestRunEnd(DataRecord runData, Result result) {
277         // Does nothing
278     }
279 
280     /**
281      * Called when {@link #testStarted(Description)} is called.
282      *
283      * @param testData structure where metrics can be put.
284      * @param description the {@link Description} for the test case about to start.
285      */
onTestStart(DataRecord testData, Description description)286     public void onTestStart(DataRecord testData, Description description) {
287         // Does nothing
288     }
289 
290     /**
291      * Called when {@link #testFailure(Failure)} is called.
292      *
293      * @param testData structure where metrics can be put.
294      * @param description the {@link Description} for the test case that just failed.
295      * @param failure the {@link Failure} describing the failure.
296      */
onTestFail(DataRecord testData, Description description, Failure failure)297     public void onTestFail(DataRecord testData, Description description, Failure failure) {
298         // Does nothing
299     }
300 
301     /**
302      * Called when {@link #testFinished(Description)} is called.
303      *
304      * @param testData structure where metrics can be put.
305      * @param description the {@link Description} of the test coming from the runner.
306      */
onTestEnd(DataRecord testData, Description description)307     public void onTestEnd(DataRecord testData, Description description) {
308         // Does nothing
309     }
310 
311     /**
312      * To add listener-specific extra args, implement this method in the sub class and add the
313      * listener specific args.
314      */
setupAdditionalArgs()315     public void setupAdditionalArgs() {
316         // NO-OP by default
317     }
318 
319     /**
320      * Turn executeShellCommand into a blocking operation.
321      *
322      * @param command shell command to be executed.
323      * @return byte array of execution result
324      */
executeCommandBlocking(String command)325     public byte[] executeCommandBlocking(String command) {
326         try (
327                 InputStream is = new ParcelFileDescriptor.AutoCloseInputStream(
328                         getInstrumentation().getUiAutomation().executeShellCommand(command));
329                 ByteArrayOutputStream out = new ByteArrayOutputStream()
330         ) {
331             byte[] buf = new byte[BUFFER_SIZE];
332             int length;
333             while ((length = is.read(buf)) >= 0) {
334                 out.write(buf, 0, length);
335             }
336             return out.toByteArray();
337         } catch (IOException e) {
338             Log.e(getTag(), "Error executing: " + command, e);
339             return null;
340         }
341     }
342 
343     /**
344      * Create a directory inside external storage, and optionally empty it.
345      *
346      * @param dir full path to the dir to be created.
347      * @param empty whether to empty the new dirctory.
348      * @return directory file created
349      */
createDirectory(String dir, boolean empty)350     public File createDirectory(String dir, boolean empty) {
351         File rootDir = Environment.getExternalStorageDirectory();
352         File destDir = new File(rootDir, dir);
353         if (empty) {
354             executeCommandBlocking("rm -rf " + destDir.getAbsolutePath());
355         }
356         if (!destDir.exists() && !destDir.mkdirs()) {
357             Log.e(getTag(), "Unable to create dir: " + destDir.getAbsolutePath());
358             return null;
359         }
360         return destDir;
361     }
362 
363     /**
364      * Create a directory inside external storage, and empty it.
365      *
366      * @param dir full path to the dir to be created.
367      * @return directory file created
368      */
createAndEmptyDirectory(String dir)369     public File createAndEmptyDirectory(String dir) {
370         return createDirectory(dir, true);
371     }
372 
373     /**
374      * Delete a directory and all the file inside.
375      *
376      * @param rootDir the {@link File} directory to delete.
377      */
recursiveDelete(File rootDir)378     public void recursiveDelete(File rootDir) {
379         if (rootDir != null) {
380             if (rootDir.isDirectory()) {
381                 File[] childFiles = rootDir.listFiles();
382                 if (childFiles != null) {
383                     for (File child : childFiles) {
384                         recursiveDelete(child);
385                     }
386                 }
387             }
388             rootDir.delete();
389         }
390     }
391 
392     /** Sets whether metrics should be reported directly to instrumentation results. */
setReportAsInstrumentationResults(boolean enabled)393     public final void setReportAsInstrumentationResults(boolean enabled) {
394         mReportAsInstrumentationResults = enabled;
395     }
396 
397     /**
398      * Returns the name of the current class to be used as a logging tag.
399      */
getTag()400     String getTag() {
401         return this.getClass().getName();
402     }
403 
404     /**
405      * Returns the bundle containing the instrumentation arguments.
406      */
getArgsBundle()407     protected final Bundle getArgsBundle() {
408         if (mArgsBundle == null) {
409             mArgsBundle = InstrumentationRegistry.getArguments();
410         }
411         return mArgsBundle;
412     }
413 
parseArguments()414     protected void parseArguments() {
415         Bundle args = getArgsBundle();
416         // First filter the arguments with the alias
417         filterAlias(args);
418         // Handle filtering
419         String includeGroup = args.getString(INCLUDE_FILTER_GROUP_KEY);
420         String excludeGroup = args.getString(EXCLUDE_FILTER_GROUP_KEY);
421         if (includeGroup != null) {
422             mIncludeFilters.addAll(Arrays.asList(includeGroup.split(",")));
423         }
424         if (excludeGroup != null) {
425             mExcludeFilters.addAll(Arrays.asList(excludeGroup.split(",")));
426         }
427         mCollectIterationInterval = Integer.parseInt(args.getString(
428                 COLLECT_ITERATION_INTERVAL, String.valueOf(DEFAULT_COLLECT_INTERVAL)));
429         mSkipMetricUntilIteration = Integer.parseInt(args.getString(
430                 SKIP_METRIC_UNTIL_ITERATION, String.valueOf(SKIP_UNTIL_DEFAULT_ITERATION)));
431 
432         if (mCollectIterationInterval < 1) {
433             Log.i(getTag(), "Metric collection iteration interval cannot be less than 1."
434                     + "Switching to collect for all the iterations.");
435             // Reset to collect for all the iterations.
436             mCollectIterationInterval = 1;
437         }
438         String logOnly = args.getString(ARGUMENT_LOG_ONLY);
439         if (logOnly != null) {
440             mLogOnly = Boolean.parseBoolean(logOnly);
441         }
442     }
443 
444     /**
445      * Filter the alias-ed options from the bundle, each implementation of BaseMetricListener will
446      * have its own list of arguments.
447      * TODO: Split the filtering logic outside the collector class in a utility/helper.
448      */
filterAlias(Bundle bundle)449     private void filterAlias(Bundle bundle) {
450         Set<String> keySet = new HashSet<>(bundle.keySet());
451         OptionClass optionClass = this.getClass().getAnnotation(OptionClass.class);
452         if (optionClass == null) {
453             // No @OptionClass was specified, remove all alias-ed options.
454             for (String key : keySet) {
455                 if (key.indexOf(NAMESPACE_SEPARATOR) != -1) {
456                     bundle.remove(key);
457                 }
458             }
459             return;
460         }
461         // Alias is a required field so if OptionClass is set, alias is set.
462         String alias = optionClass.alias();
463         for (String key : keySet) {
464             if (key.indexOf(NAMESPACE_SEPARATOR) == -1) {
465                 continue;
466             }
467             String optionAlias = key.split(NAMESPACE_SEPARATOR)[0];
468             if (alias.equals(optionAlias)) {
469                 // Place the option again, without alias.
470                 String optionName = key.split(NAMESPACE_SEPARATOR)[1];
471                 bundle.putString(optionName, bundle.getString(key));
472                 bundle.remove(key);
473             } else {
474                 // Remove other aliases.
475                 bundle.remove(key);
476             }
477         }
478     }
479 
480     /**
481      * Helper to decide whether the collector should run or not against the test case.
482      *
483      * @param desc The {@link Description} of the method.
484      * @return True if the collector should run.
485      */
shouldRun(Description desc)486     private boolean shouldRun(Description desc) {
487         if (mLogOnly) {
488             return false;
489         }
490 
491         MetricOption annotation = desc.getAnnotation(MetricOption.class);
492         List<String> groups = new ArrayList<>();
493         if (annotation != null) {
494             String group = annotation.group();
495             groups.addAll(Arrays.asList(group.split(",")));
496         }
497         if (!mExcludeFilters.isEmpty()) {
498             for (String group : groups) {
499                 // Exclude filters has priority, if any of the group is excluded, exclude the method
500                 if (mExcludeFilters.contains(group)) {
501                     return false;
502                 }
503             }
504         }
505         // If we have include filters, we can only run what's part of them.
506         if (!mIncludeFilters.isEmpty()) {
507             for (String group : groups) {
508                 if (mIncludeFilters.contains(group)) {
509                     return true;
510                 }
511             }
512             // We have include filter and did not match them.
513             return false;
514         }
515 
516         // Skip metric collection if current iteration is lesser than or equal to
517         // given skip until iteration count.
518         // mTestIdInvocationCount uses 1 indexing.
519         if (mTestIdInvocationCount.containsKey(desc.toString())
520                 && mTestIdInvocationCount.get(desc.toString()) <= mSkipMetricUntilIteration) {
521             Log.i(getTag(), String.format("Skipping metric collection. Current iteration is %d."
522                     + "Requested to skip metric until %d",
523                     mTestIdInvocationCount.get(desc.toString()),
524                     mSkipMetricUntilIteration));
525             return false;
526         }
527 
528         // Check for iteration interval metric collection criteria.
529         if (mTestIdInvocationCount.containsKey(desc.toString())
530                 && (mTestIdInvocationCount.get(desc.toString()) % mCollectIterationInterval != 0)) {
531             return false;
532         }
533         return true;
534     }
535 }
536