• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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