blob: a875634c9f66d5caf40faf26138f6990ae2b5390 [file] [log] [blame]
Neil Fullerbe496c72017-07-07 18:19:39 +01001/*
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 */
16package com.android.timezone.xts;
17
Neil Fuller57b633a2017-07-14 17:10:13 +010018import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper;
19import com.android.tradefed.build.IBuildInfo;
Neil Fullerbe496c72017-07-07 18:19:39 +010020import com.android.tradefed.config.Option;
21import com.android.tradefed.log.LogUtil;
22import com.android.tradefed.testtype.DeviceTestCase;
Neil Fuller57b633a2017-07-14 17:10:13 +010023import com.android.tradefed.testtype.IBuildReceiver;
Neil Fuller3c4897a2017-07-18 15:43:43 +010024import com.android.tradefed.util.FileUtil;
Neil Fullerbe496c72017-07-07 18:19:39 +010025
26import java.io.File;
Neil Fullerbe496c72017-07-07 18:19:39 +010027import 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.
Neil Fullerf3a394e2017-07-25 13:37:20 +010052// http://b/64015928
Neil Fuller57b633a2017-07-14 17:10:13 +010053public class TimeZoneUpdateHostTest extends DeviceTestCase implements IBuildReceiver {
Neil Fullerbe496c72017-07-07 18:19:39 +010054
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";
Neil Fullerdb996a22017-07-12 17:23:05 +010058 private static final String STAGED_OPERATION_UNINSTALL = "Uninstall";
Neil Fullerbe496c72017-07-07 18:19:39 +010059 private static final String INSTALL_STATE_INSTALLED = "Installed";
60
Neil Fuller57b633a2017-07-14 17:10:13 +010061 private IBuildInfo mBuildInfo;
62 private File mTempDir;
Neil Fullerbe496c72017-07-07 18:19:39 +010063
64 @Option(name = "oem-data-app-package-name",
65 description="The OEM-specific package name for the data app",
66 mandatory = true)
Neil Fuller57b633a2017-07-14 17:10:13 +010067 private String mOemDataAppPackageName;
Neil Fullerbe496c72017-07-07 18:19:39 +010068
69 private String getTimeZoneDataPackageName() {
Neil Fuller57b633a2017-07-14 17:10:13 +010070 assertNotNull(mOemDataAppPackageName);
71 return mOemDataAppPackageName;
Neil Fullerbe496c72017-07-07 18:19:39 +010072 }
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)
Neil Fuller57b633a2017-07-14 17:10:13 +010079 private String mOemDataAppApkPrefix;
Neil Fullerbe496c72017-07-07 18:19:39 +010080
Neil Fuller57b633a2017-07-14 17:10:13 +010081 private String getTimeZoneDataApkName(String testId) {
82 assertNotNull(mOemDataAppApkPrefix);
83 return mOemDataAppApkPrefix + "_" + testId + ".apk";
84 }
85
86 @Override
87 public void setBuild(IBuildInfo buildInfo) {
88 mBuildInfo = buildInfo;
Neil Fullerbe496c72017-07-07 18:19:39 +010089 }
90
91 @Override
92 public void setUp() throws Exception {
93 super.setUp();
94 createTempDir();
95 resetDeviceToClean();
96 }
97
98 @Override
99 protected void tearDown() throws Exception {
100 resetDeviceToClean();
101 deleteTempDir();
102 super.tearDown();
103 }
104
105 // @Before
106 public void createTempDir() throws Exception {
Neil Fuller57b633a2017-07-14 17:10:13 +0100107 mTempDir = File.createTempFile("timeZoneUpdateTest", null);
108 assertTrue(mTempDir.delete());
109 assertTrue(mTempDir.mkdir());
Neil Fullerbe496c72017-07-07 18:19:39 +0100110 }
111
112 // @After
113 public void deleteTempDir() throws Exception {
Neil Fuller3c4897a2017-07-18 15:43:43 +0100114 FileUtil.recursiveDelete(mTempDir);
Neil Fullerbe496c72017-07-07 18:19:39 +0100115 }
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
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
Neil Fullerdb996a22017-07-12 17:23:05 +0100128 // Reboot as needed to apply any staged operation.
Neil Fullerbe496c72017-07-07 18:19:39 +0100129 if (!STAGED_OPERATION_NONE.equals(getStagedOperationType())) {
130 rebootDeviceAndWaitForRestart();
131 }
132
Neil Fullerdb996a22017-07-12 17:23:05 +0100133 // A "clean" device means no time zone data .apk installed in /data at all, try to get to
134 // that state.
Neil Fullerbe496c72017-07-07 18:19:39 +0100135 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
Neil Fullerdb996a22017-07-12 17:23:05 +0100147 // 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();
Neil Fullerbe496c72017-07-07 18:19:39 +0100154
Neil Fullerdb996a22017-07-12 17:23:05 +0100155 rebootDeviceAndWaitForRestart();
156 }
Neil Fullerbe496c72017-07-07 18:19:39 +0100157 }
158 assertActiveRulesVersion(getSystemRulesVersion());
159 assertEquals(STAGED_OPERATION_NONE, getStagedOperationType());
160 }
161
162 // @Test
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");
Neil Fuller3c4897a2017-07-18 15:43:43 +0100172 getDevice().installPackage(appFile, true /* reinstall */);
Neil Fullerbe496c72017-07-07 18:19:39 +0100173
Neil Fullerdb996a22017-07-12 17:23:05 +0100174 waitForStagedInstall(test1VersionInfo);
Neil Fullerbe496c72017-07-07 18:19:39 +0100175
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
189 public void testInstallOlderRulesVersion() throws Exception {
190 File appFile = getTimeZoneDataApkFile("test2");
Neil Fuller3c4897a2017-07-18 15:43:43 +0100191 getDevice().installPackage(appFile, true /* reinstall */);
Neil Fullerbe496c72017-07-07 18:19:39 +0100192
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
Neil Fullerbe496c72017-07-07 18:19:39 +0100204 private void rebootDeviceAndWaitForRestart() throws Exception {
205 log("Rebooting device");
Neil Fuller3c4897a2017-07-18 15:43:43 +0100206 getDevice().reboot();
Neil Fullerbe496c72017-07-07 18:19:39 +0100207 }
208
209 private void logDeviceTimeZoneState() throws Exception {
210 log("Initial device state: " + dumpEntireTimeZoneStatusToString());
211 }
212
213 private static void log(String msg) {
214 LogUtil.CLog.i(msg);
215 }
216
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
226 private String getCurrentInstalledVersion() throws Exception {
227 return waitForNoOperationInProgressAndReturn(StateType.CURRENTLY_INSTALLED_VERSION);
228 }
229
230 private String getCurrentInstallState() throws Exception {
231 return waitForNoOperationInProgressAndReturn(StateType.CURRENT_INSTALL_STATE);
232 }
233
234 private String getStagedInstallVersion() throws Exception {
235 return waitForNoOperationInProgressAndReturn(StateType.STAGED_INSTALL_VERSION);
236 }
237
238 private String getStagedOperationType() throws Exception {
239 return waitForNoOperationInProgressAndReturn(StateType.STAGED_OPERATION_TYPE);
240 }
241
242 private String getSystemRulesVersion() throws Exception {
243 return waitForNoOperationInProgressAndReturn(StateType.SYSTEM_RULES_VERSION);
244 }
245
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
256 private String waitForNoOperationInProgressAndReturn(StateType stateType) throws Exception {
257 waitForCondition(() -> !isOperationInProgress());
258 return getDeviceTimeZoneState(stateType);
259 }
260
Neil Fullerdb996a22017-07-12 17:23:05 +0100261 private void waitForStagedUninstall() throws Exception {
262 waitForCondition(() -> isStagedUninstall());
Neil Fullerbe496c72017-07-07 18:19:39 +0100263 }
264
Neil Fullerdb996a22017-07-12 17:23:05 +0100265 private void waitForStagedInstall(String versionString) throws Exception {
266 waitForCondition(() -> isStagedInstall(versionString));
267 }
268
269 private boolean isStagedUninstall() {
Neil Fullerbe496c72017-07-07 18:19:39 +0100270 try {
Neil Fullerdb996a22017-07-12 17:23:05 +0100271 return getStagedOperationType().equals(STAGED_OPERATION_UNINSTALL);
272 } catch (Exception e) {
273 throw new AssertionError("Failed to read staged status", e);
274 }
275 }
276
277 private boolean isStagedInstall(String versionString) {
278 try {
279 return getStagedOperationType().equals(STAGED_OPERATION_INSTALL)
Neil Fullerbe496c72017-07-07 18:19:39 +0100280 && getStagedInstallVersion().equals(versionString);
281 } catch (Exception e) {
282 throw new AssertionError("Failed to read staged status", e);
283 }
284 }
285
286 private static void waitForCondition(BooleanSupplier condition) throws Exception {
287 int count = 0;
Neil Fuller415f20a2017-07-24 14:34:29 +0100288 boolean lastResult;
289 while (!(lastResult = condition.getAsBoolean()) && count++ < 30) {
Neil Fullerbe496c72017-07-07 18:19:39 +0100290 Thread.sleep(1000);
291 }
Neil Fuller415f20a2017-07-24 14:34:29 +0100292 // 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);
Neil Fullerbe496c72017-07-07 18:19:39 +0100295 }
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
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
329 private String getDeviceTimeZoneState(StateType stateType) throws Exception {
Neil Fuller3c4897a2017-07-18 15:43:43 +0100330 String output = getDevice().executeShellCommand(
331 "dumpsys timezone -format_state " + stateType.getFormatStateChar());
Neil Fullerbe496c72017-07-07 18:19:39 +0100332 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
338 private String dumpEntireTimeZoneStatusToString() throws Exception {
Neil Fuller3c4897a2017-07-18 15:43:43 +0100339 String output = getDevice().executeShellCommand("dumpsys timezone");
Neil Fullerbe496c72017-07-07 18:19:39 +0100340 assertNotNull(output);
341 return output;
342 }
343
344 private File getTimeZoneDataApkFile(String testId) throws Exception {
Neil Fuller57b633a2017-07-14 17:10:13 +0100345 CompatibilityBuildHelper buildHelper = new CompatibilityBuildHelper(mBuildInfo);
346 String fileName = getTimeZoneDataApkName(testId);
Neil Fullerbe496c72017-07-07 18:19:39 +0100347
Neil Fuller57b633a2017-07-14 17:10:13 +0100348 // TODO(nfuller): Replace with getTestFile(fileName) when it's available in aosp/master.
349 return new File(buildHelper.getTestsDir(), fileName);
Neil Fullerbe496c72017-07-07 18:19:39 +0100350 }
351
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
361 private String uninstallPackage(String packageName) throws Exception {
362 return getDevice().uninstallPackage(packageName);
363 }
364}