• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 com.android.timezone.xts;
17 
18 import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper;
19 import com.android.tradefed.build.IBuildInfo;
20 import com.android.tradefed.config.Option;
21 import com.android.tradefed.log.LogUtil;
22 import com.android.tradefed.testtype.DeviceTestCase;
23 import com.android.tradefed.testtype.IBuildReceiver;
24 import com.android.tradefed.util.FileUtil;
25 
26 import java.io.File;
27 import java.util.function.BooleanSupplier;
28 
29 /**
30  * Class for host-side tests that the time zone rules update feature works as intended. This is
31  * intended to give confidence to OEMs that they have implemented / configured the OEM parts of the
32  * feature correctly.
33  *
34  * <p>There are two main operations involved in time zone updates:
35  * <ol>
36  *     <li>Package installs/uninstalls - asynchronously stage operations for install</li>
37  *     <li>Reboots - perform the staged operations / delete bad installed data</li>
38  * </ol>
39  * Both these operations are time consuming and there's a degree of non-determinism involved.
40  *
41  * <p>A "clean" device can also be in one of two main states depending on whether it has been wiped
42  * and/or rebooted before this test runs:
43  * <ul>
44  *     <li>A device may have nothing staged / installed in /data/misc/zoneinfo at all.</li>
45  *     <li>A device may have the time zone data from the default system image version of the time
46  *     zone data app staged or installed.</li>
47  * </ul>
48  * This test attempts to handle both of these cases.
49  *
50  */
51 // TODO(nfuller): Switch this to JUnit4 when HostTest supports @Option with JUnit4.
52 // http://b/64015928
53 public class TimeZoneUpdateHostTest extends DeviceTestCase implements IBuildReceiver {
54 
55     // These must match equivalent values in RulesManagerService dumpsys code.
56     private static final String STAGED_OPERATION_NONE = "None";
57     private static final String STAGED_OPERATION_INSTALL = "Install";
58     private static final String STAGED_OPERATION_UNINSTALL = "Uninstall";
59     private static final String INSTALL_STATE_INSTALLED = "Installed";
60 
61     private IBuildInfo mBuildInfo;
62     private File mTempDir;
63 
64     @Option(name = "oem-data-app-package-name",
65             description="The OEM-specific package name for the data app",
66             mandatory = true)
67     private String mOemDataAppPackageName;
68 
getTimeZoneDataPackageName()69     private String getTimeZoneDataPackageName() {
70         assertNotNull(mOemDataAppPackageName);
71         return mOemDataAppPackageName;
72     }
73 
74     @Option(name = "oem-data-app-apk-prefix",
75             description="The OEM-specific APK name for the data app test files, e.g."
76                     + "for TimeZoneDataOemCorp_test1.apk the prefix would be"
77                     + "\"TimeZoneDataOemCorp\"",
78             mandatory = true)
79     private String mOemDataAppApkPrefix;
80 
getTimeZoneDataApkName(String testId)81     private String getTimeZoneDataApkName(String testId) {
82         assertNotNull(mOemDataAppApkPrefix);
83         return mOemDataAppApkPrefix + "_" + testId + ".apk";
84     }
85 
86     @Override
setBuild(IBuildInfo buildInfo)87     public void setBuild(IBuildInfo buildInfo) {
88         mBuildInfo = buildInfo;
89     }
90 
91     @Override
setUp()92     public void setUp() throws Exception {
93         super.setUp();
94         createTempDir();
95         resetDeviceToClean();
96     }
97 
98     @Override
tearDown()99     protected void tearDown() throws Exception {
100         resetDeviceToClean();
101         deleteTempDir();
102         super.tearDown();
103     }
104 
105     // @Before
createTempDir()106     public void createTempDir() throws Exception {
107         mTempDir = File.createTempFile("timeZoneUpdateTest", null);
108         assertTrue(mTempDir.delete());
109         assertTrue(mTempDir.mkdir());
110     }
111 
112     // @After
deleteTempDir()113     public void deleteTempDir() throws Exception {
114         FileUtil.recursiveDelete(mTempDir);
115     }
116 
117     /**
118      * Reset the device to having no installed time zone data outside of the /system/priv-app
119      * version that came with the system image.
120      */
121     // @Before
122     // @After
resetDeviceToClean()123     public void resetDeviceToClean() throws Exception {
124         // If this fails the data app isn't present on device. No point in starting.
125         assertTrue(getTimeZoneDataPackageName() + " not installed",
126                 isPackageInstalled(getTimeZoneDataPackageName()));
127 
128         // Reboot as needed to apply any staged operation.
129         if (!STAGED_OPERATION_NONE.equals(getStagedOperationType())) {
130             rebootDeviceAndWaitForRestart();
131         }
132 
133         // A "clean" device means no time zone data .apk installed in /data at all, try to get to
134         // that state.
135         for (int i = 0; i < 2; i++) {
136             logDeviceTimeZoneState();
137 
138             String errorCode = uninstallPackage(getTimeZoneDataPackageName());
139             if (errorCode != null) {
140                 // Failed to uninstall, which we take to mean the device is "clean".
141                 break;
142             }
143             // Success, meaning there was something that could be uninstalled, so we should wait
144             // for the device to react to the uninstall and reboot. If the time zone update system
145             // is not configured correctly this is likely to be where tests fail.
146 
147             // If the package we uninstalled was not valid then there would be nothing installed and
148             // so nothing will be staged by the uninstall. Check and do what it takes to get the
149             // device to having nothing installed again.
150             if (INSTALL_STATE_INSTALLED.equals(getCurrentInstallState())) {
151                 // We expect the device to get to the staged state "UNINSTALL", meaning it will try
152                 // to revert to no distro installed on next boot.
153                 waitForStagedUninstall();
154 
155                 rebootDeviceAndWaitForRestart();
156             }
157         }
158         assertActiveRulesVersion(getSystemRulesVersion());
159         assertEquals(STAGED_OPERATION_NONE, getStagedOperationType());
160     }
161 
162     // @Test
testInstallNewerRulesVersion()163     public void testInstallNewerRulesVersion() throws Exception {
164         // This information must match the rules version in test1: IANA version=2030a, revision=1
165         String test1VersionInfo = "2030a,1";
166 
167         // Confirm the staged / install state before we start.
168         assertFalse(test1VersionInfo.equals(getCurrentInstalledVersion()));
169         assertEquals(STAGED_OPERATION_NONE, getStagedOperationType());
170 
171         File appFile = getTimeZoneDataApkFile("test1");
172         getDevice().installPackage(appFile, true /* reinstall */);
173 
174         waitForStagedInstall(test1VersionInfo);
175 
176         // Confirm the install state hasn't changed.
177         assertFalse(test1VersionInfo.equals(getCurrentInstalledVersion()));
178 
179         // Now reboot, and the staged version should become the installed version.
180         rebootDeviceAndWaitForRestart();
181 
182         // After reboot, check the state.
183         assertEquals(STAGED_OPERATION_NONE, getStagedOperationType());
184         assertEquals(INSTALL_STATE_INSTALLED, getCurrentInstallState());
185         assertEquals(test1VersionInfo, getCurrentInstalledVersion());
186     }
187 
188     // @Test
testInstallOlderRulesVersion()189     public void testInstallOlderRulesVersion() throws Exception {
190         File appFile = getTimeZoneDataApkFile("test2");
191         getDevice().installPackage(appFile, true /* reinstall */);
192 
193         // The attempt to install a version of the data that is older than the version in the system
194         // image should be rejected and nothing should be staged. There's currently no way (short of
195         // looking at logs) to tell this has happened, but combined with other tests and given a
196         // suitable delay it gives us some confidence that the attempt has been made and it was
197         // rejected.
198 
199         Thread.sleep(30000);
200 
201         assertEquals(STAGED_OPERATION_NONE, getStagedOperationType());
202     }
203 
rebootDeviceAndWaitForRestart()204     private void rebootDeviceAndWaitForRestart() throws Exception {
205         log("Rebooting device");
206         getDevice().reboot();
207     }
208 
logDeviceTimeZoneState()209     private void logDeviceTimeZoneState() throws Exception {
210         log("Initial device state: " + dumpEntireTimeZoneStatusToString());
211     }
212 
log(String msg)213     private static void log(String msg) {
214         LogUtil.CLog.i(msg);
215     }
216 
assertActiveRulesVersion(String expectedRulesVersion)217     private void assertActiveRulesVersion(String expectedRulesVersion) throws Exception {
218         // Dumpsys reports the version reported by ICU and libcore, but they should always match.
219         String expectedActiveRulesVersion = expectedRulesVersion + "," + expectedRulesVersion;
220 
221         String actualActiveRulesVersion =
222                 waitForNoOperationInProgressAndReturn(StateType.ACTIVE_RULES_VERSION);
223         assertEquals(expectedActiveRulesVersion, actualActiveRulesVersion);
224     }
225 
getCurrentInstalledVersion()226     private String getCurrentInstalledVersion() throws Exception {
227         return waitForNoOperationInProgressAndReturn(StateType.CURRENTLY_INSTALLED_VERSION);
228     }
229 
getCurrentInstallState()230     private String getCurrentInstallState() throws Exception {
231         return waitForNoOperationInProgressAndReturn(StateType.CURRENT_INSTALL_STATE);
232     }
233 
getStagedInstallVersion()234     private String getStagedInstallVersion() throws Exception {
235         return waitForNoOperationInProgressAndReturn(StateType.STAGED_INSTALL_VERSION);
236     }
237 
getStagedOperationType()238     private String getStagedOperationType() throws Exception {
239         return waitForNoOperationInProgressAndReturn(StateType.STAGED_OPERATION_TYPE);
240     }
241 
getSystemRulesVersion()242     private String getSystemRulesVersion() throws Exception {
243         return waitForNoOperationInProgressAndReturn(StateType.SYSTEM_RULES_VERSION);
244     }
245 
isOperationInProgress()246     private boolean isOperationInProgress() {
247         try {
248             String operationInProgressString =
249                     getDeviceTimeZoneState(StateType.OPERATION_IN_PROGRESS);
250             return Boolean.parseBoolean(operationInProgressString);
251         } catch (Exception e) {
252             throw new AssertionError("Failed to read staged status", e);
253         }
254     }
255 
waitForNoOperationInProgressAndReturn(StateType stateType)256     private String waitForNoOperationInProgressAndReturn(StateType stateType) throws Exception {
257         waitForCondition(() -> !isOperationInProgress());
258         return getDeviceTimeZoneState(stateType);
259     }
260 
waitForStagedUninstall()261     private void waitForStagedUninstall() throws Exception {
262         waitForCondition(() -> isStagedUninstall());
263     }
264 
waitForStagedInstall(String versionString)265     private void waitForStagedInstall(String versionString) throws Exception {
266         waitForCondition(() -> isStagedInstall(versionString));
267     }
268 
isStagedUninstall()269     private boolean isStagedUninstall() {
270         try {
271             return getStagedOperationType().equals(STAGED_OPERATION_UNINSTALL);
272         } catch (Exception e) {
273             throw new AssertionError("Failed to read staged status", e);
274         }
275     }
276 
isStagedInstall(String versionString)277     private boolean isStagedInstall(String versionString) {
278         try {
279             return getStagedOperationType().equals(STAGED_OPERATION_INSTALL)
280                     && getStagedInstallVersion().equals(versionString);
281         } catch (Exception e) {
282             throw new AssertionError("Failed to read staged status", e);
283         }
284     }
285 
waitForCondition(BooleanSupplier condition)286     private static void waitForCondition(BooleanSupplier condition) throws Exception {
287         int count = 0;
288         boolean lastResult;
289         while (!(lastResult = condition.getAsBoolean()) && count++ < 30) {
290             Thread.sleep(1000);
291         }
292         // Some conditions may not be stable so using the lastResult instead of
293         // condition.getAsBoolean() ensures we understand why we exited the loop.
294         assertTrue("Failed condition: " + condition, lastResult);
295     }
296 
297     private enum StateType {
298         OPERATION_IN_PROGRESS,
299         SYSTEM_RULES_VERSION,
300         CURRENT_INSTALL_STATE,
301         CURRENTLY_INSTALLED_VERSION,
302         STAGED_OPERATION_TYPE,
303         STAGED_INSTALL_VERSION,
304         ACTIVE_RULES_VERSION;
305 
getFormatStateChar()306         public String getFormatStateChar() {
307             // This switch must match values in com.android.server.timezone.RulesManagerService.
308             switch (this) {
309                 case OPERATION_IN_PROGRESS:
310                     return "p";
311                 case SYSTEM_RULES_VERSION:
312                     return "s";
313                 case CURRENT_INSTALL_STATE:
314                     return "c";
315                 case CURRENTLY_INSTALLED_VERSION:
316                     return "i";
317                 case STAGED_OPERATION_TYPE:
318                     return "o";
319                 case STAGED_INSTALL_VERSION:
320                     return "t";
321                 case ACTIVE_RULES_VERSION:
322                     return "a";
323                 default:
324                     throw new AssertionError("Unknown state type: " + this);
325             }
326         }
327     }
328 
getDeviceTimeZoneState(StateType stateType)329     private String getDeviceTimeZoneState(StateType stateType) throws Exception {
330         String output = getDevice().executeShellCommand(
331                 "dumpsys timezone -format_state " + stateType.getFormatStateChar());
332         assertNotNull(output);
333         // Output will be "Foo: bar\n". We want the "bar".
334         String value = output.split(":")[1];
335         return value.substring(1, value.length() - 1);
336     }
337 
dumpEntireTimeZoneStatusToString()338     private String dumpEntireTimeZoneStatusToString() throws Exception {
339         String output = getDevice().executeShellCommand("dumpsys timezone");
340         assertNotNull(output);
341         return output;
342     }
343 
getTimeZoneDataApkFile(String testId)344     private File getTimeZoneDataApkFile(String testId) throws Exception {
345         CompatibilityBuildHelper buildHelper = new CompatibilityBuildHelper(mBuildInfo);
346         String fileName = getTimeZoneDataApkName(testId);
347 
348         // TODO(nfuller): Replace with getTestFile(fileName) when it's available in aosp/master.
349         return new File(buildHelper.getTestsDir(), fileName);
350     }
351 
isPackageInstalled(String pkg)352     private boolean isPackageInstalled(String pkg) throws Exception {
353         for (String installedPackage : getDevice().getInstalledPackageNames()) {
354             if (pkg.equals(installedPackage)) {
355                 return true;
356             }
357         }
358         return false;
359     }
360 
uninstallPackage(String packageName)361     private String uninstallPackage(String packageName) throws Exception {
362         return getDevice().uninstallPackage(packageName);
363     }
364 }
365