• 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 
BaseMetricListener()104     public BaseMetricListener() {
105         mIncludeFilters = new ArrayList<>();
106         mExcludeFilters = new ArrayList<>();
107     }
108 
109     /**
110      * Constructor to simulate receiving the instrumentation arguments. Should not be used except
111      * for testing.
112      */
113     @VisibleForTesting
BaseMetricListener(Bundle argsBundle)114     protected BaseMetricListener(Bundle argsBundle) {
115         this();
116         mArgsBundle = argsBundle;
117     }
118 
119     @Override
testRunStarted(Description description)120     public final void testRunStarted(Description description) throws Exception {
121         parseArguments();
122         if (!mLogOnly) {
123             try {
124                 mRunData = createDataRecord();
125                 onTestRunStart(mRunData, description);
126             } catch (RuntimeException e) {
127                 // Prevent exception from reporting events.
128                 Log.e(getTag(), "Exception during onTestRunStart.", e);
129             }
130         }
131         super.testRunStarted(description);
132     }
133 
134     @Override
testRunFinished(Result result)135     public final void testRunFinished(Result result) throws Exception {
136         if (!mLogOnly) {
137             try {
138                 onTestRunEnd(mRunData, result);
139             } catch (RuntimeException e) {
140                 // Prevent exception from reporting events.
141                 Log.e(getTag(), "Exception during onTestRunEnd.", e);
142             }
143         }
144         super.testRunFinished(result);
145     }
146 
147     @Override
testStarted(Description description)148     public final void testStarted(Description description) throws Exception {
149 
150         // Update the current invocation before proceeding with metric collection.
151         // mTestIdInvocationCount uses 1 indexing.
152         mTestIdInvocationCount.compute(description.toString(),
153                 (key, value) -> (value == null) ? 1 : value + 1);
154 
155         if (shouldRun(description)) {
156             try {
157                 mTestData = createDataRecord();
158                 onTestStart(mTestData, description);
159             } catch (RuntimeException e) {
160                 // Prevent exception from reporting events.
161                 Log.e(getTag(), "Exception during onTestStart.", e);
162             }
163         }
164         super.testStarted(description);
165     }
166 
167     @Override
testFailure(Failure failure)168     public final void testFailure(Failure failure) throws Exception {
169         Description description = failure.getDescription();
170         if (shouldRun(description)) {
171             try {
172                 onTestFail(mTestData, description, failure);
173             } catch (RuntimeException e) {
174                 // Prevent exception from reporting events.
175                 Log.e(getTag(), "Exception during onTestFail.", e);
176             }
177         }
178         super.testFailure(failure);
179     }
180 
181     @Override
testFinished(Description description)182     public final void testFinished(Description description) throws Exception {
183         if (shouldRun(description)) {
184             try {
185                 onTestEnd(mTestData, description);
186             } catch (RuntimeException e) {
187                 // Prevent exception from reporting events.
188                 Log.e(getTag(), "Exception during onTestEnd.", e);
189             }
190             if (mTestData.hasMetrics()) {
191                 // Only send the status progress if there are metrics
192                 SendToInstrumentation.sendBundle(getInstrumentation(),
193                         mTestData.createBundleFromMetrics());
194             }
195         }
196         super.testFinished(description);
197     }
198 
199     @Override
instrumentationRunFinished( PrintStream streamResult, Bundle resultBundle, Result junitResults)200     public void instrumentationRunFinished(
201             PrintStream streamResult, Bundle resultBundle, Result junitResults) {
202         // Test Run data goes into the INSTRUMENTATION_RESULT
203         if (mRunData != null) {
204             resultBundle.putAll(mRunData.createBundleFromMetrics());
205         }
206     }
207 
208     /**
209      * Create a {@link DataRecord}. Exposed for testing.
210      */
211     @VisibleForTesting
createDataRecord()212     DataRecord createDataRecord() {
213         return new DataRecord();
214     }
215 
216     // ---------- Interfaces that can be implemented to take action on each test state.
217 
218     /**
219      * Called when {@link #testRunStarted(Description)} is called.
220      *
221      * @param runData structure where metrics can be put.
222      * @param description the {@link Description} for the run about to start.
223      */
onTestRunStart(DataRecord runData, Description description)224     public void onTestRunStart(DataRecord runData, Description description) {
225         // Does nothing
226     }
227 
228     /**
229      * Called when {@link #testRunFinished(Result result)} is called.
230      *
231      * @param runData structure where metrics can be put.
232      * @param result the {@link Result} for the run coming from the runner.
233      */
onTestRunEnd(DataRecord runData, Result result)234     public void onTestRunEnd(DataRecord runData, Result result) {
235         // Does nothing
236     }
237 
238     /**
239      * Called when {@link #testStarted(Description)} is called.
240      *
241      * @param testData structure where metrics can be put.
242      * @param description the {@link Description} for the test case about to start.
243      */
onTestStart(DataRecord testData, Description description)244     public void onTestStart(DataRecord testData, Description description) {
245         // Does nothing
246     }
247 
248     /**
249      * Called when {@link #testFailure(Failure)} is called.
250      *
251      * @param testData structure where metrics can be put.
252      * @param description the {@link Description} for the test case that just failed.
253      * @param failure the {@link Failure} describing the failure.
254      */
onTestFail(DataRecord testData, Description description, Failure failure)255     public void onTestFail(DataRecord testData, Description description, Failure failure) {
256         // Does nothing
257     }
258 
259     /**
260      * Called when {@link #testFinished(Description)} is called.
261      *
262      * @param testData structure where metrics can be put.
263      * @param description the {@link Description} of the test coming from the runner.
264      */
onTestEnd(DataRecord testData, Description description)265     public void onTestEnd(DataRecord testData, Description description) {
266         // Does nothing
267     }
268 
269     /**
270      * Turn executeShellCommand into a blocking operation.
271      *
272      * @param command shell command to be executed.
273      * @return byte array of execution result
274      */
executeCommandBlocking(String command)275     public byte[] executeCommandBlocking(String command) {
276         try (
277                 InputStream is = new ParcelFileDescriptor.AutoCloseInputStream(
278                         getInstrumentation().getUiAutomation().executeShellCommand(command));
279                 ByteArrayOutputStream out = new ByteArrayOutputStream()
280         ) {
281             byte[] buf = new byte[BUFFER_SIZE];
282             int length;
283             while ((length = is.read(buf)) >= 0) {
284                 out.write(buf, 0, length);
285             }
286             return out.toByteArray();
287         } catch (IOException e) {
288             Log.e(getTag(), "Error executing: " + command, e);
289             return null;
290         }
291     }
292 
293     /**
294      * Create a directory inside external storage, and empty it.
295      *
296      * @param dir full path to the dir to be created.
297      * @return directory file created
298      */
createAndEmptyDirectory(String dir)299     public File createAndEmptyDirectory(String dir) {
300         File rootDir = Environment.getExternalStorageDirectory();
301         File destDir = new File(rootDir, dir);
302         executeCommandBlocking("rm -rf " + destDir.getAbsolutePath());
303         if (!destDir.exists() && !destDir.mkdirs()) {
304             Log.e(getTag(), "Unable to create dir: " + destDir.getAbsolutePath());
305             return null;
306         }
307         return destDir;
308     }
309 
310     /**
311      * Delete a directory and all the file inside.
312      *
313      * @param rootDir the {@link File} directory to delete.
314      */
recursiveDelete(File rootDir)315     public void recursiveDelete(File rootDir) {
316         if (rootDir != null) {
317             if (rootDir.isDirectory()) {
318                 File[] childFiles = rootDir.listFiles();
319                 if (childFiles != null) {
320                     for (File child : childFiles) {
321                         recursiveDelete(child);
322                     }
323                 }
324             }
325             rootDir.delete();
326         }
327     }
328 
329     /**
330      * Returns the name of the current class to be used as a logging tag.
331      */
getTag()332     String getTag() {
333         return this.getClass().getName();
334     }
335 
336     /**
337      * Returns the bundle containing the instrumentation arguments.
338      */
getArgsBundle()339     protected final Bundle getArgsBundle() {
340         if (mArgsBundle == null) {
341             mArgsBundle = InstrumentationRegistry.getArguments();
342         }
343         return mArgsBundle;
344     }
345 
parseArguments()346     private void parseArguments() {
347         Bundle args = getArgsBundle();
348         // First filter the arguments with the alias
349         filterAlias(args);
350         // Handle filtering
351         String includeGroup = args.getString(INCLUDE_FILTER_GROUP_KEY);
352         String excludeGroup = args.getString(EXCLUDE_FILTER_GROUP_KEY);
353         if (includeGroup != null) {
354             mIncludeFilters.addAll(Arrays.asList(includeGroup.split(",")));
355         }
356         if (excludeGroup != null) {
357             mExcludeFilters.addAll(Arrays.asList(excludeGroup.split(",")));
358         }
359         mCollectIterationInterval = Integer.parseInt(args.getString(
360                 COLLECT_ITERATION_INTERVAL, String.valueOf(DEFAULT_COLLECT_INTERVAL)));
361         mSkipMetricUntilIteration = Integer.parseInt(args.getString(
362                 SKIP_METRIC_UNTIL_ITERATION, String.valueOf(SKIP_UNTIL_DEFAULT_ITERATION)));
363 
364         if (mCollectIterationInterval < 1) {
365             Log.i(getTag(), "Metric collection iteration interval cannot be less than 1."
366                     + "Switching to collect for all the iterations.");
367             // Reset to collect for all the iterations.
368             mCollectIterationInterval = 1;
369         }
370         String logOnly = args.getString(ARGUMENT_LOG_ONLY);
371         if (logOnly != null) {
372             mLogOnly = Boolean.parseBoolean(logOnly);
373         }
374     }
375 
376     /**
377      * Filter the alias-ed options from the bundle, each implementation of BaseMetricListener will
378      * have its own list of arguments.
379      * TODO: Split the filtering logic outside the collector class in a utility/helper.
380      */
filterAlias(Bundle bundle)381     private void filterAlias(Bundle bundle) {
382         Set<String> keySet = new HashSet<>(bundle.keySet());
383         OptionClass optionClass = this.getClass().getAnnotation(OptionClass.class);
384         if (optionClass == null) {
385             // No @OptionClass was specified, remove all alias-ed options.
386             for (String key : keySet) {
387                 if (key.indexOf(NAMESPACE_SEPARATOR) != -1) {
388                     bundle.remove(key);
389                 }
390             }
391             return;
392         }
393         // Alias is a required field so if OptionClass is set, alias is set.
394         String alias = optionClass.alias();
395         for (String key : keySet) {
396             if (key.indexOf(NAMESPACE_SEPARATOR) == -1) {
397                 continue;
398             }
399             String optionAlias = key.split(NAMESPACE_SEPARATOR)[0];
400             if (alias.equals(optionAlias)) {
401                 // Place the option again, without alias.
402                 String optionName = key.split(NAMESPACE_SEPARATOR)[1];
403                 bundle.putString(optionName, bundle.getString(key));
404                 bundle.remove(key);
405             } else {
406                 // Remove other aliases.
407                 bundle.remove(key);
408             }
409         }
410     }
411 
412     /**
413      * Helper to decide whether the collector should run or not against the test case.
414      *
415      * @param desc The {@link Description} of the method.
416      * @return True if the collector should run.
417      */
shouldRun(Description desc)418     private boolean shouldRun(Description desc) {
419         if (mLogOnly) {
420             return false;
421         }
422 
423         MetricOption annotation = desc.getAnnotation(MetricOption.class);
424         List<String> groups = new ArrayList<>();
425         if (annotation != null) {
426             String group = annotation.group();
427             groups.addAll(Arrays.asList(group.split(",")));
428         }
429         if (!mExcludeFilters.isEmpty()) {
430             for (String group : groups) {
431                 // Exclude filters has priority, if any of the group is excluded, exclude the method
432                 if (mExcludeFilters.contains(group)) {
433                     return false;
434                 }
435             }
436         }
437         // If we have include filters, we can only run what's part of them.
438         if (!mIncludeFilters.isEmpty()) {
439             for (String group : groups) {
440                 if (mIncludeFilters.contains(group)) {
441                     return true;
442                 }
443             }
444             // We have include filter and did not match them.
445             return false;
446         }
447 
448         // Skip metric collection if current iteration is lesser than or equal to
449         // given skip until iteration count.
450         // mTestIdInvocationCount uses 1 indexing.
451         if (mTestIdInvocationCount.containsKey(desc.toString())
452                 && mTestIdInvocationCount.get(desc.toString()) <= mSkipMetricUntilIteration) {
453             Log.i(getTag(), String.format("Skipping metric collection. Current iteration is %d."
454                     + "Requested to skip metric until %d",
455                     mTestIdInvocationCount.get(desc.toString()),
456                     mSkipMetricUntilIteration));
457             return false;
458         }
459 
460         // Check for iteration interval metric collection criteria.
461         if (mTestIdInvocationCount.containsKey(desc.toString())
462                 && (mTestIdInvocationCount.get(desc.toString()) % mCollectIterationInterval != 0)) {
463             return false;
464         }
465         return true;
466     }
467 }
468