1 /* 2 * Copyright (C) 2019 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 17 package android.compat.cts; 18 19 import static com.android.tradefed.targetprep.UserHelper.getRunTestsAsUser; 20 21 import static com.google.common.truth.Truth.assertThat; 22 import static com.google.common.truth.Truth.assertWithMessage; 23 24 import android.cts.statsdatom.lib.ReportUtils; 25 26 import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper; 27 import com.android.ddmlib.testrunner.RemoteAndroidTestRunner; 28 import com.android.ddmlib.testrunner.TestResult.TestStatus; 29 import com.android.internal.os.StatsdConfigProto; 30 import com.android.os.AtomsProto; 31 import com.android.os.AtomsProto.Atom; 32 import com.android.os.StatsLog; 33 import com.android.os.StatsLog.ConfigMetricsReportList; 34 import com.android.tradefed.build.IBuildInfo; 35 import com.android.tradefed.device.CollectingByteOutputReceiver; 36 import com.android.tradefed.device.CollectingOutputReceiver; 37 import com.android.tradefed.device.DeviceNotAvailableException; 38 import com.android.tradefed.device.ITestDevice; 39 import com.android.tradefed.invoker.TestInformation; 40 import com.android.tradefed.log.LogUtil.CLog; 41 import com.android.tradefed.result.CollectingTestListener; 42 import com.android.tradefed.result.ITestInvocationListener; 43 import com.android.tradefed.result.TestDescription; 44 import com.android.tradefed.result.TestResult; 45 import com.android.tradefed.result.TestRunResult; 46 import com.android.tradefed.testtype.DeviceTestCase; 47 import com.android.tradefed.testtype.IBuildReceiver; 48 49 import com.google.common.io.Files; 50 import com.google.protobuf.InvalidProtocolBufferException; 51 52 import java.io.File; 53 import java.io.FileNotFoundException; 54 import java.io.IOException; 55 import java.util.Arrays; 56 import java.util.List; 57 import java.util.Map; 58 import java.util.Objects; 59 import java.util.Set; 60 import java.util.stream.Collectors; 61 62 import javax.annotation.Nonnull; 63 64 // Shamelessly plagiarised from incident's ProtoDumpTestCase and statsd's BaseTestCase family 65 public class CompatChangeGatingTestCase extends DeviceTestCase implements IBuildReceiver { 66 protected IBuildInfo mCtsBuild; 67 68 private static final String UPDATE_CONFIG_CMD = "cat %s | cmd stats config update %d"; 69 private static final String DUMP_REPORT_CMD = 70 "cmd stats dump-report %d --include_current_bucket --proto"; 71 private static final String REMOVE_CONFIG_CMD = "cmd stats config remove %d"; 72 73 private static final String TEST_RUNNER = "androidx.test.runner.AndroidJUnitRunner"; 74 75 private int mTestRunningUserId; 76 77 @Override run(TestInformation testInfo, ITestInvocationListener listener)78 public void run(TestInformation testInfo, ITestInvocationListener listener) 79 throws DeviceNotAvailableException { 80 // The test runs as the current user in most cases. For secondary_user_on_secondary_display 81 // case, we set mTestRunningUserId from RUN_TEST_AS_USER. 82 mTestRunningUserId = getDevice().getCurrentUser(); 83 if (getDevice().isVisibleBackgroundUsersSupported()) { 84 mTestRunningUserId = getRunTestsAsUser(testInfo); 85 } 86 super.run(testInfo, listener); 87 } 88 89 @Override setUp()90 protected void setUp() throws Exception { 91 super.setUp(); 92 assertThat(mCtsBuild).isNotNull(); 93 } 94 95 @Override setBuild(IBuildInfo buildInfo)96 public void setBuild(IBuildInfo buildInfo) { 97 mCtsBuild = buildInfo; 98 } 99 100 /** 101 * Install a device side test package. 102 * 103 * @param appFileName Apk file name, such as "CtsNetStatsApp.apk". 104 * @param grantPermissions whether to give runtime permissions. 105 */ installPackage(String appFileName, boolean grantPermissions)106 protected void installPackage(String appFileName, boolean grantPermissions) 107 throws FileNotFoundException, DeviceNotAvailableException { 108 CLog.d("Installing app " + appFileName); 109 CompatibilityBuildHelper buildHelper = new CompatibilityBuildHelper(mCtsBuild); 110 final String result = getDevice().installPackage(buildHelper.getTestFile(appFileName), true, 111 grantPermissions, "-t"); 112 assertWithMessage("Failed to install %s: %s", appFileName, result).that(result).isNull(); 113 } 114 115 /** 116 * Uninstall a device side test package. 117 * 118 * @param appFileName Apk file name, such as "CtsNetStatsApp.apk". 119 * @param shouldSucceed Whether to assert on failure. 120 */ uninstallPackage(String packageName, boolean shouldSucceed)121 protected void uninstallPackage(String packageName, boolean shouldSucceed) 122 throws DeviceNotAvailableException { 123 final String result = getDevice().uninstallPackage(packageName); 124 if (shouldSucceed) { 125 assertWithMessage("uninstallPackage(%s) failed: %s", packageName, result) 126 .that(result).isNull(); 127 assertFalse(getDevice().isPackageInstalled(packageName)); 128 } 129 } 130 131 /** 132 * Run a device side compat test. 133 * 134 * @param pkgName Test package name, such as 135 * "com.android.server.cts.netstats". 136 * @param testClassName Test class name; either a fully qualified name, or "." 137 * + a class name. 138 * @param testMethodName Test method name. 139 * @param enabledChanges Set of compat changes to enable. 140 * @param disabledChanges Set of compat changes to disable. 141 */ runDeviceCompatTest(@onnull String pkgName, @Nonnull String testClassName, @Nonnull String testMethodName, Set<Long> enabledChanges, Set<Long> disabledChanges)142 protected void runDeviceCompatTest(@Nonnull String pkgName, @Nonnull String testClassName, 143 @Nonnull String testMethodName, 144 Set<Long> enabledChanges, Set<Long> disabledChanges) 145 throws DeviceNotAvailableException { 146 runDeviceCompatTestReported(pkgName, testClassName, testMethodName, enabledChanges, 147 disabledChanges, enabledChanges, disabledChanges); 148 } 149 150 /** 151 * Run a device side compat test where not all changes are reported through statsd. 152 * 153 * @param pkgName Test package name, such as 154 * "com.android.server.cts.netstats". 155 * @param testClassName Test class name; either a fully qualified name, or "." 156 * + a class name. 157 * @param testMethodName Test method name. 158 * @param enabledChanges Set of compat changes to enable. 159 * @param disabledChanges Set of compat changes to disable. 160 * @param reportedEnabledChanges Expected enabled changes in statsd report. 161 * @param reportedDisabledChanges Expected disabled changes in statsd report. 162 */ runDeviceCompatTestReported(@onnull String pkgName, @Nonnull String testClassName, @Nonnull String testMethodName, Set<Long> enabledChanges, Set<Long> disabledChanges, Set<Long> reportedEnabledChanges, Set<Long> reportedDisabledChanges)163 protected void runDeviceCompatTestReported(@Nonnull String pkgName, @Nonnull String testClassName, 164 @Nonnull String testMethodName, 165 Set<Long> enabledChanges, Set<Long> disabledChanges, 166 Set<Long> reportedEnabledChanges, Set<Long> reportedDisabledChanges) 167 throws DeviceNotAvailableException { 168 169 // Set compat overrides 170 setCompatConfig(enabledChanges, disabledChanges, pkgName); 171 // Send statsd config 172 final long configId = getClass().getCanonicalName().hashCode(); 173 createAndUploadStatsdConfig(configId, pkgName); 174 175 try { 176 // Run device-side test 177 if (testClassName.startsWith(".")) { 178 testClassName = pkgName + testClassName; 179 } 180 RemoteAndroidTestRunner testRunner = new RemoteAndroidTestRunner(pkgName, TEST_RUNNER, 181 getDevice().getIDevice()); 182 testRunner.setMethodName(testClassName, testMethodName); 183 CollectingTestListener listener = new CollectingTestListener(); 184 assertThat(getDevice().runInstrumentationTestsAsUser( 185 testRunner, mTestRunningUserId, listener)).isTrue(); 186 187 // Check that device side test occurred as expected 188 final TestRunResult result = listener.getCurrentRunResults(); 189 assertWithMessage("Failed to successfully run device tests for %s: %s", 190 result.getName(), result.getRunFailureMessage()) 191 .that(result.isRunFailure()).isFalse(); 192 assertWithMessage("Should run only exactly one test method!") 193 .that(result.getNumTests()).isEqualTo(1); 194 if (result.hasFailedTests()) { 195 // build a meaningful error message 196 StringBuilder errorBuilder = new StringBuilder("On-device test failed:\n"); 197 for (Map.Entry<TestDescription, TestResult> resultEntry : 198 result.getTestResults().entrySet()) { 199 if (!resultEntry.getValue().getStatus().equals(TestStatus.PASSED)) { 200 errorBuilder.append(resultEntry.getKey().toString()); 201 errorBuilder.append(":\n"); 202 errorBuilder.append(resultEntry.getValue().getStackTrace()); 203 } 204 } 205 throw new AssertionError(errorBuilder.toString()); 206 } 207 208 } finally { 209 // Cleanup compat overrides 210 resetCompatConfig(pkgName, enabledChanges, disabledChanges); 211 // Validate statsd report 212 validatePostRunStatsdReport(configId, pkgName, reportedEnabledChanges, 213 reportedDisabledChanges); 214 } 215 216 } 217 218 /** 219 * Gets the statsd report. Note that this also deletes that report from statsd. 220 */ getReportList(long configId)221 private ConfigMetricsReportList getReportList(long configId) 222 throws DeviceNotAvailableException { 223 try { 224 final CollectingByteOutputReceiver receiver = new CollectingByteOutputReceiver(); 225 getDevice().executeShellCommand(String.format(DUMP_REPORT_CMD, configId), receiver); 226 return ConfigMetricsReportList.parser() 227 .parseFrom(receiver.getOutput()); 228 } catch (InvalidProtocolBufferException e) { 229 throw new IllegalStateException("Failed to fetch and parse the statsd output report.", 230 e); 231 } 232 } 233 getEventMetricDataList( ConfigMetricsReportList reportList)234 private static List<StatsLog.EventMetricData> getEventMetricDataList( 235 ConfigMetricsReportList reportList) { 236 try { 237 return ReportUtils.getEventMetricDataList(reportList); 238 } catch (Exception e) { 239 throw new IllegalStateException("Failed to parse ConfigMetrisReportList", e); 240 } 241 } 242 243 /** 244 * Creates and uploads a statsd config that matches the AppCompatibilityChangeReported atom 245 * logged by a given package name. 246 * 247 * @param configId A unique config id. 248 * @param pkgName The package name of the app that is expected to report the atom. It will be 249 * the only allowed log source. 250 */ createAndUploadStatsdConfig(long configId, String pkgName)251 protected void createAndUploadStatsdConfig(long configId, String pkgName) 252 throws DeviceNotAvailableException { 253 final String atomName = "Atom" + System.nanoTime(); 254 final String eventName = "Event" + System.nanoTime(); 255 final ITestDevice device = getDevice(); 256 257 StatsdConfigProto.StatsdConfig.Builder configBuilder = 258 StatsdConfigProto.StatsdConfig.newBuilder() 259 .setId(configId) 260 .addAllowedLogSource(pkgName) 261 .addWhitelistedAtomIds(Atom.APP_COMPATIBILITY_CHANGE_REPORTED_FIELD_NUMBER); 262 StatsdConfigProto.SimpleAtomMatcher.Builder simpleAtomMatcherBuilder = 263 StatsdConfigProto.SimpleAtomMatcher 264 .newBuilder().setAtomId( 265 Atom.APP_COMPATIBILITY_CHANGE_REPORTED_FIELD_NUMBER); 266 configBuilder.addAtomMatcher( 267 StatsdConfigProto.AtomMatcher.newBuilder() 268 .setId(atomName.hashCode()) 269 .setSimpleAtomMatcher(simpleAtomMatcherBuilder)); 270 configBuilder.addEventMetric( 271 StatsdConfigProto.EventMetric.newBuilder() 272 .setId(eventName.hashCode()) 273 .setWhat(atomName.hashCode())); 274 StatsdConfigProto.StatsdConfig config = configBuilder.build(); 275 try { 276 File configFile = File.createTempFile("statsdconfig", ".config"); 277 configFile.deleteOnExit(); 278 Files.write(config.toByteArray(), configFile); 279 String remotePath = "/data/local/tmp/" + configFile.getName(); 280 device.pushFile(configFile, remotePath); 281 device.executeShellCommand(String.format(UPDATE_CONFIG_CMD, remotePath, configId)); 282 device.executeShellCommand("rm " + remotePath); 283 } catch (IOException e) { 284 throw new RuntimeException("IO error when writing to temp file.", e); 285 } 286 // Purge data 287 getReportList(configId); 288 } 289 290 /** 291 * Gets the uid of the test app. 292 */ getUid(@onnull String packageName)293 protected int getUid(@Nonnull String packageName) throws DeviceNotAvailableException { 294 String uidLines = getDevice() 295 .executeShellCommand( 296 "cmd package list packages -U --user " + mTestRunningUserId + " " 297 + packageName); 298 for (String uidLine : uidLines.split("\n")) { 299 if (uidLine.startsWith("package:" + packageName + " uid:")) { 300 String[] uidLineParts = uidLine.split(":"); 301 // 3rd entry is package uid 302 assertThat(uidLineParts.length).isGreaterThan(2); 303 int uid = Integer.parseInt(uidLineParts[2].trim()); 304 assertThat(uid).isGreaterThan(10000); 305 return uid; 306 } 307 } 308 throw new IllegalStateException("Failed to find the test app on the device"); 309 } 310 311 /** 312 * Set the compat config using adb. 313 * 314 * @param enabledChanges Changes to be enabled. 315 * @param disabledChanges Changes to be disabled. 316 * @param packageName Package name for the app whose config is being changed. 317 */ setCompatConfig(Set<Long> enabledChanges, Set<Long> disabledChanges, @Nonnull String packageName)318 protected void setCompatConfig(Set<Long> enabledChanges, Set<Long> disabledChanges, 319 @Nonnull String packageName) throws DeviceNotAvailableException { 320 for (Long enabledChange : enabledChanges) { 321 runCommand("am compat enable " + enabledChange + " " + packageName); 322 } 323 for (Long disabledChange : disabledChanges) { 324 runCommand("am compat disable " + disabledChange + " " + packageName); 325 } 326 } 327 328 /** 329 * Reset changes to default for a package. 330 */ resetCompatChanges(Set<Long> changes, @Nonnull String packageName)331 protected void resetCompatChanges(Set<Long> changes, @Nonnull String packageName) 332 throws DeviceNotAvailableException { 333 for (Long change : changes) { 334 runCommand("am compat reset " + change + " " + packageName); 335 } 336 } 337 338 /** 339 * Remove statsd config for a given id. 340 */ removeStatsdConfig(long configId)341 private void removeStatsdConfig(long configId) throws DeviceNotAvailableException { 342 getDevice().executeShellCommand( 343 String.join(" ", REMOVE_CONFIG_CMD, String.valueOf(configId))); 344 } 345 346 /** 347 * Get the compat changes that were logged. 348 */ getReportedChanges(long configId, String pkgName)349 private Map<Long, Boolean> getReportedChanges(long configId, String pkgName) 350 throws DeviceNotAvailableException { 351 final int packageUid = getUid(pkgName); 352 return getEventMetricDataList(getReportList(configId)).stream() 353 .filter(eventMetricData -> eventMetricData.hasAtom()) 354 .map(eventMetricData -> eventMetricData.getAtom()) 355 .map(atom -> atom.getAppCompatibilityChangeReported()) 356 .filter(atom -> atom != null && atom.getUid() == packageUid) // Should be redundant 357 .collect(Collectors.toMap( 358 atom -> atom.getChangeId(), // Key 359 atom -> atom.getState() == // Value 360 AtomsProto.AppCompatibilityChangeReported.State.ENABLED, 361 (a, b) -> { 362 if (!Objects.equals(a, b)) { 363 throw new IllegalStateException( 364 "inconsistent compatibility states"); 365 } 366 return a; 367 })); 368 } 369 370 /** 371 * Cleanup the altered change ids under test. 372 * 373 * @param pkgName Package name of the app under test. 374 * @param enabledChanges Set of changes that were enabled during the test and need to be 375 * reset to the default value. 376 * @param disabledChanges Set of changes that were disabled during the test and need to 377 * be reset to the default value. 378 */ 379 protected void resetCompatConfig( String pkgName, Set<Long> enabledChanges, 380 Set<Long> disabledChanges) throws DeviceNotAvailableException { 381 // Clear overrides. 382 resetCompatChanges(enabledChanges, pkgName); 383 resetCompatChanges(disabledChanges, pkgName); 384 } 385 386 /** 387 * Validate that all overridden changes were logged while running the test. 388 * 389 * @param configId The unique config id used to track change id queries. 390 * @param pkgName Package name of the app under test. 391 * @param loggedEnabledChanges Changes expected to be logged as enabled during the test. 392 * @param loggedDisabledChanges Changes expected to be logged as disabled during the test. 393 */ 394 protected void validatePostRunStatsdReport(long configId, String pkgName, 395 Set<Long> loggedEnabledChanges, Set<Long> loggedDisabledChanges) 396 throws DeviceNotAvailableException { 397 // Clear statsd report data and remove config 398 Map<Long, Boolean> reportedChanges = getReportedChanges(configId, pkgName); 399 removeStatsdConfig(configId); 400 401 for (Long enabledChange : loggedEnabledChanges) { 402 assertThat(reportedChanges) 403 .containsEntry(enabledChange, true); 404 } 405 for (Long disabledChange : loggedDisabledChanges) { 406 assertThat(reportedChanges) 407 .containsEntry(disabledChange, false); 408 } 409 } 410 411 /** 412 * Execute the given command, and returns the output. 413 */ 414 protected String runCommand(String command) throws DeviceNotAvailableException { 415 final CollectingOutputReceiver receiver = new CollectingOutputReceiver(); 416 getDevice().executeShellCommand(command, receiver); 417 return receiver.getOutput(); 418 } 419 420 /** 421 * Get the on device compat config. 422 */ 423 protected List<Change> getOnDeviceCompatConfig() throws Exception { 424 String config = runCommand("dumpsys platform_compat"); 425 return Arrays.stream(config.split("\n")) 426 .map(Change::fromString) 427 .collect(Collectors.toList()); 428 } 429 430 protected Change getOnDeviceChangeIdConfig(long changeId) throws Exception { 431 List<Change> changes = getOnDeviceCompatConfig(); 432 for (Change change : changes) { 433 if (change.changeId == changeId) { 434 return change; 435 } 436 } 437 return null; 438 } 439 } 440