1 /* 2 * Copyright (C) 2020 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 android.tzdata.mts; 17 18 import static org.junit.Assert.assertEquals; 19 import static org.junit.Assert.assertTrue; 20 import static org.junit.Assert.fail; 21 import static org.junit.Assume.assumeTrue; 22 23 import static java.util.stream.Collectors.toMap; 24 25 import android.icu.util.VersionInfo; 26 import android.os.Build; 27 import android.util.TimeUtils; 28 29 import com.android.i18n.timezone.TzDataSetVersion; 30 31 import org.junit.Test; 32 33 import java.io.File; 34 import java.io.FileInputStream; 35 import java.io.IOException; 36 import java.io.UncheckedIOException; 37 import java.lang.reflect.Method; 38 import java.nio.charset.StandardCharsets; 39 import java.util.HashSet; 40 import java.util.Map; 41 import java.util.Set; 42 43 /** 44 * Tests concerning version information associated with, or affected by, the time zone data module. 45 * 46 * <p>Generally we don't want to assert anything too specific here (like exact version), since that 47 * would mean more to update every tzdb release. Also, if the module being tested contains an old 48 * version then why wouldn't the tests be just as old too? 49 */ 50 public class TimeZoneVersionTest { 51 52 private static final File TIME_ZONE_MODULE_VERSION_FILE = 53 new File("/apex/com.android.tzdata/etc/tz/tz_version"); 54 55 private static final String VERSIONED_DATA_LOCATION = 56 "/apex/com.android.tzdata/etc/tz/versioned/"; 57 58 // Android V. 59 private static final int MINIMAL_SUPPORTED_MAJOR_VERSION = 8; 60 // Android B. 61 // LINT.IfChange 62 private static final int THE_LATEST_MAJOR_VERSION = 9; 63 // LINT.ThenChange(/android_icu4j/libcore_bridge/src/java/com/android/i18n/timezone/TzDataSetVersion.java) 64 65 @Test timeZoneModuleIsCompatibleWithThisRelease()66 public void timeZoneModuleIsCompatibleWithThisRelease() { 67 String majorVersion = readMajorFormatVersionForVersion(getCurrentFormatMajorVersion()); 68 69 // Each time a release version of Android is declared, this list needs to be updated to 70 // map the Android release to the time zone format version it uses. 71 if (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) { 72 assertEquals("003", majorVersion); 73 } else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) { 74 assertEquals("004", majorVersion); 75 } else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.S) { 76 assertEquals("005", majorVersion); 77 } else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.S_V2) { 78 // S_V2 is 5.x, as the format version did not change from S. 79 assertEquals("005", majorVersion); 80 } else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.TIRAMISU) { 81 assertEquals("006", majorVersion); 82 } else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { 83 assertEquals("007", majorVersion); 84 } else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.VANILLA_ICE_CREAM) { 85 assertEquals("008", majorVersion); 86 } else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.BAKLAVA) { 87 // The "main" branch is also the staging area for the next Android release that won't 88 // have an Android release constant yet. Instead, we have to infer what the expected tz 89 // data set version should be when the SDK_INT identifies it as the latest Android release 90 // in case it is actually the "main" branch. Below we assume that an increment to ICU is 91 // involved with each release of Android and requires an tz data set version increment. 92 93 94 // ICU version in B is 76. When we update it in a next release major version 95 // should be updated too. 96 if (VersionInfo.ICU_VERSION.getMajor() > 76) { 97 assertEquals("010", majorVersion); 98 } else { 99 assertEquals("009", majorVersion); 100 } 101 } else { 102 // If this fails, a new API level has likely been finalized and can be made 103 // an explicit case. Keep this clause and add an explicit "else if" above. 104 // Consider removing any checks for pre-release devices too if they're not 105 // needed for now. 106 fail("Unhandled SDK_INT version:" + Build.VERSION.SDK_INT); 107 } 108 } 109 110 /** 111 * Confirms that tzdb version information available via published APIs is consistent. 112 */ 113 @Test tzdbVersionIsConsistentAcrossApis()114 public void tzdbVersionIsConsistentAcrossApis() { 115 String tzModuleTzdbVersion = readTzDbVersionFromModuleVersionFile(); 116 117 String icu4jTzVersion = android.icu.util.TimeZone.getTZDataVersion(); 118 assertEquals(tzModuleTzdbVersion, icu4jTzVersion); 119 120 assertEquals(tzModuleTzdbVersion, TimeUtils.getTimeZoneDatabaseVersion()); 121 } 122 123 @Test majorVersion_isValid()124 public void majorVersion_isValid() { 125 String msg = "THE_LATEST_MAJOR_VERSION is " 126 + THE_LATEST_MAJOR_VERSION 127 + " but getCurrentMajorFormatVersion() is greater: " 128 + getCurrentFormatMajorVersion(); 129 assertTrue(msg, THE_LATEST_MAJOR_VERSION >= getCurrentFormatMajorVersion()); 130 } 131 132 @Test versionFiles_areConsistent()133 public void versionFiles_areConsistent() { 134 // Test validates data installed in /versioned/ directory. It was introduced in tzdata6, 135 // and it is targeted to Android V+ only. 136 assumeTrue(Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM); 137 138 // Version in tz_version under versioned/N should be N. 139 for (int version = MINIMAL_SUPPORTED_MAJOR_VERSION; 140 version <= THE_LATEST_MAJOR_VERSION; 141 ++version) { 142 // Version in tz_version is zero padded. 143 String expectedVersion = "%03d".formatted(version); 144 assertEquals(expectedVersion, readMajorFormatVersionForVersion(version)); 145 } 146 147 // IANA version should be the same across tz_version files. 148 Set<File> versionFiles = new HashSet<>(); 149 versionFiles.add(TIME_ZONE_MODULE_VERSION_FILE); 150 151 for (int version = MINIMAL_SUPPORTED_MAJOR_VERSION; 152 version <= THE_LATEST_MAJOR_VERSION; 153 ++version) { 154 versionFiles.add( 155 new File("%s/%d/tz_version".formatted(VERSIONED_DATA_LOCATION, version))); 156 } 157 158 Map<String, String> ianaVersionInVersionFile = versionFiles.stream() 159 .collect(toMap(File::toString, TimeZoneVersionTest::readTzDbVersionFrom)); 160 161 String msg = "Versions are not consistent: " + ianaVersionInVersionFile; 162 assertEquals(msg, 1, Set.of(ianaVersionInVersionFile.values()).size()); 163 } 164 getCurrentFormatMajorVersion()165 private static int getCurrentFormatMajorVersion() { 166 // TzDataSetVersion was moved from /libcore to /external/icu in S. 167 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { 168 return TzDataSetVersion.currentFormatMajorVersion(); 169 } else { 170 try { 171 Class<?> libcoreTzDataSetVersion = 172 Class.forName("libcore.timezone.TzDataSetVersion"); 173 Method m = libcoreTzDataSetVersion.getDeclaredMethod("currentFormatMajorVersion"); 174 m.setAccessible(true); 175 return (int) m.invoke(null); 176 } catch (ReflectiveOperationException roe) { 177 throw new AssertionError(roe); 178 } 179 } 180 } 181 182 /** 183 * Reads up to {@code maxBytes} bytes from the specified file. The returned array can be 184 * shorter than {@code maxBytes} if the file is shorter. 185 */ readBytes(File file, int maxBytes)186 private static byte[] readBytes(File file, int maxBytes) { 187 if (maxBytes <= 0) { 188 throw new IllegalArgumentException("maxBytes ==" + maxBytes); 189 } 190 191 try (FileInputStream in = new FileInputStream(file)) { 192 byte[] max = new byte[maxBytes]; 193 int bytesRead = in.read(max, 0, maxBytes); 194 byte[] toReturn = new byte[bytesRead]; 195 System.arraycopy(max, 0, toReturn, 0, bytesRead); 196 return toReturn; 197 } catch (IOException ioe) { 198 throw new UncheckedIOException(ioe); 199 } 200 } 201 readTzDbVersionFrom(File file)202 private static String readTzDbVersionFrom(File file) { 203 byte[] versionBytes = readBytes(file, 13); 204 assertEquals(13, versionBytes.length); 205 206 String versionString = new String(versionBytes, StandardCharsets.US_ASCII); 207 // Format is: xxx.yyy|zzzzz|...., we want zzzzz 208 String[] dataSetVersionComponents = versionString.split("\\|"); 209 return dataSetVersionComponents[1]; 210 } 211 readTzDbVersionFromModuleVersionFile()212 private static String readTzDbVersionFromModuleVersionFile() { 213 return readTzDbVersionFrom(TIME_ZONE_MODULE_VERSION_FILE); 214 } 215 readMajorFormatVersionFrom(File file)216 private static String readMajorFormatVersionFrom(File file) { 217 byte[] versionBytes = readBytes(file, 7); 218 assertEquals(7, versionBytes.length); 219 220 String versionString = new String(versionBytes, StandardCharsets.US_ASCII); 221 // Format is: xxx.yyy|zzzz|.... we want xxx 222 String[] dataSetVersionComponents = versionString.split("\\."); 223 return dataSetVersionComponents[0]; 224 } 225 readMajorFormatVersionForVersion(int version)226 private static String readMajorFormatVersionForVersion(int version) { 227 File tzVersion; 228 if (version >= 8) { 229 tzVersion = new File( 230 "%s/%d/tz_version".formatted(VERSIONED_DATA_LOCATION, version)); 231 } else { 232 tzVersion = TIME_ZONE_MODULE_VERSION_FILE; 233 } 234 return readMajorFormatVersionFrom(tzVersion); 235 } 236 } 237