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