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