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