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