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