summaryrefslogtreecommitdiff
path: root/testing/xts/src/com/android/timezone/xts/TimeZoneUpdateHostTest.java
blob: 68fabc14e8938cb88e5917709f1602226a088e53 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
/*
 * Copyright (C) 2017 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.android.timezone.xts;

import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper;
import com.android.tradefed.build.IBuildInfo;
import com.android.tradefed.config.Option;
import com.android.tradefed.device.ITestDevice;
import com.android.tradefed.log.LogUtil;
import com.android.tradefed.testtype.DeviceTestCase;
import com.android.tradefed.testtype.IBuildReceiver;
import com.android.tradefed.util.FileUtil;

import java.io.File;
import java.util.function.BooleanSupplier;

/**
 * Class for host-side tests that the time zone rules update feature works as intended. This is
 * intended to give confidence to OEMs that they have implemented / configured the OEM parts of the
 * feature correctly.
 *
 * <p>There are two main operations involved in time zone updates:
 * <ol>
 *     <li>Package installs/uninstalls - asynchronously stage operations for install</li>
 *     <li>Reboots - perform the staged operations / delete bad installed data</li>
 * </ol>
 * Both these operations are time consuming and there's a degree of non-determinism involved.
 *
 * <p>A "clean" device can also be in one of two main states depending on whether it has been wiped
 * and/or rebooted before this test runs:
 * <ul>
 *     <li>A device may have nothing staged / installed in /data/misc/zoneinfo at all.</li>
 *     <li>A device may have the time zone data from the default system image version of the time
 *     zone data app staged or installed.</li>
 * </ul>
 * This test attempts to handle both of these cases.
 *
 */
// TODO(nfuller): Switch this to JUnit4 when HostTest supports @Option with JUnit4.
// http://b/64015928
public class TimeZoneUpdateHostTest extends DeviceTestCase implements IBuildReceiver {

    // These must match equivalent values in RulesManagerService dumpsys code.
    private static final String STAGED_OPERATION_NONE = "None";
    private static final String STAGED_OPERATION_INSTALL = "Install";
    private static final String STAGED_OPERATION_UNINSTALL = "Uninstall";
    private static final String INSTALL_STATE_INSTALLED = "Installed";

    private IBuildInfo mBuildInfo;
    private File mTempDir;

    @Option(name = "oem-data-app-package-name",
            description="The OEM-specific package name for the data app",
            mandatory = true)
    private String mOemDataAppPackageName;

    private String getTimeZoneDataPackageName() {
        assertNotNull(mOemDataAppPackageName);
        return mOemDataAppPackageName;
    }

    @Option(name = "oem-data-app-apk-prefix",
            description="The OEM-specific APK name for the data app test files, e.g."
                    + "for TimeZoneDataOemCorp_test1.apk the prefix would be"
                    + "\"TimeZoneDataOemCorp\"",
            mandatory = true)
    private String mOemDataAppApkPrefix;

    private String getTimeZoneDataApkName(String testId) {
        assertNotNull(mOemDataAppApkPrefix);
        return mOemDataAppApkPrefix + "_" + testId + ".apk";
    }

    @Override
    public void setBuild(IBuildInfo buildInfo) {
        mBuildInfo = buildInfo;
    }

    @Override
    public void setUp() throws Exception {
        super.setUp();
        createTempDir();
        resetDeviceToClean();
    }

    @Override
    protected void tearDown() throws Exception {
        resetDeviceToClean();
        deleteTempDir();
        super.tearDown();
    }

    // @Before
    public void createTempDir() throws Exception {
        mTempDir = File.createTempFile("timeZoneUpdateTest", null);
        assertTrue(mTempDir.delete());
        assertTrue(mTempDir.mkdir());
    }

    // @After
    public void deleteTempDir() throws Exception {
        FileUtil.recursiveDelete(mTempDir);
    }

    /**
     * Reset the device to having no installed time zone data outside of the /system/priv-app
     * version that came with the system image.
     */
    // @Before
    // @After
    public void resetDeviceToClean() throws Exception {
        // If this fails the data app isn't present on device. No point in starting.
        assertTrue(getTimeZoneDataPackageName() + " not installed",
                isPackageInstalled(getTimeZoneDataPackageName()));

        // Reboot as needed to apply any staged operation.
        if (!STAGED_OPERATION_NONE.equals(getStagedOperationType())) {
            rebootDeviceAndWaitForRestart();
        }

        // A "clean" device means no time zone data .apk installed in /data at all, try to get to
        // that state.
        for (int i = 0; i < 2; i++) {
            logDeviceTimeZoneState();

            String errorCode = uninstallPackage(getTimeZoneDataPackageName());
            if (errorCode != null) {
                // Failed to uninstall, which we take to mean the device is "clean".
                break;
            }
            // Success, meaning there was something that could be uninstalled, so we should wait
            // for the device to react to the uninstall and reboot. If the time zone update system
            // is not configured correctly this is likely to be where tests fail.

            // If the package we uninstalled was not valid then there would be nothing installed and
            // so nothing will be staged by the uninstall. Check and do what it takes to get the
            // device to having nothing installed again.
            if (INSTALL_STATE_INSTALLED.equals(getCurrentInstallState())) {
                // We expect the device to get to the staged state "UNINSTALL", meaning it will try
                // to revert to no distro installed on next boot.
                waitForStagedUninstall();

                rebootDeviceAndWaitForRestart();
            }
        }
        assertActiveRulesVersion(getSystemRulesVersion());
        assertEquals(STAGED_OPERATION_NONE, getStagedOperationType());
    }

    // @Test
    public void testInstallNewerRulesVersion() throws Exception {
        // This information must match the rules version in test1: IANA version=2030a, revision=1
        String test1VersionInfo = "2030a,1";

        // Confirm the staged / install state before we start.
        assertFalse(test1VersionInfo.equals(getCurrentInstalledVersion()));
        assertEquals(STAGED_OPERATION_NONE, getStagedOperationType());

        File appFile = getTimeZoneDataApkFile("test1");
        getDevice().installPackage(appFile, true /* reinstall */);

        waitForStagedInstall(test1VersionInfo);

        // Confirm the install state hasn't changed.
        assertFalse(test1VersionInfo.equals(getCurrentInstalledVersion()));

        // Now reboot, and the staged version should become the installed version.
        rebootDeviceAndWaitForRestart();

        // After reboot, check the state.
        assertEquals(STAGED_OPERATION_NONE, getStagedOperationType());
        assertEquals(INSTALL_STATE_INSTALLED, getCurrentInstallState());
        assertEquals(test1VersionInfo, getCurrentInstalledVersion());
    }

    // @Test
    public void testInstallNewerRulesVersion_secondaryUser() throws Exception {
        ITestDevice device = getDevice();
        if (!device.isMultiUserSupported()) {
            // Just pass on non-multi-user devices.
            return;
        }

        int userId = device.createUser("TimeZoneTest", false /* guest */, false /* ephemeral */);
        try {

            // This information must match the rules version in test1: IANA version=2030a, revision=1
            String test1VersionInfo = "2030a,1";

            // Confirm the staged / install state before we start.
            assertFalse(test1VersionInfo.equals(getCurrentInstalledVersion()));
            assertEquals(STAGED_OPERATION_NONE, getStagedOperationType());

            File appFile = getTimeZoneDataApkFile("test1");

            // Install the app for the test user. It should still all work.
            device.installPackageForUser(appFile, true /* reinstall */, userId);

            waitForStagedInstall(test1VersionInfo);

            // Confirm the install state hasn't changed.
            assertFalse(test1VersionInfo.equals(getCurrentInstalledVersion()));

            // Now reboot, and the staged version should become the installed version.
            rebootDeviceAndWaitForRestart();

            // After reboot, check the state.
            assertEquals(STAGED_OPERATION_NONE, getStagedOperationType());
            assertEquals(INSTALL_STATE_INSTALLED, getCurrentInstallState());
            assertEquals(test1VersionInfo, getCurrentInstalledVersion());
        }
        finally {
            // If this fails, the device may be left in a bad state.
            device.removeUser(userId);
        }
    }

    // @Test
    public void testInstallOlderRulesVersion() throws Exception {
        File appFile = getTimeZoneDataApkFile("test2");
        getDevice().installPackage(appFile, true /* reinstall */);

        // The attempt to install a version of the data that is older than the version in the system
        // image should be rejected and nothing should be staged. There's currently no way (short of
        // looking at logs) to tell this has happened, but combined with other tests and given a
        // suitable delay it gives us some confidence that the attempt has been made and it was
        // rejected.

        Thread.sleep(30000);

        assertEquals(STAGED_OPERATION_NONE, getStagedOperationType());
    }

    private void rebootDeviceAndWaitForRestart() throws Exception {
        log("Rebooting device");
        getDevice().reboot();
    }

    private void logDeviceTimeZoneState() throws Exception {
        log("Initial device state: " + dumpEntireTimeZoneStatusToString());
    }

    private static void log(String msg) {
        LogUtil.CLog.i(msg);
    }

    private void assertActiveRulesVersion(String expectedRulesVersion) throws Exception {
        // Dumpsys reports the version reported by ICU, ZoneInfoDB and TimeZoneFinder and they
        // should always match.
        String expectedActiveRulesVersion =
                expectedRulesVersion + "," + expectedRulesVersion + "," + expectedRulesVersion;

        String actualActiveRulesVersion =
                waitForNoOperationInProgressAndReturn(StateType.ACTIVE_RULES_VERSION);
        assertEquals(expectedActiveRulesVersion, actualActiveRulesVersion);
    }

    private String getCurrentInstalledVersion() throws Exception {
        return waitForNoOperationInProgressAndReturn(StateType.CURRENTLY_INSTALLED_VERSION);
    }

    private String getCurrentInstallState() throws Exception {
        return waitForNoOperationInProgressAndReturn(StateType.CURRENT_INSTALL_STATE);
    }

    private String getStagedInstallVersion() throws Exception {
        return waitForNoOperationInProgressAndReturn(StateType.STAGED_INSTALL_VERSION);
    }

    private String getStagedOperationType() throws Exception {
        return waitForNoOperationInProgressAndReturn(StateType.STAGED_OPERATION_TYPE);
    }

    private String getSystemRulesVersion() throws Exception {
        return waitForNoOperationInProgressAndReturn(StateType.SYSTEM_RULES_VERSION);
    }

    private boolean isOperationInProgress() {
        try {
            String operationInProgressString =
                    getDeviceTimeZoneState(StateType.OPERATION_IN_PROGRESS);
            return Boolean.parseBoolean(operationInProgressString);
        } catch (Exception e) {
            throw new AssertionError("Failed to read staged status", e);
        }
    }

    private String waitForNoOperationInProgressAndReturn(StateType stateType) throws Exception {
        waitForCondition(() -> !isOperationInProgress());
        return getDeviceTimeZoneState(stateType);
    }

    private void waitForStagedUninstall() throws Exception {
        waitForCondition(() -> isStagedUninstall());
    }

    private void waitForStagedInstall(String versionString) throws Exception {
        waitForCondition(() -> isStagedInstall(versionString));
    }

    private boolean isStagedUninstall() {
        try {
            return getStagedOperationType().equals(STAGED_OPERATION_UNINSTALL);
        } catch (Exception e) {
            throw new AssertionError("Failed to read staged status", e);
        }
    }

    private boolean isStagedInstall(String versionString) {
        try {
            return getStagedOperationType().equals(STAGED_OPERATION_INSTALL)
                    && getStagedInstallVersion().equals(versionString);
        } catch (Exception e) {
            throw new AssertionError("Failed to read staged status", e);
        }
    }

    private static void waitForCondition(BooleanSupplier condition) throws Exception {
        int count = 0;
        boolean lastResult;
        while (!(lastResult = condition.getAsBoolean()) && count++ < 30) {
            Thread.sleep(1000);
        }
        // Some conditions may not be stable so using the lastResult instead of
        // condition.getAsBoolean() ensures we understand why we exited the loop.
        assertTrue("Failed condition: " + condition, lastResult);
    }

    private enum StateType {
        OPERATION_IN_PROGRESS,
        SYSTEM_RULES_VERSION,
        CURRENT_INSTALL_STATE,
        CURRENTLY_INSTALLED_VERSION,
        STAGED_OPERATION_TYPE,
        STAGED_INSTALL_VERSION,
        ACTIVE_RULES_VERSION;

        public String getFormatStateChar() {
            // This switch must match values in com.android.server.timezone.RulesManagerService.
            switch (this) {
                case OPERATION_IN_PROGRESS:
                    return "p";
                case SYSTEM_RULES_VERSION:
                    return "s";
                case CURRENT_INSTALL_STATE:
                    return "c";
                case CURRENTLY_INSTALLED_VERSION:
                    return "i";
                case STAGED_OPERATION_TYPE:
                    return "o";
                case STAGED_INSTALL_VERSION:
                    return "t";
                case ACTIVE_RULES_VERSION:
                    return "a";
                default:
                    throw new AssertionError("Unknown state type: " + this);
            }
        }
    }

    private String getDeviceTimeZoneState(StateType stateType) throws Exception {
        String output = getDevice().executeShellCommand(
                "dumpsys timezone -format_state " + stateType.getFormatStateChar());
        assertNotNull(output);
        // Output will be "Foo: bar\n". We want the "bar".
        String value = output.split(":")[1];
        return value.substring(1, value.length() - 1);
    }

    private String dumpEntireTimeZoneStatusToString() throws Exception {
        String output = getDevice().executeShellCommand("dumpsys timezone");
        assertNotNull(output);
        return output;
    }

    private File getTimeZoneDataApkFile(String testId) throws Exception {
        CompatibilityBuildHelper buildHelper = new CompatibilityBuildHelper(mBuildInfo);
        String fileName = getTimeZoneDataApkName(testId);

        // TODO(nfuller): Replace with getTestFile(fileName) when it's available in aosp/master.
        return new File(buildHelper.getTestsDir(), fileName);
    }

    private boolean isPackageInstalled(String pkg) throws Exception {
        for (String installedPackage : getDevice().getInstalledPackageNames()) {
            if (pkg.equals(installedPackage)) {
                return true;
            }
        }
        return false;
    }

    private String uninstallPackage(String packageName) throws Exception {
        return getDevice().uninstallPackage(packageName);
    }
}