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 17package libcore.util; 18 19import org.junit.After; 20import org.junit.Before; 21import org.junit.Test; 22 23import android.icu.util.TimeZone; 24 25import java.io.IOException; 26import java.nio.charset.StandardCharsets; 27import java.nio.file.FileVisitResult; 28import java.nio.file.Files; 29import java.nio.file.Path; 30import java.nio.file.SimpleFileVisitor; 31import java.nio.file.attribute.BasicFileAttributes; 32import java.util.Arrays; 33import java.util.HashMap; 34import java.util.HashSet; 35import java.util.List; 36import java.util.Map; 37import java.util.Set; 38import java.util.stream.Collectors; 39 40import static org.junit.Assert.assertEquals; 41import static org.junit.Assert.assertNull; 42import static org.junit.Assert.fail; 43 44public class TimeZoneFinderTest { 45 46 private static final int HOUR_MILLIS = 60 * 60 * 1000; 47 48 // Zones used in the tests. NEW_YORK_TZ and LONDON_TZ chosen because they never overlap but both 49 // have DST. 50 private static final TimeZone NEW_YORK_TZ = TimeZone.getTimeZone("America/New_York"); 51 private static final TimeZone LONDON_TZ = TimeZone.getTimeZone("Europe/London"); 52 // A zone that matches LONDON_TZ for WHEN_NO_DST. It does not have DST so differs for WHEN_DST. 53 private static final TimeZone REYKJAVIK_TZ = TimeZone.getTimeZone("Atlantic/Reykjavik"); 54 // Another zone that matches LONDON_TZ for WHEN_NO_DST. It does not have DST so differs for 55 // WHEN_DST. 56 private static final TimeZone UTC_TZ = TimeZone.getTimeZone("Etc/UTC"); 57 58 // 22nd July 2017, 13:14:15 UTC (DST time in all the timezones used in these tests that observe 59 // DST). 60 private static final long WHEN_DST = 1500729255000L; 61 // 22nd January 2018, 13:14:15 UTC (non-DST time in all timezones used in these tests). 62 private static final long WHEN_NO_DST = 1516626855000L; 63 64 private static final int LONDON_DST_OFFSET_MILLIS = HOUR_MILLIS; 65 private static final int LONDON_NO_DST_OFFSET_MILLIS = 0; 66 67 private static final int NEW_YORK_DST_OFFSET_MILLIS = -4 * HOUR_MILLIS; 68 private static final int NEW_YORK_NO_DST_OFFSET_MILLIS = -5 * HOUR_MILLIS; 69 70 private Path testDir; 71 72 @Before 73 public void setUp() throws Exception { 74 testDir = Files.createTempDirectory("TimeZoneFinderTest"); 75 } 76 77 @After 78 public void tearDown() throws Exception { 79 // Delete the testDir and all contents. 80 Files.walkFileTree(testDir, new SimpleFileVisitor<Path>() { 81 @Override 82 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) 83 throws IOException { 84 Files.delete(file); 85 return FileVisitResult.CONTINUE; 86 } 87 88 @Override 89 public FileVisitResult postVisitDirectory(Path dir, IOException exc) 90 throws IOException { 91 Files.delete(dir); 92 return FileVisitResult.CONTINUE; 93 } 94 }); 95 } 96 97 @Test 98 public void createInstanceWithFallback() throws Exception { 99 String validXml1 = "<timezones>\n" 100 + " <countryzones>\n" 101 + " <country code=\"gb\">\n" 102 + " <id>Europe/London</id>\n" 103 + " </country>\n" 104 + " </countryzones>\n" 105 + "</timezones>\n"; 106 String validXml2 = "<timezones>\n" 107 + " <countryzones>\n" 108 + " <country code=\"gb\">\n" 109 + " <id>Europe/Paris</id>\n" 110 + " </country>\n" 111 + " </countryzones>\n" 112 + "</timezones>\n"; 113 114 String invalidXml = "<foo></foo>\n"; 115 checkValidateThrowsParserException(invalidXml); 116 117 String validFile1 = createFile(validXml1); 118 String validFile2 = createFile(validXml2); 119 String invalidFile = createFile(invalidXml); 120 String missingFile = createMissingFile(); 121 122 TimeZoneFinder file1ThenFile2 = 123 TimeZoneFinder.createInstanceWithFallback(validFile1, validFile2); 124 assertZonesEqual(zones("Europe/London"), file1ThenFile2.lookupTimeZonesByCountry("gb")); 125 126 TimeZoneFinder missingFileThenFile1 = 127 TimeZoneFinder.createInstanceWithFallback(missingFile, validFile1); 128 assertZonesEqual(zones("Europe/London"), 129 missingFileThenFile1.lookupTimeZonesByCountry("gb")); 130 131 TimeZoneFinder file2ThenFile1 = 132 TimeZoneFinder.createInstanceWithFallback(validFile2, validFile1); 133 assertZonesEqual(zones("Europe/Paris"), file2ThenFile1.lookupTimeZonesByCountry("gb")); 134 135 // We assume the file has been validated so an invalid file is not checked ahead of time. 136 // We will find out when we look something up. 137 TimeZoneFinder invalidThenValid = 138 TimeZoneFinder.createInstanceWithFallback(invalidFile, validFile1); 139 assertNull(invalidThenValid.lookupTimeZonesByCountry("gb")); 140 141 // This is not a normal case: It would imply a define shipped without a file in /system! 142 TimeZoneFinder missingFiles = 143 TimeZoneFinder.createInstanceWithFallback(missingFile, missingFile); 144 assertNull(missingFiles.lookupTimeZonesByCountry("gb")); 145 } 146 147 @Test 148 public void xmlParsing_emptyFile() throws Exception { 149 checkValidateThrowsParserException(""); 150 } 151 152 @Test 153 public void xmlParsing_unexpectedRootElement() throws Exception { 154 checkValidateThrowsParserException("<foo></foo>\n"); 155 } 156 157 @Test 158 public void xmlParsing_missingCountryZones() throws Exception { 159 checkValidateThrowsParserException("<timezones></timezones>\n"); 160 } 161 162 @Test 163 public void xmlParsing_noCountriesOk() throws Exception { 164 validate("<timezones>\n" 165 + " <countryzones>\n" 166 + " </countryzones>\n" 167 + "</timezones>\n"); 168 } 169 170 @Test 171 public void xmlParsing_unexpectedComments() throws Exception { 172 TimeZoneFinder finder = validate("<timezones>\n" 173 + " <countryzones>\n" 174 + " <country code=\"gb\">\n" 175 + " <!-- This is a comment -->" 176 + " <id>Europe/London</id>\n" 177 + " </country>\n" 178 + " </countryzones>\n" 179 + "</timezones>\n"); 180 assertZonesEqual(zones("Europe/London"), finder.lookupTimeZonesByCountry("gb")); 181 182 // This is a crazy comment, but also helps prove that TEXT nodes are coalesced by the 183 // parser. 184 finder = validate("<timezones>\n" 185 + " <countryzones>\n" 186 + " <country code=\"gb\">\n" 187 + " <id>Europe/<!-- Don't freak out! -->London</id>\n" 188 + " </country>\n" 189 + " </countryzones>\n" 190 + "</timezones>\n"); 191 assertZonesEqual(zones("Europe/London"), finder.lookupTimeZonesByCountry("gb")); 192 } 193 194 @Test 195 public void xmlParsing_unexpectedElementsIgnored() throws Exception { 196 String unexpectedElement = "<unexpected-element>\n<a /></unexpected-element>\n"; 197 TimeZoneFinder finder = validate("<timezones>\n" 198 + " " + unexpectedElement 199 + " <countryzones>\n" 200 + " <country code=\"gb\">\n" 201 + " <id>Europe/London</id>\n" 202 + " </country>\n" 203 + " </countryzones>\n" 204 + "</timezones>\n"); 205 assertZonesEqual(zones("Europe/London"), finder.lookupTimeZonesByCountry("gb")); 206 207 finder = validate("<timezones>\n" 208 + " <countryzones>\n" 209 + " " + unexpectedElement 210 + " <country code=\"gb\">\n" 211 + " <id>Europe/London</id>\n" 212 + " </country>\n" 213 + " </countryzones>\n" 214 + "</timezones>\n"); 215 assertZonesEqual(zones("Europe/London"), finder.lookupTimeZonesByCountry("gb")); 216 217 finder = validate("<timezones>\n" 218 + " <countryzones>\n" 219 + " <country code=\"gb\">\n" 220 + " " + unexpectedElement 221 + " <id>Europe/London</id>\n" 222 + " </country>\n" 223 + " </countryzones>\n" 224 + "</timezones>\n"); 225 assertZonesEqual(zones("Europe/London"), finder.lookupTimeZonesByCountry("gb")); 226 227 finder = validate("<timezones>\n" 228 + " <countryzones>\n" 229 + " <country code=\"gb\">\n" 230 + " <id>Europe/London</id>\n" 231 + " " + unexpectedElement 232 + " <id>Europe/Paris</id>\n" 233 + " </country>\n" 234 + " </countryzones>\n" 235 + "</timezones>\n"); 236 assertZonesEqual(zones("Europe/London", "Europe/Paris"), 237 finder.lookupTimeZonesByCountry("gb")); 238 239 finder = validate("<timezones>\n" 240 + " <countryzones>\n" 241 + " <country code=\"gb\">\n" 242 + " <id>Europe/London</id>\n" 243 + " </country>\n" 244 + " " + unexpectedElement 245 + " </countryzones>\n" 246 + "</timezones>\n"); 247 assertZonesEqual(zones("Europe/London"), finder.lookupTimeZonesByCountry("gb")); 248 249 // This test is important because it ensures we can extend the format in future with 250 // more information. 251 finder = validate("<timezones>\n" 252 + " <countryzones>\n" 253 + " <country code=\"gb\">\n" 254 + " <id>Europe/London</id>\n" 255 + " </country>\n" 256 + " </countryzones>\n" 257 + " " + unexpectedElement 258 + "</timezones>\n"); 259 assertZonesEqual(zones("Europe/London"), finder.lookupTimeZonesByCountry("gb")); 260 } 261 262 @Test 263 public void xmlParsing_unexpectedTextIgnored() throws Exception { 264 String unexpectedText = "unexpected-text"; 265 TimeZoneFinder finder = validate("<timezones>\n" 266 + " " + unexpectedText 267 + " <countryzones>\n" 268 + " <country code=\"gb\">\n" 269 + " <id>Europe/London</id>\n" 270 + " </country>\n" 271 + " </countryzones>\n" 272 + "</timezones>\n"); 273 assertZonesEqual(zones("Europe/London"), finder.lookupTimeZonesByCountry("gb")); 274 275 finder = validate("<timezones>\n" 276 + " <countryzones>\n" 277 + " " + unexpectedText 278 + " <country code=\"gb\">\n" 279 + " <id>Europe/London</id>\n" 280 + " </country>\n" 281 + " </countryzones>\n" 282 + "</timezones>\n"); 283 assertZonesEqual(zones("Europe/London"), finder.lookupTimeZonesByCountry("gb")); 284 285 finder = validate("<timezones>\n" 286 + " <countryzones>\n" 287 + " <country code=\"gb\">\n" 288 + " " + unexpectedText 289 + " <id>Europe/London</id>\n" 290 + " </country>\n" 291 + " </countryzones>\n" 292 + "</timezones>\n"); 293 assertZonesEqual(zones("Europe/London"), finder.lookupTimeZonesByCountry("gb")); 294 295 finder = validate("<timezones>\n" 296 + " <countryzones>\n" 297 + " <country code=\"gb\">\n" 298 + " <id>Europe/London</id>\n" 299 + " " + unexpectedText 300 + " <id>Europe/Paris</id>\n" 301 + " </country>\n" 302 + " </countryzones>\n" 303 + "</timezones>\n"); 304 assertZonesEqual(zones("Europe/London", "Europe/Paris"), 305 finder.lookupTimeZonesByCountry("gb")); 306 } 307 308 @Test 309 public void xmlParsing_truncatedInput() throws Exception { 310 checkValidateThrowsParserException("<timezones>\n"); 311 312 checkValidateThrowsParserException("<timezones>\n" 313 + " <countryzones>\n"); 314 315 checkValidateThrowsParserException("<timezones>\n" 316 + " <countryzones>\n" 317 + " <country code=\"gb\">\n"); 318 319 checkValidateThrowsParserException("<timezones>\n" 320 + " <countryzones>\n" 321 + " <country code=\"gb\">\n" 322 + " <id>Europe/London</id>\n"); 323 324 checkValidateThrowsParserException("<timezones>\n" 325 + " <countryzones>\n" 326 + " <country code=\"gb\">\n" 327 + " <id>Europe/London</id>\n" 328 + " </country>\n"); 329 330 checkValidateThrowsParserException("<timezones>\n" 331 + " <countryzones>\n" 332 + " <country code=\"gb\">\n" 333 + " <id>Europe/London</id>\n" 334 + " </country>\n" 335 + " </countryzones>\n"); 336 } 337 338 @Test 339 public void xmlParsing_unexpectedChildInTimeZoneIdThrows() throws Exception { 340 checkValidateThrowsParserException("<timezones>\n" 341 + " <countryzones>\n" 342 + " <country code=\"gb\">\n" 343 + " <id><unexpected-element /></id>\n" 344 + " </country>\n" 345 + " </countryzones>\n" 346 + "</timezones>\n"); 347 } 348 349 @Test 350 public void xmlParsing_unknownTimeZoneIdIgnored() throws Exception { 351 TimeZoneFinder finder = validate("<timezones>\n" 352 + " <countryzones>\n" 353 + " <country code=\"gb\">\n" 354 + " <id>Unknown_Id</id>\n" 355 + " <id>Europe/London</id>\n" 356 + " </country>\n" 357 + " </countryzones>\n" 358 + "</timezones>\n"); 359 assertZonesEqual(zones("Europe/London"), finder.lookupTimeZonesByCountry("gb")); 360 } 361 362 @Test 363 public void xmlParsing_missingCountryCode() throws Exception { 364 checkValidateThrowsParserException("<timezones>\n" 365 + " <countryzones>\n" 366 + " <country>\n" 367 + " <id>Europe/London</id>\n" 368 + " </country>\n" 369 + " </countryzones>\n" 370 + "</timezones>\n"); 371 } 372 373 @Test 374 public void xmlParsing_unknownCountryReturnsNull() throws Exception { 375 TimeZoneFinder finder = validate("<timezones>\n" 376 + " <countryzones>\n" 377 + " </countryzones>\n" 378 + "</timezones>\n"); 379 assertNull(finder.lookupTimeZonesByCountry("gb")); 380 } 381 382 @Test 383 public void lookupTimeZonesByCountry_structuresAreImmutable() throws Exception { 384 TimeZoneFinder finder = validate("<timezones>\n" 385 + " <countryzones>\n" 386 + " <country code=\"gb\">\n" 387 + " <id>Europe/London</id>\n" 388 + " </country>\n" 389 + " </countryzones>\n" 390 + "</timezones>\n"); 391 392 List<TimeZone> gbList = finder.lookupTimeZonesByCountry("gb"); 393 assertEquals(1, gbList.size()); 394 assertImmutableList(gbList); 395 assertImmutableTimeZone(gbList.get(0)); 396 397 assertNull(finder.lookupTimeZonesByCountry("unknown")); 398 } 399 400 @Test 401 public void lookupTimeZoneByCountryAndOffset_unknownCountry() throws Exception { 402 TimeZoneFinder finder = validate("<timezones>\n" 403 + " <countryzones>\n" 404 + " <country code=\"xx\">\n" 405 + " <id>Europe/London</id>\n" 406 + " </country>\n" 407 + " </countryzones>\n" 408 + "</timezones>\n"); 409 410 // Demonstrate the arguments work for a known country. 411 assertZoneEquals(LONDON_TZ, 412 finder.lookupTimeZoneByCountryAndOffset("xx", LONDON_DST_OFFSET_MILLIS, 413 true /* isDst */, WHEN_DST, null /* bias */)); 414 415 // Test with an unknown country. 416 String unknownCountryCode = "yy"; 417 assertNull(finder.lookupTimeZoneByCountryAndOffset(unknownCountryCode, 418 LONDON_DST_OFFSET_MILLIS, true /* isDst */, WHEN_DST, null /* bias */)); 419 420 assertNull(finder.lookupTimeZoneByCountryAndOffset(unknownCountryCode, 421 LONDON_DST_OFFSET_MILLIS, true /* isDst */, WHEN_DST, LONDON_TZ /* bias */)); 422 } 423 424 @Test 425 public void lookupTimeZoneByCountryAndOffset_oneCandidate() throws Exception { 426 TimeZoneFinder finder = validate("<timezones>\n" 427 + " <countryzones>\n" 428 + " <country code=\"xx\">\n" 429 + " <id>Europe/London</id>\n" 430 + " </country>\n" 431 + " </countryzones>\n" 432 + "</timezones>\n"); 433 434 // The three parameters match the configured zone: offset, isDst and when. 435 assertZoneEquals(LONDON_TZ, 436 finder.lookupTimeZoneByCountryAndOffset("xx", LONDON_DST_OFFSET_MILLIS, 437 true /* isDst */, WHEN_DST, null /* bias */)); 438 assertZoneEquals(LONDON_TZ, 439 finder.lookupTimeZoneByCountryAndOffset("xx", LONDON_NO_DST_OFFSET_MILLIS, 440 false /* isDst */, WHEN_NO_DST, null /* bias */)); 441 442 // Some lookup failure cases where the offset, isDst and when do not match the configured 443 // zone. 444 TimeZone noDstMatch1 = finder.lookupTimeZoneByCountryAndOffset("xx", 445 LONDON_DST_OFFSET_MILLIS, true /* isDst */, WHEN_NO_DST, null /* bias */); 446 assertNull(noDstMatch1); 447 448 TimeZone noDstMatch2 = finder.lookupTimeZoneByCountryAndOffset("xx", 449 LONDON_DST_OFFSET_MILLIS, false /* isDst */, WHEN_NO_DST, null /* bias */); 450 assertNull(noDstMatch2); 451 452 TimeZone noDstMatch3 = finder.lookupTimeZoneByCountryAndOffset("xx", 453 LONDON_NO_DST_OFFSET_MILLIS, true /* isDst */, WHEN_DST, null /* bias */); 454 assertNull(noDstMatch3); 455 456 TimeZone noDstMatch4 = finder.lookupTimeZoneByCountryAndOffset("xx", 457 LONDON_NO_DST_OFFSET_MILLIS, true /* isDst */, WHEN_NO_DST, null /* bias */); 458 assertNull(noDstMatch4); 459 460 TimeZone noDstMatch5 = finder.lookupTimeZoneByCountryAndOffset("xx", 461 LONDON_DST_OFFSET_MILLIS, false /* isDst */, WHEN_DST, null /* bias */); 462 assertNull(noDstMatch5); 463 464 TimeZone noDstMatch6 = finder.lookupTimeZoneByCountryAndOffset("xx", 465 LONDON_NO_DST_OFFSET_MILLIS, false /* isDst */, WHEN_DST, null /* bias */); 466 assertNull(noDstMatch6); 467 468 // Some bias cases below. 469 470 // The bias is irrelevant here: it matches what would be returned anyway. 471 assertZoneEquals(LONDON_TZ, 472 finder.lookupTimeZoneByCountryAndOffset("xx", LONDON_DST_OFFSET_MILLIS, 473 true /* isDst */, WHEN_DST, LONDON_TZ /* bias */)); 474 assertZoneEquals(LONDON_TZ, 475 finder.lookupTimeZoneByCountryAndOffset("xx", LONDON_NO_DST_OFFSET_MILLIS, 476 false /* isDst */, WHEN_NO_DST, LONDON_TZ /* bias */)); 477 // A sample of a non-matching case with bias. 478 assertNull(finder.lookupTimeZoneByCountryAndOffset("xx", LONDON_DST_OFFSET_MILLIS, 479 true /* isDst */, WHEN_NO_DST, LONDON_TZ /* bias */)); 480 481 // The bias should be ignored: it doesn't match any of the country's zones. 482 assertZoneEquals(LONDON_TZ, 483 finder.lookupTimeZoneByCountryAndOffset("xx", LONDON_DST_OFFSET_MILLIS, 484 true /* isDst */, WHEN_DST, NEW_YORK_TZ /* bias */)); 485 486 // The bias should still be ignored even though it matches the offset information given: 487 // it doesn't match any of the country's configured zones. 488 assertNull(finder.lookupTimeZoneByCountryAndOffset("xx", NEW_YORK_DST_OFFSET_MILLIS, 489 true /* isDst */, WHEN_DST, NEW_YORK_TZ /* bias */)); 490 } 491 492 @Test 493 public void lookupTimeZoneByCountryAndOffset_multipleNonOverlappingCandidates() 494 throws Exception { 495 TimeZoneFinder finder = validate("<timezones>\n" 496 + " <countryzones>\n" 497 + " <country code=\"xx\">\n" 498 + " <id>America/New_York</id>\n" 499 + " <id>Europe/London</id>\n" 500 + " </country>\n" 501 + " </countryzones>\n" 502 + "</timezones>\n"); 503 504 // The three parameters match the configured zone: offset, isDst and when. 505 assertZoneEquals(LONDON_TZ, finder.lookupTimeZoneByCountryAndOffset("xx", 506 LONDON_DST_OFFSET_MILLIS, true /* isDst */, WHEN_DST, null /* bias */)); 507 assertZoneEquals(LONDON_TZ, finder.lookupTimeZoneByCountryAndOffset("xx", 508 LONDON_NO_DST_OFFSET_MILLIS, false /* isDst */, WHEN_NO_DST, null /* bias */)); 509 assertZoneEquals(NEW_YORK_TZ, finder.lookupTimeZoneByCountryAndOffset("xx", 510 NEW_YORK_DST_OFFSET_MILLIS, true /* isDst */, WHEN_DST, null /* bias */)); 511 assertZoneEquals(NEW_YORK_TZ, finder.lookupTimeZoneByCountryAndOffset("xx", 512 NEW_YORK_NO_DST_OFFSET_MILLIS, false /* isDst */, WHEN_NO_DST, null /* bias */)); 513 514 // Some lookup failure cases where the offset, isDst and when do not match the configured 515 // zone. This is a sample, not complete. 516 TimeZone noDstMatch1 = finder.lookupTimeZoneByCountryAndOffset("xx", 517 LONDON_DST_OFFSET_MILLIS, true /* isDst */, WHEN_NO_DST, null /* bias */); 518 assertNull(noDstMatch1); 519 520 TimeZone noDstMatch2 = finder.lookupTimeZoneByCountryAndOffset("xx", 521 LONDON_DST_OFFSET_MILLIS, false /* isDst */, WHEN_NO_DST, null /* bias */); 522 assertNull(noDstMatch2); 523 524 TimeZone noDstMatch3 = finder.lookupTimeZoneByCountryAndOffset("xx", 525 NEW_YORK_NO_DST_OFFSET_MILLIS, true /* isDst */, WHEN_DST, null /* bias */); 526 assertNull(noDstMatch3); 527 528 TimeZone noDstMatch4 = finder.lookupTimeZoneByCountryAndOffset("xx", 529 NEW_YORK_NO_DST_OFFSET_MILLIS, true /* isDst */, WHEN_NO_DST, null /* bias */); 530 assertNull(noDstMatch4); 531 532 TimeZone noDstMatch5 = finder.lookupTimeZoneByCountryAndOffset("xx", 533 LONDON_DST_OFFSET_MILLIS, false /* isDst */, WHEN_DST, null /* bias */); 534 assertNull(noDstMatch5); 535 536 TimeZone noDstMatch6 = finder.lookupTimeZoneByCountryAndOffset("xx", 537 LONDON_NO_DST_OFFSET_MILLIS, false /* isDst */, WHEN_DST, null /* bias */); 538 assertNull(noDstMatch6); 539 540 // Some bias cases below. 541 542 // The bias is irrelevant here: it matches what would be returned anyway. 543 assertZoneEquals(LONDON_TZ, 544 finder.lookupTimeZoneByCountryAndOffset("xx", LONDON_DST_OFFSET_MILLIS, 545 true /* isDst */, WHEN_DST, LONDON_TZ /* bias */)); 546 assertZoneEquals(LONDON_TZ, 547 finder.lookupTimeZoneByCountryAndOffset("xx", LONDON_NO_DST_OFFSET_MILLIS, 548 false /* isDst */, WHEN_NO_DST, LONDON_TZ /* bias */)); 549 // A sample of a non-matching case with bias. 550 assertNull(finder.lookupTimeZoneByCountryAndOffset("xx", LONDON_DST_OFFSET_MILLIS, 551 true /* isDst */, WHEN_NO_DST, LONDON_TZ /* bias */)); 552 553 // The bias should be ignored: it matches a configured zone, but the offset is wrong so 554 // should not be considered a match. 555 assertZoneEquals(LONDON_TZ, 556 finder.lookupTimeZoneByCountryAndOffset("xx", LONDON_DST_OFFSET_MILLIS, 557 true /* isDst */, WHEN_DST, NEW_YORK_TZ /* bias */)); 558 } 559 560 // This is an artificial case very similar to America/Denver and America/Phoenix in the US: both 561 // have the same offset for 6 months of the year but diverge. Australia/Lord_Howe too. 562 @Test 563 public void lookupTimeZoneByCountryAndOffset_multipleOverlappingCandidates() throws Exception { 564 // Three zones that have the same offset for some of the year. Europe/London changes 565 // offset WHEN_DST, the others do not. 566 TimeZoneFinder finder = validate("<timezones>\n" 567 + " <countryzones>\n" 568 + " <country code=\"xx\">\n" 569 + " <id>Atlantic/Reykjavik</id>\n" 570 + " <id>Europe/London</id>\n" 571 + " <id>Etc/UTC</id>\n" 572 + " </country>\n" 573 + " </countryzones>\n" 574 + "</timezones>\n"); 575 576 // This is the no-DST offset for LONDON_TZ, REYKJAVIK_TZ. UTC_TZ. 577 final int noDstOffset = LONDON_NO_DST_OFFSET_MILLIS; 578 // This is the DST offset for LONDON_TZ. 579 final int dstOffset = LONDON_DST_OFFSET_MILLIS; 580 581 // The three parameters match the configured zone: offset, isDst and when. 582 assertZoneEquals(LONDON_TZ, finder.lookupTimeZoneByCountryAndOffset("xx", dstOffset, 583 true /* isDst */, WHEN_DST, null /* bias */)); 584 assertZoneEquals(REYKJAVIK_TZ, finder.lookupTimeZoneByCountryAndOffset("xx", noDstOffset, 585 false /* isDst */, WHEN_NO_DST, null /* bias */)); 586 assertZoneEquals(LONDON_TZ, finder.lookupTimeZoneByCountryAndOffset("xx", dstOffset, 587 true /* isDst */, WHEN_DST, null /* bias */)); 588 assertZoneEquals(REYKJAVIK_TZ, finder.lookupTimeZoneByCountryAndOffset("xx", noDstOffset, 589 false /* isDst */, WHEN_NO_DST, null /* bias */)); 590 assertZoneEquals(REYKJAVIK_TZ, finder.lookupTimeZoneByCountryAndOffset("xx", noDstOffset, 591 false /* isDst */, WHEN_DST, null /* bias */)); 592 593 // Some lookup failure cases where the offset, isDst and when do not match the configured 594 // zones. 595 TimeZone noDstMatch1 = finder.lookupTimeZoneByCountryAndOffset("xx", dstOffset, 596 true /* isDst */, WHEN_NO_DST, null /* bias */); 597 assertNull(noDstMatch1); 598 599 TimeZone noDstMatch2 = finder.lookupTimeZoneByCountryAndOffset("xx", noDstOffset, 600 true /* isDst */, WHEN_DST, null /* bias */); 601 assertNull(noDstMatch2); 602 603 TimeZone noDstMatch3 = finder.lookupTimeZoneByCountryAndOffset("xx", noDstOffset, 604 true /* isDst */, WHEN_NO_DST, null /* bias */); 605 assertNull(noDstMatch3); 606 607 TimeZone noDstMatch4 = finder.lookupTimeZoneByCountryAndOffset("xx", dstOffset, 608 false /* isDst */, WHEN_DST, null /* bias */); 609 assertNull(noDstMatch4); 610 611 612 // Some bias cases below. 613 614 // The bias is relevant here: it overrides what would be returned naturally. 615 assertZoneEquals(REYKJAVIK_TZ, finder.lookupTimeZoneByCountryAndOffset("xx", noDstOffset, 616 false /* isDst */, WHEN_NO_DST, null /* bias */)); 617 assertZoneEquals(LONDON_TZ, finder.lookupTimeZoneByCountryAndOffset("xx", noDstOffset, 618 false /* isDst */, WHEN_NO_DST, LONDON_TZ /* bias */)); 619 assertZoneEquals(UTC_TZ, finder.lookupTimeZoneByCountryAndOffset("xx", noDstOffset, 620 false /* isDst */, WHEN_NO_DST, UTC_TZ /* bias */)); 621 622 // The bias should be ignored: it matches a configured zone, but the offset is wrong so 623 // should not be considered a match. 624 assertZoneEquals(LONDON_TZ, finder.lookupTimeZoneByCountryAndOffset("xx", 625 LONDON_DST_OFFSET_MILLIS, true /* isDst */, WHEN_DST, REYKJAVIK_TZ /* bias */)); 626 } 627 628 @Test 629 public void consistencyTest() throws Exception { 630 // Confirm that no new zones have been added to zones.tab without also adding them to the 631 // configuration used to drive TimeZoneFinder. 632 633 // zone.tab is a tab separated ASCII file provided by IANA and included in Android's tzdata 634 // file. Each line contains a mapping from country code -> zone ID. The ordering used by 635 // TimeZoneFinder is Android-specific, but we can use zone.tab to make sure we know about 636 // all country zones. Any update to tzdata that adds, renames, or removes zones should be 637 // reflected in the file used by TimeZoneFinder. 638 Map<String, Set<String>> zoneTabMappings = new HashMap<>(); 639 for (String line : ZoneInfoDB.getInstance().getZoneTab().split("\n")) { 640 int countryCodeEnd = line.indexOf('\t', 1); 641 int olsonIdStart = line.indexOf('\t', 4) + 1; 642 int olsonIdEnd = line.indexOf('\t', olsonIdStart); 643 if (olsonIdEnd == -1) { 644 olsonIdEnd = line.length(); // Not all zone.tab lines have a comment. 645 } 646 String countryCode = line.substring(0, countryCodeEnd); 647 String olsonId = line.substring(olsonIdStart, olsonIdEnd); 648 Set<String> zoneIds = zoneTabMappings.get(countryCode); 649 if (zoneIds == null) { 650 zoneIds = new HashSet<>(); 651 zoneTabMappings.put(countryCode, zoneIds); 652 } 653 zoneIds.add(olsonId); 654 } 655 656 TimeZoneFinder timeZoneFinder = TimeZoneFinder.getInstance(); 657 for (Map.Entry<String, Set<String>> countryEntry : zoneTabMappings.entrySet()) { 658 String countryCode = countryEntry.getKey(); 659 // Android uses lower case, IANA uses upper. 660 countryCode = countryCode.toLowerCase(); 661 662 List<String> ianaZoneIds = countryEntry.getValue().stream().sorted() 663 .collect(Collectors.toList()); 664 List<TimeZone> androidZones = timeZoneFinder.lookupTimeZonesByCountry(countryCode); 665 List<String> androidZoneIds = 666 androidZones.stream().map(TimeZone::getID).sorted() 667 .collect(Collectors.toList()); 668 669 assertEquals("Android zones for " + countryCode + " do not match IANA data", 670 ianaZoneIds, androidZoneIds); 671 } 672 } 673 674 private void assertImmutableTimeZone(TimeZone timeZone) { 675 try { 676 timeZone.setRawOffset(1000); 677 fail(); 678 } catch (UnsupportedOperationException expected) { 679 } 680 } 681 682 private static void assertImmutableList(List<TimeZone> timeZones) { 683 try { 684 timeZones.add(null); 685 fail(); 686 } catch (UnsupportedOperationException expected) { 687 } 688 } 689 690 private static void assertZoneEquals(TimeZone expected, TimeZone actual) { 691 // TimeZone.equals() only checks the ID, but that's ok for these tests. 692 assertEquals(expected, actual); 693 } 694 695 private static void assertZonesEqual(List<TimeZone> expected, List<TimeZone> actual) { 696 // TimeZone.equals() only checks the ID, but that's ok for these tests. 697 assertEquals(expected, actual); 698 } 699 700 private static void checkValidateThrowsParserException(String xml) throws Exception { 701 try { 702 validate(xml); 703 fail(); 704 } catch (IOException expected) { 705 } 706 } 707 708 private static TimeZoneFinder validate(String xml) throws IOException { 709 TimeZoneFinder timeZoneFinder = TimeZoneFinder.createInstanceForTests(xml); 710 timeZoneFinder.validate(); 711 return timeZoneFinder; 712 } 713 714 private static List<TimeZone> zones(String... ids) { 715 return Arrays.stream(ids).map(TimeZone::getTimeZone).collect(Collectors.toList()); 716 } 717 718 private String createFile(String fileContent) throws IOException { 719 Path filePath = Files.createTempFile(testDir, null, null); 720 Files.write(filePath, fileContent.getBytes(StandardCharsets.UTF_8)); 721 return filePath.toString(); 722 } 723 724 private String createMissingFile() throws IOException { 725 Path filePath = Files.createTempFile(testDir, null, null); 726 Files.delete(filePath); 727 return filePath.toString(); 728 } 729} 730