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