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