• 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.HashSet;
42 import java.util.List;
43 import java.util.Set;
44 
45 /**
46  * Base implementation of a device metric listener that will capture and output metrics for each
47  * test run or test cases. Collectors will have access to {@link DataRecord} objects where they
48  * can put results and the base class ensure these results will be send to the instrumentation.
49  *
50  * Any subclass that calls {@link #createAndEmptyDirectory(String)} needs external storage
51  * permission. So to use this class at runtime, your test need to
52  * <a href="{@docRoot}training/basics/data-storage/files.html#GetWritePermission">have storage
53  * permission enabled</a>, and preferably granted at install time (to avoid interrupting the test).
54  * For testing at desk, run adb install -r -g testpackage.apk
55  * "-g" grants all required permission at install time.
56  *
57  * Filtering:
58  * You can annotate any test method (@Test) with {@link MetricOption} and specify an arbitrary
59  * group name that the test will be part of. It is possible to trigger the collection only against
60  * test part of a group using '--include-filter-group [group name]' or to exclude a particular
61  * group using '--exclude-filter-group [group name]'.
62  * Several group name can be passed using a comma separated argument.
63  *
64  */
65 public class BaseMetricListener extends InstrumentationRunListener {
66 
67     public static final int BUFFER_SIZE = 1024;
68 
69     /** Options keys that the collector can receive. */
70     // Filter groups, comma separated list of group name to be included or excluded
71     public static final String INCLUDE_FILTER_GROUP_KEY = "include-filter-group";
72     public static final String EXCLUDE_FILTER_GROUP_KEY = "exclude-filter-group";
73     // Argument passed to AndroidJUnitRunner to make it log-only, we shouldn't collect on log only.
74     public static final String ARGUMENT_LOG_ONLY = "log";
75 
76     private static final String NAMESPACE_SEPARATOR = ":";
77 
78     private DataRecord mRunData;
79     private DataRecord mTestData;
80 
81     private Bundle mArgsBundle = null;
82     private final List<String> mIncludeFilters;
83     private final List<String> mExcludeFilters;
84     private boolean mLogOnly = false;
85 
BaseMetricListener()86     public BaseMetricListener() {
87         mIncludeFilters = new ArrayList<>();
88         mExcludeFilters = new ArrayList<>();
89     }
90 
91     /**
92      * Constructor to simulate receiving the instrumentation arguments. Should not be used except
93      * for testing.
94      */
95     @VisibleForTesting
BaseMetricListener(Bundle argsBundle)96     protected BaseMetricListener(Bundle argsBundle) {
97         this();
98         mArgsBundle = argsBundle;
99     }
100 
101     @Override
testRunStarted(Description description)102     public final void testRunStarted(Description description) throws Exception {
103         parseArguments();
104         if (!mLogOnly) {
105             try {
106                 mRunData = createDataRecord();
107                 onTestRunStart(mRunData, description);
108             } catch (RuntimeException e) {
109                 // Prevent exception from reporting events.
110                 Log.e(getTag(), "Exception during onTestRunStart.", e);
111             }
112         }
113         super.testRunStarted(description);
114     }
115 
116     @Override
testRunFinished(Result result)117     public final void testRunFinished(Result result) throws Exception {
118         if (!mLogOnly) {
119             try {
120                 onTestRunEnd(mRunData, result);
121             } catch (RuntimeException e) {
122                 // Prevent exception from reporting events.
123                 Log.e(getTag(), "Exception during onTestRunEnd.", e);
124             }
125         }
126         super.testRunFinished(result);
127     }
128 
129     @Override
testStarted(Description description)130     public final void testStarted(Description description) throws Exception {
131         if (shouldRun(description)) {
132             try {
133                 mTestData = createDataRecord();
134                 onTestStart(mTestData, description);
135             } catch (RuntimeException e) {
136                 // Prevent exception from reporting events.
137                 Log.e(getTag(), "Exception during onTestStart.", e);
138             }
139         }
140         super.testStarted(description);
141     }
142 
143     @Override
testFailure(Failure failure)144     public final void testFailure(Failure failure) throws Exception {
145         Description description = failure.getDescription();
146         if (shouldRun(description)) {
147             try {
148                 onTestFail(mTestData, description, failure);
149             } catch (RuntimeException e) {
150                 // Prevent exception from reporting events.
151                 Log.e(getTag(), "Exception during onTestFail.", e);
152             }
153         }
154         super.testFailure(failure);
155     }
156 
157     @Override
testFinished(Description description)158     public final void testFinished(Description description) throws Exception {
159         if (shouldRun(description)) {
160             try {
161                 onTestEnd(mTestData, description);
162             } catch (RuntimeException e) {
163                 // Prevent exception from reporting events.
164                 Log.e(getTag(), "Exception during onTestEnd.", e);
165             }
166             if (mTestData.hasMetrics()) {
167                 // Only send the status progress if there are metrics
168                 SendToInstrumentation.sendBundle(getInstrumentation(),
169                         mTestData.createBundleFromMetrics());
170             }
171         }
172         super.testFinished(description);
173     }
174 
175     @Override
instrumentationRunFinished( PrintStream streamResult, Bundle resultBundle, Result junitResults)176     public void instrumentationRunFinished(
177             PrintStream streamResult, Bundle resultBundle, Result junitResults) {
178         // Test Run data goes into the INSTRUMENTATION_RESULT
179         if (mRunData != null) {
180             resultBundle.putAll(mRunData.createBundleFromMetrics());
181         }
182     }
183 
184     /**
185      * Create a {@link DataRecord}. Exposed for testing.
186      */
187     @VisibleForTesting
createDataRecord()188     DataRecord createDataRecord() {
189         return new DataRecord();
190     }
191 
192     // ---------- Interfaces that can be implemented to take action on each test state.
193 
194     /**
195      * Called when {@link #testRunStarted(Description)} is called.
196      *
197      * @param runData structure where metrics can be put.
198      * @param description the {@link Description} for the run about to start.
199      */
onTestRunStart(DataRecord runData, Description description)200     public void onTestRunStart(DataRecord runData, Description description) {
201         // Does nothing
202     }
203 
204     /**
205      * Called when {@link #testRunFinished(Result result)} is called.
206      *
207      * @param runData structure where metrics can be put.
208      * @param result the {@link Result} for the run coming from the runner.
209      */
onTestRunEnd(DataRecord runData, Result result)210     public void onTestRunEnd(DataRecord runData, Result result) {
211         // Does nothing
212     }
213 
214     /**
215      * Called when {@link #testStarted(Description)} is called.
216      *
217      * @param testData structure where metrics can be put.
218      * @param description the {@link Description} for the test case about to start.
219      */
onTestStart(DataRecord testData, Description description)220     public void onTestStart(DataRecord testData, Description description) {
221         // Does nothing
222     }
223 
224     /**
225      * Called when {@link #testFailure(Failure)} is called.
226      *
227      * @param testData structure where metrics can be put.
228      * @param description the {@link Description} for the test case that just failed.
229      * @param failure the {@link Failure} describing the failure.
230      */
onTestFail(DataRecord testData, Description description, Failure failure)231     public void onTestFail(DataRecord testData, Description description, Failure failure) {
232         // Does nothing
233     }
234 
235     /**
236      * Called when {@link #testFinished(Description)} is called.
237      *
238      * @param testData structure where metrics can be put.
239      * @param description the {@link Description} of the test coming from the runner.
240      */
onTestEnd(DataRecord testData, Description description)241     public void onTestEnd(DataRecord testData, Description description) {
242         // Does nothing
243     }
244 
245     /**
246      * Turn executeShellCommand into a blocking operation.
247      *
248      * @param command shell command to be executed.
249      * @return byte array of execution result
250      */
executeCommandBlocking(String command)251     public byte[] executeCommandBlocking(String command) {
252         try (
253                 InputStream is = new ParcelFileDescriptor.AutoCloseInputStream(
254                         getInstrumentation().getUiAutomation().executeShellCommand(command));
255                 ByteArrayOutputStream out = new ByteArrayOutputStream()
256         ) {
257             byte[] buf = new byte[BUFFER_SIZE];
258             int length;
259             while ((length = is.read(buf)) >= 0) {
260                 out.write(buf, 0, length);
261             }
262             return out.toByteArray();
263         } catch (IOException e) {
264             Log.e(getTag(), "Error executing: " + command, e);
265             return null;
266         }
267     }
268 
269     /**
270      * Create a directory inside external storage, and empty it.
271      *
272      * @param dir full path to the dir to be created.
273      * @return directory file created
274      */
createAndEmptyDirectory(String dir)275     public File createAndEmptyDirectory(String dir) {
276         File rootDir = Environment.getExternalStorageDirectory();
277         File destDir = new File(rootDir, dir);
278         executeCommandBlocking("rm -rf " + destDir.getAbsolutePath());
279         if (!destDir.exists() && !destDir.mkdirs()) {
280             Log.e(getTag(), "Unable to create dir: " + destDir.getAbsolutePath());
281             return null;
282         }
283         return destDir;
284     }
285 
286     /**
287      * Delete a directory and all the file inside.
288      *
289      * @param rootDir the {@link File} directory to delete.
290      */
recursiveDelete(File rootDir)291     public void recursiveDelete(File rootDir) {
292         if (rootDir != null) {
293             if (rootDir.isDirectory()) {
294                 File[] childFiles = rootDir.listFiles();
295                 if (childFiles != null) {
296                     for (File child : childFiles) {
297                         recursiveDelete(child);
298                     }
299                 }
300             }
301             rootDir.delete();
302         }
303     }
304 
305     /**
306      * Returns the name of the current class to be used as a logging tag.
307      */
getTag()308     String getTag() {
309         return this.getClass().getName();
310     }
311 
312     /**
313      * Returns the bundle containing the instrumentation arguments.
314      */
getArgsBundle()315     protected final Bundle getArgsBundle() {
316         if (mArgsBundle == null) {
317             mArgsBundle = InstrumentationRegistry.getArguments();
318         }
319         return mArgsBundle;
320     }
321 
parseArguments()322     private void parseArguments() {
323         Bundle args = getArgsBundle();
324         // First filter the arguments with the alias
325         filterAlias(args);
326         // Handle filtering
327         String includeGroup = args.getString(INCLUDE_FILTER_GROUP_KEY);
328         String excludeGroup = args.getString(EXCLUDE_FILTER_GROUP_KEY);
329         if (includeGroup != null) {
330             mIncludeFilters.addAll(Arrays.asList(includeGroup.split(",")));
331         }
332         if (excludeGroup != null) {
333             mExcludeFilters.addAll(Arrays.asList(excludeGroup.split(",")));
334         }
335         String logOnly = args.getString(ARGUMENT_LOG_ONLY);
336         if (logOnly != null) {
337             mLogOnly = Boolean.parseBoolean(logOnly);
338         }
339     }
340 
341     /**
342      * Filter the alias-ed options from the bundle, each implementation of BaseMetricListener will
343      * have its own list of arguments.
344      * TODO: Split the filtering logic outside the collector class in a utility/helper.
345      */
filterAlias(Bundle bundle)346     private void filterAlias(Bundle bundle) {
347         Set<String> keySet = new HashSet<>(bundle.keySet());
348         OptionClass optionClass = this.getClass().getAnnotation(OptionClass.class);
349         if (optionClass == null) {
350             // No @OptionClass was specified, remove all alias-ed options.
351             for (String key : keySet) {
352                 if (key.indexOf(NAMESPACE_SEPARATOR) != -1) {
353                     bundle.remove(key);
354                 }
355             }
356             return;
357         }
358         // Alias is a required field so if OptionClass is set, alias is set.
359         String alias = optionClass.alias();
360         for (String key : keySet) {
361             if (key.indexOf(NAMESPACE_SEPARATOR) == -1) {
362                 continue;
363             }
364             String optionAlias = key.split(NAMESPACE_SEPARATOR)[0];
365             if (alias.equals(optionAlias)) {
366                 // Place the option again, without alias.
367                 String optionName = key.split(NAMESPACE_SEPARATOR)[1];
368                 bundle.putString(optionName, bundle.getString(key));
369                 bundle.remove(key);
370             } else {
371                 // Remove other aliases.
372                 bundle.remove(key);
373             }
374         }
375     }
376 
377     /**
378      * Helper to decide whether the collector should run or not against the test case.
379      *
380      * @param desc The {@link Description} of the method.
381      * @return True if the collector should run.
382      */
shouldRun(Description desc)383     private boolean shouldRun(Description desc) {
384         if (mLogOnly) {
385             return false;
386         }
387         MetricOption annotation = desc.getAnnotation(MetricOption.class);
388         List<String> groups = new ArrayList<>();
389         if (annotation != null) {
390             String group = annotation.group();
391             groups.addAll(Arrays.asList(group.split(",")));
392         }
393         if (!mExcludeFilters.isEmpty()) {
394             for (String group : groups) {
395                 // Exclude filters has priority, if any of the group is excluded, exclude the method
396                 if (mExcludeFilters.contains(group)) {
397                     return false;
398                 }
399             }
400         }
401         // If we have include filters, we can only run what's part of them.
402         if (!mIncludeFilters.isEmpty()) {
403             for (String group : groups) {
404                 if (mIncludeFilters.contains(group)) {
405                     return true;
406                 }
407             }
408             // We have include filter and did not match them.
409             return false;
410         }
411         return true;
412     }
413 }
414