Host-side test for the tzdatacheck binary
A test that exercises tzdatacheck binary in various ways.
Many of the scenarios tested here would be impossible to
check on a correctly functioning device.
Bug: 31008728
Test: CTS: run cts -m CtsHostTzDataTests
Change-Id: I2c6c069a9f535c5177615229a5cc355bf91f8774
diff --git a/hostsidetests/tzdata/Android.mk b/hostsidetests/tzdata/Android.mk
new file mode 100644
index 0000000..6eab01c
--- /dev/null
+++ b/hostsidetests/tzdata/Android.mk
@@ -0,0 +1,33 @@
+# 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.
+
+LOCAL_PATH := $(call my-dir)
+
+include $(CLEAR_VARS)
+
+# Only compile source java files in this apk.
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_MODULE := CtsHostTzDataTests
+
+LOCAL_JAVA_LIBRARIES := tradefed
+
+LOCAL_STATIC_JAVA_LIBRARIES := tzdata-testing-host tzdata_shared2-host tzdata_tools2-host
+
+LOCAL_CTS_TEST_PACKAGE := android.host.tzdata
+
+# Tag this module as a cts test artifact
+LOCAL_COMPATIBILITY_SUITE := cts
+
+include $(BUILD_CTS_HOST_JAVA_LIBRARY)
diff --git a/hostsidetests/tzdata/AndroidTest.xml b/hostsidetests/tzdata/AndroidTest.xml
new file mode 100644
index 0000000..cccfe5a
--- /dev/null
+++ b/hostsidetests/tzdata/AndroidTest.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<configuration description="Config for CTS tzdatacheck host test cases">
+ <test class="com.android.compatibility.common.tradefed.testtype.JarHostTest" >
+ <option name="jar" value="CtsHostTzDataTests.jar" />
+ </test>
+</configuration>
diff --git a/hostsidetests/tzdata/src/com/android/cts/tzdata/TzDataCheckTest.java b/hostsidetests/tzdata/src/com/android/cts/tzdata/TzDataCheckTest.java
new file mode 100644
index 0000000..c059e89
--- /dev/null
+++ b/hostsidetests/tzdata/src/com/android/cts/tzdata/TzDataCheckTest.java
@@ -0,0 +1,976 @@
+/*
+ * 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.cts.tzdata;
+
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.testtype.DeviceTestCase;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Comparator;
+import java.util.StringJoiner;
+import java.util.function.Consumer;
+import libcore.tzdata.shared2.DistroVersion;
+import libcore.tzdata.shared2.TimeZoneDistro;
+import libcore.tzdata.testing.ZoneInfoTestHelper;
+import libcore.tzdata.update2.tools.TimeZoneDistroBuilder;
+
+import static org.junit.Assert.assertArrayEquals;
+
+/**
+ * Tests for the tzdatacheck binary.
+ *
+ * <p>The tzdatacheck binary operates over two directories: the "system directory" containing the
+ * time zone rules in the system image, and a "data directory" in the data partition which can
+ * optionally contain time zone rules data files for bionic/libcore and ICU.
+ *
+ * <p>This test executes the tzdatacheck binary to confirm it operates correctly in a number of
+ * simulated situations; simulated system and data directories in various states are created in a
+ * location the shell user has permission to access and the tzdatacheck binary is then executed.
+ * The status code and directory state after execution is then used to determine if the tzdatacheck
+ * binary operated correctly.
+ *
+ * <p>Most of the tests below prepare simulated directory structure for the system and data dirs
+ * on the host before pushing them to the device. Device state is then checked rather than syncing
+ * the files back.
+ */
+public class TzDataCheckTest extends DeviceTestCase {
+
+ /**
+ * The name of the directory containing the current time zone rules data beneath
+ * {@link #mDataDir}. Also known to {@link libcore.tzdata.update2.TimeZoneDistroInstaller} and
+ * tzdatacheck.cpp.
+ */
+ private static final String CURRENT_DIR_NAME = "current";
+
+ /**
+ * The name of the directory containing the staged time zone rules data beneath
+ * {@link #mDataDir}. Also known to {@link libcore.tzdata.update2.TimeZoneDistroInstaller} and
+ * tzdatacheck.cpp.
+ */
+ private static final String STAGED_DIR_NAME = "staged";
+
+ /**
+ * The name of the file inside the staged directory that indicates the staged operation is an
+ * uninstall. Also known to {@link libcore.tzdata.update2.TimeZoneDistroInstaller} and
+ * tzdatacheck.cpp.
+ */
+ private static final String UNINSTALL_TOMBSTONE_FILE_NAME = "STAGED_UNINSTALL_TOMBSTONE";
+
+ /**
+ * The name of the /system time zone data file. Also known to
+ * {@link libcore.tzdata.update2.TimeZoneDistroInstaller} and tzdatacheck.cpp.
+ */
+ private static final String SYSTEM_TZDATA_FILE_NAME = "tzdata";
+
+ /** A valid time zone rules version guaranteed to be older than {@link #RULES_VERSION_TWO} */
+ private static final String RULES_VERSION_ONE = "2016g";
+ /** A valid time zone rules version guaranteed to be newer than {@link #RULES_VERSION_ONE} */
+ private static final String RULES_VERSION_TWO = "2016h";
+ /**
+ * An arbitrary, valid time zone rules version used when it doesn't matter what the rules
+ * version is.
+ */
+ private static final String VALID_RULES_VERSION = RULES_VERSION_ONE;
+
+ /** An arbitrary valid revision number. */
+ private static final int VALID_REVISION = 1;
+
+ private String mDeviceAndroidRootDir;
+ private PathPair mTestRootDir;
+ private PathPair mSystemDir;
+ private PathPair mDataDir;
+
+ public void setUp() throws Exception {
+ super.setUp();
+
+ // It's not clear how we would get this without invoking "/system/bin/sh", but we need the
+ // value first to do so. It has been hardcoded instead.
+ mDeviceAndroidRootDir = "/system";
+
+ // Create a test root directory on host and device.
+ Path hostTestRootDir = Files.createTempDirectory("tzdatacheck_test");
+ mTestRootDir = new PathPair(
+ hostTestRootDir,
+ "/data/local/tmp/tzdatacheck_test");
+ createDeviceDirectory(mTestRootDir);
+
+ // tzdatacheck requires two directories: a "system" path and a "data" path.
+ mSystemDir = mTestRootDir.createSubPath("system_dir");
+ mDataDir = mTestRootDir.createSubPath("data_dir");
+
+ // Create the host-side directory structure (for preparing files before pushing them to
+ // device and looking at files retrieved from device).
+ createHostDirectory(mSystemDir);
+ createHostDirectory(mDataDir);
+
+ // Create the equivalent device-side directory structure for receiving files.
+ createDeviceDirectory(mSystemDir);
+ createDeviceDirectory(mDataDir);
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ // Remove the test root directories that have been created by this test.
+ deleteHostDirectory(mTestRootDir, true /* failOnError */);
+ deleteDeviceDirectory(mTestRootDir, true /* failOnError */);
+ super.tearDown();
+ }
+
+ public void testTooFewArgs() throws Exception {
+ // No need to set up or push files to the device for this test.
+ assertEquals(1, runTzDataCheckWithArgs(new String[0]));
+ assertEquals(1, runTzDataCheckWithArgs(new String[] { "oneArg" }));
+ }
+
+ // {dataDir}/staged exists but it is a file.
+ public void testStaging_stagingDirIsFile() throws Exception {
+ // Set up the /system directory structure on host.
+ createSystemTzDataFileOnHost(VALID_RULES_VERSION);
+
+ // Set up the /data directory structure on host.
+ PathPair dataStagedDir = mDataDir.createSubPath(STAGED_DIR_NAME);
+ // Create a file with the same name as the directory that tzdatacheck expects.
+ Files.write(dataStagedDir.hostPath, new byte[] { 'a' });
+
+ // Push the host test directory and contents to the device.
+ pushHostTestDirToDevice();
+
+ // Execute tzdatacheck and check the status code. Failures due to staging issues are
+ // generally ignored providing the device is left in a reasonable state.
+ assertEquals(0, runTzDataCheckOnDevice());
+
+ // Assert the file was just ignored. This is a fairly arbitrary choice to leave it rather
+ // than delete.
+ assertDevicePathExists(dataStagedDir);
+ assertDevicePathIsFile(dataStagedDir);
+ }
+
+ // {dataDir}/staged exists but /current dir is a file.
+ public void testStaging_uninstall_currentDirIsFile() throws Exception {
+ // Set up the /system directory structure on host.
+ createSystemTzDataFileOnHost(VALID_RULES_VERSION);
+
+ // Set up the /data directory structure on host.
+
+ // Create a staged uninstall.
+ PathPair dataStagedDir = mDataDir.createSubPath(STAGED_DIR_NAME);
+ createStagedUninstallOnHost(dataStagedDir);
+
+ // Create a file with the same name as the directory that tzdatacheck expects.
+ PathPair dataCurrentDir = mDataDir.createSubPath(CURRENT_DIR_NAME);
+ Files.write(dataCurrentDir.hostPath, new byte[] { 'a' });
+
+ // Push the host test directory and contents to the device.
+ pushHostTestDirToDevice();
+
+ // Execute tzdatacheck and check the status code.
+ assertEquals(0, runTzDataCheckOnDevice());
+
+ // Assert the device was left in a valid "uninstalled" state.
+ assertDevicePathDoesNotExist(dataStagedDir);
+ assertDevicePathDoesNotExist(dataCurrentDir);
+ }
+
+ // {dataDir}/staged contains an uninstall, but there is nothing to uninstall.
+ public void testStaging_uninstall_noCurrent() throws Exception {
+ // Set up the /system directory structure on host.
+ createSystemTzDataFileOnHost(VALID_RULES_VERSION);
+
+ PathPair dataCurrentDir = mDataDir.createSubPath(CURRENT_DIR_NAME);
+
+ // Set up the /data directory structure on host.
+
+ // Create a staged uninstall.
+ PathPair dataStagedDir = mDataDir.createSubPath(STAGED_DIR_NAME);
+ createStagedUninstallOnHost(dataStagedDir);
+
+ // Push the host test directory and contents to the device.
+ pushHostTestDirToDevice();
+
+ // Execute tzdatacheck and check the status code. Failures due to staging issues are
+ // generally ignored providing the device is left in a reasonable state.
+ assertEquals(0, runTzDataCheckOnDevice());
+
+ // Assert the device was left in a valid "uninstalled" state.
+ assertDevicePathDoesNotExist(dataStagedDir);
+ assertDevicePathDoesNotExist(dataCurrentDir);
+ }
+
+ // {dataDir}/staged contains an uninstall, and there is something to uninstall.
+ public void testStaging_uninstall_withCurrent() throws Exception {
+ // Set up the /system directory structure on host.
+ createSystemTzDataFileOnHost(VALID_RULES_VERSION);
+
+ // Set up the /data directory structure on host.
+
+ // Create a staged uninstall.
+ PathPair dataStagedDir = mDataDir.createSubPath(STAGED_DIR_NAME);
+ createStagedUninstallOnHost(dataStagedDir);
+
+ // Create a current installed distro.
+ PathPair dataCurrentDir = mDataDir.createSubPath(CURRENT_DIR_NAME);
+ TimeZoneDistro distro = createValidDistroBuilder().build();
+ unpackOnHost(dataCurrentDir, distro);
+
+ // Push the host test directory and contents to the device.
+ pushHostTestDirToDevice();
+
+ // Execute tzdatacheck and check the status code. Failures due to staging issues are
+ // generally ignored providing the device is left in a reasonable state.
+ assertEquals(0, runTzDataCheckOnDevice());
+
+ // Assert the device was left in a valid "uninstalled" state.
+ assertDevicePathDoesNotExist(dataStagedDir);
+ assertDevicePathDoesNotExist(dataCurrentDir);
+ }
+
+ // {dataDir}/staged exists but /current dir is a file.
+ public void testStaging_install_currentDirIsFile() throws Exception {
+ // Set up the /system directory structure on host.
+ createSystemTzDataFileOnHost(VALID_RULES_VERSION);
+
+ // Set up the /data directory structure on host.
+
+ // Create a staged install.
+ PathPair dataStagedDir = mDataDir.createSubPath(STAGED_DIR_NAME);
+ TimeZoneDistro distro = createValidDistroBuilder().build();
+ unpackOnHost(dataStagedDir, distro);
+
+ // Create a file with the same name as the directory that tzdatacheck expects.
+ PathPair dataCurrentDir = mDataDir.createSubPath(CURRENT_DIR_NAME);
+ Files.write(dataCurrentDir.hostPath, new byte[] { 'a' });
+
+ // Push the host test directory and contents to the device.
+ pushHostTestDirToDevice();
+
+ // Execute tzdatacheck and check the status code. Failures due to staging issues are
+ // generally ignored providing the device is left in a reasonable state.
+ assertEquals(0, runTzDataCheckOnDevice());
+
+ // Assert the device was left in a valid "installed" state.
+ assertDevicePathDoesNotExist(dataStagedDir);
+ assertDeviceDirContainsDistro(dataCurrentDir, distro);
+ }
+
+ // {dataDir}/staged contains an install, but there is nothing to replace.
+ public void testStaging_install_noCurrent() throws Exception {
+ // Set up the /system directory structure on host.
+ createSystemTzDataFileOnHost(VALID_RULES_VERSION);
+
+ PathPair dataCurrentDir = mDataDir.createSubPath(CURRENT_DIR_NAME);
+
+ // Set up the /data directory structure on host.
+
+ // Create a staged install.
+ PathPair dataStagedDir = mDataDir.createSubPath(STAGED_DIR_NAME);
+ TimeZoneDistro stagedDistro = createValidDistroBuilder().build();
+ unpackOnHost(dataStagedDir, stagedDistro);
+
+ // Push the host test directory and contents to the device.
+ pushHostTestDirToDevice();
+
+ // Execute tzdatacheck and check the status code. Failures due to staging issues are
+ // generally ignored providing the device is left in a reasonable state.
+ assertEquals(0, runTzDataCheckOnDevice());
+
+ // Assert the device was left in a valid "installed" state.
+ assertDevicePathDoesNotExist(dataStagedDir);
+ assertDeviceDirContainsDistro(dataCurrentDir, stagedDistro);
+ }
+
+ // {dataDir}/staged contains an install, and there is something to replace.
+ public void testStaging_install_withCurrent() throws Exception {
+ // Set up the /system directory structure on host.
+ createSystemTzDataFileOnHost(VALID_RULES_VERSION);
+
+ DistroVersion currentDistroVersion = new DistroVersion(
+ DistroVersion.CURRENT_FORMAT_MAJOR_VERSION, 1, VALID_RULES_VERSION, 1);
+ DistroVersion stagedDistroVersion = new DistroVersion(
+ DistroVersion.CURRENT_FORMAT_MAJOR_VERSION, 1, VALID_RULES_VERSION, 2);
+
+ // Set up the /data directory structure on host.
+
+ // Create a staged uninstall.
+ PathPair dataStagedDir = mDataDir.createSubPath(STAGED_DIR_NAME);
+ TimeZoneDistro stagedDistro = createValidDistroBuilder()
+ .setDistroVersion(stagedDistroVersion)
+ .build();
+ unpackOnHost(dataStagedDir, stagedDistro);
+
+ // Create a current installed distro.
+ PathPair dataCurrentDir = mDataDir.createSubPath(CURRENT_DIR_NAME);
+ TimeZoneDistro currentDistro = createValidDistroBuilder()
+ .setDistroVersion(currentDistroVersion)
+ .build();
+ unpackOnHost(dataCurrentDir, currentDistro);
+
+ // Push the host test directory and contents to the device.
+ pushHostTestDirToDevice();
+
+ // Execute tzdatacheck and check the status code. Failures due to staging issues are
+ // generally ignored providing the device is left in a reasonable state.
+ assertEquals(0, runTzDataCheckOnDevice());
+
+ // Assert the device was left in a valid "installed" state.
+ // The stagedDistro should now be the one in the current dir.
+ assertDevicePathDoesNotExist(dataStagedDir);
+ assertDeviceDirContainsDistro(dataCurrentDir, stagedDistro);
+ }
+
+ // {dataDir}/staged contains an invalid install, and there is something to replace.
+ // Most of the invalid cases are tested without staging; this is just to prove that staging
+ // an invalid distro is handled the same.
+ public void testStaging_install_withCurrent_invalidStaged() throws Exception {
+ // Set up the /system directory structure on host.
+ createSystemTzDataFileOnHost(VALID_RULES_VERSION);
+
+ // Set up the /data directory structure on host.
+
+ // Create a staged uninstall which contains invalid.
+ PathPair dataStagedDir = mDataDir.createSubPath(STAGED_DIR_NAME);
+ TimeZoneDistro stagedDistro = createValidDistroBuilder()
+ .clearVersionForTests()
+ .buildUnvalidated();
+ unpackOnHost(dataStagedDir, stagedDistro);
+
+ // Create a current installed distro.
+ PathPair dataCurrentDir = mDataDir.createSubPath(CURRENT_DIR_NAME);
+ TimeZoneDistro currentDistro = createValidDistroBuilder().build();
+ unpackOnHost(dataCurrentDir, currentDistro);
+
+ // Push the host test directory and contents to the device.
+ pushHostTestDirToDevice();
+
+ // Execute tzdatacheck and check the status code. The staged directory will have become the
+ // current one, but then it will be discovered to be invalid and will be removed.
+ assertEquals(3, runTzDataCheckOnDevice());
+
+ // Assert the device was left in a valid "uninstalled" state.
+ assertDevicePathDoesNotExist(dataStagedDir);
+ assertDevicePathDoesNotExist(dataCurrentDir);
+ }
+
+ // No {dataDir}/current exists.
+ public void testNoCurrentDataDir() throws Exception {
+ // Set up the /system directory structure on host.
+ createSystemTzDataFileOnHost(VALID_RULES_VERSION);
+
+ // Deliberately not creating anything on host in the data dir here, leaving the empty
+ // structure.
+
+ // Push the host test directory and contents to the device.
+ pushHostTestDirToDevice();
+
+ // Execute tzdatacheck and check the status code.
+ assertEquals(0, runTzDataCheckOnDevice());
+ }
+
+ // {dataDir}/current exists but it is a file.
+ public void testCurrentDataDirIsFile() throws Exception {
+ // Set up the /system directory structure on host.
+ createSystemTzDataFileOnHost(VALID_RULES_VERSION);
+
+ // Set up the /data directory structure on host.
+ PathPair dataCurrentDir = mDataDir.createSubPath(CURRENT_DIR_NAME);
+ // Create a file with the same name as the directory that tzdatacheck expects.
+ Files.write(dataCurrentDir.hostPath, new byte[] { 'a' });
+
+ // Push the host test directory and contents to the device.
+ pushHostTestDirToDevice();
+
+ // Execute tzdatacheck and check the status code.
+ assertEquals(2, runTzDataCheckOnDevice());
+
+ // Assert the file was just ignored. This is a fairly arbitrary choice to leave it rather
+ // than delete.
+ assertDevicePathExists(dataCurrentDir);
+ assertDevicePathIsFile(dataCurrentDir);
+ }
+
+ // {dataDir}/current exists but is missing the distro version file.
+ public void testMissingDataDirDistroVersionFile() throws Exception {
+ // Set up the /system directory structure on host.
+ createSystemTzDataFileOnHost(VALID_RULES_VERSION);
+
+ // Set up the /data directory structure on host.
+ PathPair dataCurrentDir = mDataDir.createSubPath(CURRENT_DIR_NAME);
+ TimeZoneDistro distroWithoutAVersionFile = createValidDistroBuilder()
+ .clearVersionForTests()
+ .buildUnvalidated();
+ unpackOnHost(dataCurrentDir, distroWithoutAVersionFile);
+
+ // Push the host test directory and contents to the device.
+ pushHostTestDirToDevice();
+
+ // Execute tzdatacheck and check the status code.
+ assertEquals(3, runTzDataCheckOnDevice());
+
+ // Assert the current data directory was deleted.
+ assertDevicePathDoesNotExist(dataCurrentDir);
+ }
+
+ // {dataDir}/current exists but the distro version file is short.
+ public void testShortDataDirDistroVersionFile() throws Exception {
+ // Set up the /system directory structure on host.
+ createSystemTzDataFileOnHost(VALID_RULES_VERSION);
+
+ // Set up the /data directory structure on host.
+ PathPair dataCurrentDir = mDataDir.createSubPath(CURRENT_DIR_NAME);
+ unpackOnHost(dataCurrentDir, createValidDistroBuilder().build());
+ // Replace the distro version file with a short file.
+ Path distroVersionFile =
+ dataCurrentDir.hostPath.resolve(TimeZoneDistro.DISTRO_VERSION_FILE_NAME);
+ assertHostFileExists(distroVersionFile);
+ Files.write(distroVersionFile, new byte[3]);
+
+ // Push the host test directory and contents to the device.
+ pushHostTestDirToDevice();
+
+ // Execute tzdatacheck and check the status code.
+ assertEquals(3, runTzDataCheckOnDevice());
+
+ // Assert the current data directory was deleted.
+ assertDevicePathDoesNotExist(dataCurrentDir);
+ }
+
+ // {dataDir}/current exists and the distro version file is long enough, but contains junk.
+ public void testCorruptDistroVersionFile() throws Exception {
+ // Set up the /system directory structure on host.
+ createSystemTzDataFileOnHost(VALID_RULES_VERSION);
+
+ // Set up the /data directory structure on host.
+ PathPair dataCurrentDir = mDataDir.createSubPath(CURRENT_DIR_NAME);
+ unpackOnHost(dataCurrentDir, createValidDistroBuilder().build());
+
+ // Replace the distro version file with junk.
+ Path distroVersionFile =
+ dataCurrentDir.hostPath.resolve(TimeZoneDistro.DISTRO_VERSION_FILE_NAME);
+ assertHostFileExists(distroVersionFile);
+
+ int fileLength = (int) Files.size(distroVersionFile);
+ byte[] junkArray = new byte[fileLength]; // all zeros
+ Files.write(distroVersionFile, junkArray);
+
+ // Push the host test directory and contents to the device.
+ pushHostTestDirToDevice();
+
+ // Execute tzdatacheck and check the status code.
+ assertEquals(4, runTzDataCheckOnDevice());
+
+ // Assert the current data directory was deleted.
+ assertDevicePathDoesNotExist(dataCurrentDir);
+ }
+
+ // {dataDir}/current exists but the distro version is incorrect.
+ public void testInvalidMajorDistroVersion_older() throws Exception {
+ // Set up the /system directory structure on host.
+ createSystemTzDataFileOnHost(VALID_RULES_VERSION);
+
+ // Set up the /data directory structure on host.
+ PathPair dataCurrentDir = mDataDir.createSubPath(CURRENT_DIR_NAME);
+ DistroVersion oldMajorDistroVersion = new DistroVersion(
+ DistroVersion.CURRENT_FORMAT_MAJOR_VERSION - 1, 1, VALID_RULES_VERSION, 1);
+ TimeZoneDistro distro = createValidDistroBuilder()
+ .setDistroVersion(oldMajorDistroVersion)
+ .build();
+ unpackOnHost(dataCurrentDir, distro);
+
+ // Push the host test directory and contents to the device.
+ pushHostTestDirToDevice();
+
+ // Execute tzdatacheck and check the status code.
+ assertEquals(5, runTzDataCheckOnDevice());
+
+ // Assert the current data directory was deleted.
+ assertDevicePathDoesNotExist(dataCurrentDir);
+ }
+
+ // {dataDir}/current exists but the distro version is incorrect.
+ public void testInvalidMajorDistroVersion_newer() throws Exception {
+ // Set up the /system directory structure on host.
+ createSystemTzDataFileOnHost(VALID_RULES_VERSION);
+
+ // Set up the /data directory structure on host.
+ PathPair dataCurrentDir = mDataDir.createSubPath(CURRENT_DIR_NAME);
+ DistroVersion newMajorDistroVersion = new DistroVersion(
+ DistroVersion.CURRENT_FORMAT_MAJOR_VERSION + 1,
+ DistroVersion.CURRENT_FORMAT_MINOR_VERSION,
+ VALID_RULES_VERSION, VALID_REVISION);
+ TimeZoneDistro distro = createValidDistroBuilder()
+ .setDistroVersion(newMajorDistroVersion)
+ .build();
+ unpackOnHost(dataCurrentDir, distro);
+
+ // Push the host test directory and contents to the device.
+ pushHostTestDirToDevice();
+
+ // Execute tzdatacheck and check the status code.
+ assertEquals(5, runTzDataCheckOnDevice());
+
+ // Assert the current data directory was deleted.
+ assertDevicePathDoesNotExist(dataCurrentDir);
+ }
+
+ // {dataDir}/current exists but the distro version is incorrect.
+ public void testInvalidMinorDistroVersion_older() throws Exception {
+ // Set up the /system directory structure on host.
+ createSystemTzDataFileOnHost(VALID_RULES_VERSION);
+
+ // Set up the /data directory structure on host.
+ PathPair dataCurrentDir = mDataDir.createSubPath(CURRENT_DIR_NAME);
+ DistroVersion oldMinorDistroVersion = new DistroVersion(
+ DistroVersion.CURRENT_FORMAT_MAJOR_VERSION,
+ DistroVersion.CURRENT_FORMAT_MINOR_VERSION - 1,
+ VALID_RULES_VERSION, 1);
+ TimeZoneDistro distro = createValidDistroBuilder()
+ .setDistroVersion(oldMinorDistroVersion)
+ .build();
+ unpackOnHost(dataCurrentDir, distro);
+
+ // Push the host test directory and contents to the device.
+ pushHostTestDirToDevice();
+
+ // Execute tzdatacheck and check the status code.
+ assertEquals(5, runTzDataCheckOnDevice());
+
+ // Assert the current data directory was deleted.
+ assertDevicePathDoesNotExist(dataCurrentDir);
+ }
+
+ // {dataDir}/current exists but the distro version is newer (which is accepted because it should
+ // be backwards compatible).
+ public void testValidMinorDistroVersion_newer() throws Exception {
+ // Set up the /system directory structure on host.
+ createSystemTzDataFileOnHost(VALID_RULES_VERSION);
+
+ // Set up the /data directory structure on host.
+ PathPair dataCurrentDir = mDataDir.createSubPath(CURRENT_DIR_NAME);
+ DistroVersion newMajorDistroVersion = new DistroVersion(
+ DistroVersion.CURRENT_FORMAT_MAJOR_VERSION,
+ DistroVersion.CURRENT_FORMAT_MINOR_VERSION + 1,
+ VALID_RULES_VERSION, VALID_REVISION);
+ TimeZoneDistro distro = createValidDistroBuilder()
+ .setDistroVersion(newMajorDistroVersion)
+ .build();
+ unpackOnHost(dataCurrentDir, distro);
+
+ // Push the host test directory and contents to the device.
+ pushHostTestDirToDevice();
+
+ // Execute tzdatacheck and check the status code.
+ assertEquals(0, runTzDataCheckOnDevice());
+
+ // Assert the current data directory was not touched.
+ assertDeviceDirContainsDistro(dataCurrentDir, distro);
+ }
+
+ // {dataDir}/current is valid but the tzdata file in /system is missing.
+ public void testSystemTzDataFileMissing() throws Exception {
+ // Deliberately not writing anything in /system here.
+
+ // Set up the /data directory structure on host.
+ PathPair dataCurrentDir = mDataDir.createSubPath(CURRENT_DIR_NAME);
+ TimeZoneDistro validDistro = createValidDistroBuilder().build();
+ unpackOnHost(dataCurrentDir, validDistro);
+
+ // Push the host test directory and contents to the device.
+ pushHostTestDirToDevice();
+
+ // Execute tzdatacheck and check the status code.
+ assertEquals(6, runTzDataCheckOnDevice());
+
+ // Assert the current data directory was not touched.
+ assertDeviceDirContainsDistro(dataCurrentDir, validDistro);
+ }
+
+ // {dataDir}/current is valid but the tzdata file in /system has an invalid header.
+ public void testSystemTzDataFileCorrupt() throws Exception {
+ // Set up the /system directory structure on host.
+ byte[] invalidTzDataBytes = new byte[20];
+ Files.write(mSystemDir.hostPath.resolve(SYSTEM_TZDATA_FILE_NAME), invalidTzDataBytes);
+
+ // Set up the /data directory structure on host.
+ PathPair dataCurrentDir = mDataDir.createSubPath(CURRENT_DIR_NAME);
+ TimeZoneDistro validDistro = createValidDistroBuilder().build();
+ unpackOnHost(dataCurrentDir, validDistro);
+
+ // Push the host test directory and contents to the device.
+ pushHostTestDirToDevice();
+
+ // Execute tzdatacheck and check the status code.
+ assertEquals(7, runTzDataCheckOnDevice());
+
+ // Assert the current data directory was not touched.
+ assertDeviceDirContainsDistro(dataCurrentDir, validDistro);
+ }
+
+ // {dataDir}/current is valid and the tzdata file in /system is older.
+ public void testSystemTzRulesOlder() throws Exception {
+ // Set up the /system directory structure on host.
+ createSystemTzDataFileOnHost(RULES_VERSION_ONE);
+
+ // Set up the /data directory structure on host.
+ PathPair dataCurrentDir = mDataDir.createSubPath(CURRENT_DIR_NAME);
+ // Newer than RULES_VERSION_ONE in /system
+ final String distroRulesVersion = RULES_VERSION_TWO;
+ DistroVersion distroVersion = new DistroVersion(
+ DistroVersion.CURRENT_FORMAT_MAJOR_VERSION,
+ DistroVersion.CURRENT_FORMAT_MINOR_VERSION, distroRulesVersion, VALID_REVISION);
+ TimeZoneDistro distro = createValidDistroBuilder()
+ .setDistroVersion(distroVersion)
+ .setTzDataFile(createValidTzDataBytes(distroRulesVersion))
+ .build();
+ unpackOnHost(dataCurrentDir, distro);
+
+ // Push the host test directory and contents to the device.
+ pushHostTestDirToDevice();
+
+ // Execute tzdatacheck and check the status code.
+ assertEquals(0, runTzDataCheckOnDevice());
+
+ // Assert the current data directory was not touched.
+ assertDeviceDirContainsDistro(dataCurrentDir, distro);
+ }
+
+ // {dataDir}/current is valid and the tzdata file in /system is the same (and should be kept).
+ public void testSystemTzDataSame() throws Exception {
+ // Set up the /system directory structure on host.
+ final String systemRulesVersion = VALID_RULES_VERSION;
+ createSystemTzDataFileOnHost(systemRulesVersion);
+
+ // Set up the /data directory structure on host.
+ PathPair dataCurrentDir = mDataDir.createSubPath(CURRENT_DIR_NAME);
+ DistroVersion distroVersion = new DistroVersion(
+ DistroVersion.CURRENT_FORMAT_MAJOR_VERSION,
+ DistroVersion.CURRENT_FORMAT_MINOR_VERSION, systemRulesVersion, VALID_REVISION);
+ TimeZoneDistro distro = createValidDistroBuilder()
+ .setDistroVersion(distroVersion)
+ .setTzDataFile(createValidTzDataBytes(systemRulesVersion))
+ .build();
+ unpackOnHost(dataCurrentDir, distro);
+
+ // Push the host test directory and contents to the device.
+ pushHostTestDirToDevice();
+
+ // Execute tzdatacheck and check the status code.
+ assertEquals(0, runTzDataCheckOnDevice());
+
+ // Assert the current data directory was not touched.
+ assertDeviceDirContainsDistro(dataCurrentDir, distro);
+ }
+
+ // {dataDir}/current is valid and the tzdata file in /system is the newer.
+ public void testSystemTzDataNewer() throws Exception {
+ // Set up the /system directory structure on host.
+ String systemRulesVersion = RULES_VERSION_TWO;
+ createSystemTzDataFileOnHost(systemRulesVersion);
+
+ // Set up the /data directory structure on host.
+ PathPair dataCurrentDir = mDataDir.createSubPath(CURRENT_DIR_NAME);
+ String distroRulesVersion = RULES_VERSION_ONE; // Older than the system version.
+ DistroVersion distroVersion = new DistroVersion(
+ DistroVersion.CURRENT_FORMAT_MAJOR_VERSION,
+ DistroVersion.CURRENT_FORMAT_MINOR_VERSION,
+ distroRulesVersion,
+ VALID_REVISION);
+ TimeZoneDistro distro = createValidDistroBuilder()
+ .setDistroVersion(distroVersion)
+ .setTzDataFile(createValidTzDataBytes(distroRulesVersion))
+ .build();
+ unpackOnHost(dataCurrentDir, distro);
+
+ // Push the host test directory and contents to the device.
+ pushHostTestDirToDevice();
+
+ // Execute tzdatacheck and check the status code.
+ assertEquals(0, runTzDataCheckOnDevice());
+
+ // It is important the dataCurrentDir is deleted in this case - this test case is the main
+ // reason tzdatacheck exists.
+ assertDevicePathDoesNotExist(dataCurrentDir);
+ }
+
+ private void createSystemTzDataFileOnHost(String systemRulesVersion) throws IOException {
+ byte[] systemTzData = createValidTzDataBytes(systemRulesVersion);
+ Files.write(mSystemDir.hostPath.resolve(SYSTEM_TZDATA_FILE_NAME), systemTzData);
+ }
+
+ private static void createStagedUninstallOnHost(PathPair stagedDir) throws Exception {
+ createHostDirectory(stagedDir);
+
+ PathPair uninstallTombstoneFile = stagedDir.createSubPath(UNINSTALL_TOMBSTONE_FILE_NAME);
+ // Create an empty file.
+ new FileOutputStream(uninstallTombstoneFile.hostFile()).close();
+ }
+
+ private static void unpackOnHost(PathPair path, TimeZoneDistro distro) throws Exception {
+ createHostDirectory(path);
+ distro.extractTo(path.hostFile());
+ }
+
+ private static TimeZoneDistroBuilder createValidDistroBuilder() throws Exception {
+ String distroRulesVersion = VALID_RULES_VERSION;
+ DistroVersion validDistroVersion =
+ new DistroVersion(
+ DistroVersion.CURRENT_FORMAT_MAJOR_VERSION,
+ DistroVersion.CURRENT_FORMAT_MINOR_VERSION,
+ distroRulesVersion, VALID_REVISION);
+ return new TimeZoneDistroBuilder()
+ .setDistroVersion(validDistroVersion)
+ .setTzDataFile(createValidTzDataBytes(distroRulesVersion))
+ .setIcuDataFile(new byte[10]);
+ }
+
+ private static byte[] createValidTzDataBytes(String rulesVersion) {
+ return new ZoneInfoTestHelper.TzDataBuilder()
+ .initializeToValid()
+ .setHeaderMagic("tzdata" + rulesVersion)
+ .build();
+ }
+
+ private int runTzDataCheckOnDevice() throws Exception {
+ return runTzDataCheckWithArgs(new String[] { mSystemDir.devicePath, mDataDir.devicePath });
+ }
+
+ private int runTzDataCheckWithArgs(String[] args) throws Exception {
+ String command = createTzDataCheckCommand(mDeviceAndroidRootDir, args);
+ return executeCommandOnDeviceWithResultCode(command).statusCode;
+ }
+
+ private static String createTzDataCheckCommand(String rootDir, String[] args) {
+ StringJoiner joiner = new StringJoiner(" ");
+ String tzDataCheckCommand = rootDir + "/bin/tzdatacheck";
+ joiner.add(tzDataCheckCommand);
+ for (String arg : args) {
+ joiner.add(arg);
+ }
+ return joiner.toString();
+ }
+
+ private static void assertHostFileExists(Path path) {
+ assertTrue(Files.exists(path));
+ }
+
+ private String executeCommandOnDeviceRaw(String command) throws DeviceNotAvailableException {
+ return getDevice().executeShellCommand(command);
+ }
+
+ private void createDeviceDirectory(PathPair dir) throws DeviceNotAvailableException {
+ executeCommandOnDeviceRaw("mkdir -p " + dir.devicePath);
+ }
+
+ private static void createHostDirectory(PathPair dir) throws Exception {
+ Files.createDirectory(dir.hostPath);
+ }
+
+ private static class ShellResult {
+ final String output;
+ final int statusCode;
+
+ private ShellResult(String output, int statusCode) {
+ this.output = output;
+ this.statusCode = statusCode;
+ }
+ }
+
+ private ShellResult executeCommandOnDeviceWithResultCode(String command) throws Exception {
+ // A file to hold the script we're going to create.
+ PathPair scriptFile = mTestRootDir.createSubPath("script.sh");
+ // A file to hold the output of the script.
+ PathPair scriptOut = mTestRootDir.createSubPath("script.out");
+
+ // The content of the script. Runs the command, capturing stdout and stderr to scriptOut
+ // and printing the result code.
+ String hostScriptContent = command + " > " + scriptOut.devicePath + " 2>&1 ; echo -n $?";
+
+ // Parse and return the result.
+ try {
+ Files.write(scriptFile.hostPath, hostScriptContent.getBytes(StandardCharsets.US_ASCII));
+
+ // Push the script to the device.
+ pushFile(scriptFile);
+
+ // Execute the script using "sh".
+ String execCommandUnderShell =
+ mDeviceAndroidRootDir + "/bin/sh " + scriptFile.devicePath;
+ String resultCodeString = executeCommandOnDeviceRaw(execCommandUnderShell);
+
+ // Pull back scriptOut to the host and read the content.
+ pullFile(scriptOut);
+ byte[] outputBytes = Files.readAllBytes(scriptOut.hostPath);
+ String output = new String(outputBytes, StandardCharsets.US_ASCII);
+
+ int resultCode;
+ try {
+ resultCode = Integer.parseInt(resultCodeString);
+ } catch (NumberFormatException e) {
+ fail("Command: " + command
+ + " returned a non-integer: \"" + resultCodeString + "\""
+ + ", output=\"" + output + "\"");
+ return null;
+ }
+ return new ShellResult(output, resultCode);
+ } finally {
+ deleteDeviceFile(scriptFile, false /* failOnError */);
+ deleteDeviceFile(scriptOut, false /* failOnError */);
+ deleteHostFile(scriptFile, false /* failOnError */);
+ deleteHostFile(scriptOut, false /* failOnError */);
+ }
+ }
+
+ private void pushHostTestDirToDevice() throws Exception {
+ assertTrue(getDevice().pushDir(mTestRootDir.hostFile(), mTestRootDir.devicePath));
+ }
+
+ private void pullFile(PathPair file) throws DeviceNotAvailableException {
+ assertTrue("Could not pull file " + file.devicePath + " to " + file.hostFile(),
+ getDevice().pullFile(file.devicePath, file.hostFile()));
+ }
+
+ private void pushFile(PathPair file) throws DeviceNotAvailableException {
+ assertTrue("Could not push file " + file.hostFile() + " to " + file.devicePath,
+ getDevice().pushFile(file.hostFile(), file.devicePath));
+ }
+
+ private void deleteHostFile(PathPair file, boolean failOnError) {
+ try {
+ Files.deleteIfExists(file.hostPath);
+ } catch (IOException e) {
+ if (failOnError) {
+ fail(e);
+ }
+ }
+ }
+
+ private void deleteDeviceDirectory(PathPair dir, boolean failOnError)
+ throws DeviceNotAvailableException {
+ String deviceDir = dir.devicePath;
+ try {
+ executeCommandOnDeviceRaw("rm -r " + deviceDir);
+ } catch (Exception e) {
+ if (failOnError) {
+ throw deviceFail(e);
+ }
+ }
+ }
+
+ private void deleteDeviceFile(PathPair file, boolean failOnError)
+ throws DeviceNotAvailableException {
+ try {
+ assertDevicePathIsFile(file);
+ executeCommandOnDeviceRaw("rm " + file.devicePath);
+ } catch (Exception e) {
+ if (failOnError) {
+ throw deviceFail(e);
+ }
+ }
+ }
+
+ private static void deleteHostDirectory(PathPair dir, final boolean failOnError) {
+ Path hostPath = dir.hostPath;
+ if (Files.exists(hostPath)) {
+ Consumer<Path> pathConsumer = file -> {
+ try {
+ Files.delete(file);
+ } catch (Exception e) {
+ if (failOnError) {
+ fail(e);
+ }
+ }
+ };
+
+ try {
+ Files.walk(hostPath).sorted(Comparator.reverseOrder()).forEach(pathConsumer);
+ } catch (IOException e) {
+ fail(e);
+ }
+ }
+ }
+
+ private void assertDevicePathExists(PathPair path) throws DeviceNotAvailableException {
+ assertTrue(getDevice().doesFileExist(path.devicePath));
+ }
+
+ private void assertDeviceDirContainsDistro(PathPair distroPath, TimeZoneDistro expectedDistro)
+ throws Exception {
+ // Pull back just the version file and compare it.
+ File localFile = mTestRootDir.createSubPath("temp.file").hostFile();
+ try {
+ String remoteVersionFile = distroPath.devicePath + "/"
+ + TimeZoneDistro.DISTRO_VERSION_FILE_NAME;
+ assertTrue("Could not pull file " + remoteVersionFile + " to " + localFile,
+ getDevice().pullFile(remoteVersionFile, localFile));
+
+ byte[] bytes = Files.readAllBytes(localFile.toPath());
+ assertArrayEquals(bytes, expectedDistro.getDistroVersion().toBytes());
+ } finally {
+ localFile.delete();
+ }
+ }
+
+ private void assertDevicePathDoesNotExist(PathPair path) throws DeviceNotAvailableException {
+ assertFalse(getDevice().doesFileExist(path.devicePath));
+ }
+
+ private void assertDevicePathIsFile(PathPair path) throws DeviceNotAvailableException {
+ // This check cannot rely on getDevice().getFile(devicePath).isDirectory() here because that
+ // requires that the user has rights to list all files beneath each and every directory in
+ // the path. That is not the case for the shell user and the /data and /data/local
+ // directories. http://b/35753041.
+ String output = executeCommandOnDeviceRaw("stat -c %F " + path.devicePath);
+ assertTrue(path.devicePath + " not a file. Received: " + output,
+ output.startsWith("regular") && output.endsWith("file\n"));
+ }
+
+ private static DeviceNotAvailableException deviceFail(Exception e)
+ throws DeviceNotAvailableException {
+ if (e instanceof DeviceNotAvailableException) {
+ throw (DeviceNotAvailableException) e;
+ }
+ fail(e);
+ return null;
+ }
+
+ private static void fail(Exception e) {
+ e.printStackTrace();
+ fail(e.getMessage());
+ }
+
+ /** A path that has equivalents on both host and device. */
+ private static class PathPair {
+ private final Path hostPath;
+ private final String devicePath;
+
+ PathPair(Path hostPath, String devicePath) {
+ this.hostPath = hostPath;
+ this.devicePath = devicePath;
+ }
+
+ File hostFile() {
+ return hostPath.toFile();
+ }
+
+ PathPair createSubPath(String s) {
+ return new PathPair(hostPath.resolve(s), devicePath + "/" + s);
+ }
+ }
+}