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