/* * 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 libcore.util; import org.junit.After; import org.junit.Before; import org.junit.Test; import android.icu.util.TimeZone; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import static org.junit.Assert.fail; public class TimeZoneFinderTest { private static final int HOUR_MILLIS = 60 * 60 * 1000; // Zones used in the tests. NEW_YORK_TZ and LONDON_TZ chosen because they never overlap but both // have DST. private static final TimeZone NEW_YORK_TZ = TimeZone.getTimeZone("America/New_York"); private static final TimeZone LONDON_TZ = TimeZone.getTimeZone("Europe/London"); // A zone that matches LONDON_TZ for WHEN_NO_DST. It does not have DST so differs for WHEN_DST. private static final TimeZone REYKJAVIK_TZ = TimeZone.getTimeZone("Atlantic/Reykjavik"); // Another zone that matches LONDON_TZ for WHEN_NO_DST. It does not have DST so differs for // WHEN_DST. private static final TimeZone UTC_TZ = TimeZone.getTimeZone("Etc/UTC"); // 22nd July 2017, 13:14:15 UTC (DST time in all the timezones used in these tests that observe // DST). private static final long WHEN_DST = 1500729255000L; // 22nd January 2018, 13:14:15 UTC (non-DST time in all timezones used in these tests). private static final long WHEN_NO_DST = 1516626855000L; private static final int LONDON_DST_OFFSET_MILLIS = HOUR_MILLIS; private static final int LONDON_NO_DST_OFFSET_MILLIS = 0; private static final int NEW_YORK_DST_OFFSET_MILLIS = -4 * HOUR_MILLIS; private static final int NEW_YORK_NO_DST_OFFSET_MILLIS = -5 * HOUR_MILLIS; private Path testDir; @Before public void setUp() throws Exception { testDir = Files.createTempDirectory("TimeZoneFinderTest"); } @After public void tearDown() throws Exception { // Delete the testDir and all contents. Files.walkFileTree(testDir, new SimpleFileVisitor() { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { Files.delete(file); return FileVisitResult.CONTINUE; } @Override public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { Files.delete(dir); return FileVisitResult.CONTINUE; } }); } @Test public void createInstanceWithFallback() throws Exception { String validXml1 = "\n" + " \n" + " \n" + " Europe/London\n" + " \n" + " \n" + "\n"; String validXml2 = "\n" + " \n" + " \n" + " Europe/Paris\n" + " \n" + " \n" + "\n"; String invalidXml = "\n"; checkValidateThrowsParserException(invalidXml); String validFile1 = createFile(validXml1); String validFile2 = createFile(validXml2); String invalidFile = createFile(invalidXml); String missingFile = createMissingFile(); TimeZoneFinder file1ThenFile2 = TimeZoneFinder.createInstanceWithFallback(validFile1, validFile2); assertZonesEqual(zones("Europe/London"), file1ThenFile2.lookupTimeZonesByCountry("gb")); TimeZoneFinder missingFileThenFile1 = TimeZoneFinder.createInstanceWithFallback(missingFile, validFile1); assertZonesEqual(zones("Europe/London"), missingFileThenFile1.lookupTimeZonesByCountry("gb")); TimeZoneFinder file2ThenFile1 = TimeZoneFinder.createInstanceWithFallback(validFile2, validFile1); assertZonesEqual(zones("Europe/Paris"), file2ThenFile1.lookupTimeZonesByCountry("gb")); // We assume the file has been validated so an invalid file is not checked ahead of time. // We will find out when we look something up. TimeZoneFinder invalidThenValid = TimeZoneFinder.createInstanceWithFallback(invalidFile, validFile1); assertNull(invalidThenValid.lookupTimeZonesByCountry("gb")); // This is not a normal case: It would imply a define shipped without a file in /system! TimeZoneFinder missingFiles = TimeZoneFinder.createInstanceWithFallback(missingFile, missingFile); assertNull(missingFiles.lookupTimeZonesByCountry("gb")); } @Test public void xmlParsing_emptyFile() throws Exception { checkValidateThrowsParserException(""); } @Test public void xmlParsing_unexpectedRootElement() throws Exception { checkValidateThrowsParserException("\n"); } @Test public void xmlParsing_missingCountryZones() throws Exception { checkValidateThrowsParserException("\n"); } @Test public void xmlParsing_noCountriesOk() throws Exception { validate("\n" + " \n" + " \n" + "\n"); } @Test public void xmlParsing_unexpectedComments() throws Exception { TimeZoneFinder finder = validate("\n" + " \n" + " \n" + " " + " Europe/London\n" + " \n" + " \n" + "\n"); assertZonesEqual(zones("Europe/London"), finder.lookupTimeZonesByCountry("gb")); // This is a crazy comment, but also helps prove that TEXT nodes are coalesced by the // parser. finder = validate("\n" + " \n" + " \n" + " Europe/London\n" + " \n" + " \n" + "\n"); assertZonesEqual(zones("Europe/London"), finder.lookupTimeZonesByCountry("gb")); } @Test public void xmlParsing_unexpectedElementsIgnored() throws Exception { String unexpectedElement = "\n\n"; TimeZoneFinder finder = validate("\n" + " " + unexpectedElement + " \n" + " \n" + " Europe/London\n" + " \n" + " \n" + "\n"); assertZonesEqual(zones("Europe/London"), finder.lookupTimeZonesByCountry("gb")); finder = validate("\n" + " \n" + " " + unexpectedElement + " \n" + " Europe/London\n" + " \n" + " \n" + "\n"); assertZonesEqual(zones("Europe/London"), finder.lookupTimeZonesByCountry("gb")); finder = validate("\n" + " \n" + " \n" + " " + unexpectedElement + " Europe/London\n" + " \n" + " \n" + "\n"); assertZonesEqual(zones("Europe/London"), finder.lookupTimeZonesByCountry("gb")); finder = validate("\n" + " \n" + " \n" + " Europe/London\n" + " " + unexpectedElement + " Europe/Paris\n" + " \n" + " \n" + "\n"); assertZonesEqual(zones("Europe/London", "Europe/Paris"), finder.lookupTimeZonesByCountry("gb")); finder = validate("\n" + " \n" + " \n" + " Europe/London\n" + " \n" + " " + unexpectedElement + " \n" + "\n"); assertZonesEqual(zones("Europe/London"), finder.lookupTimeZonesByCountry("gb")); // This test is important because it ensures we can extend the format in future with // more information. finder = validate("\n" + " \n" + " \n" + " Europe/London\n" + " \n" + " \n" + " " + unexpectedElement + "\n"); assertZonesEqual(zones("Europe/London"), finder.lookupTimeZonesByCountry("gb")); } @Test public void xmlParsing_unexpectedTextIgnored() throws Exception { String unexpectedText = "unexpected-text"; TimeZoneFinder finder = validate("\n" + " " + unexpectedText + " \n" + " \n" + " Europe/London\n" + " \n" + " \n" + "\n"); assertZonesEqual(zones("Europe/London"), finder.lookupTimeZonesByCountry("gb")); finder = validate("\n" + " \n" + " " + unexpectedText + " \n" + " Europe/London\n" + " \n" + " \n" + "\n"); assertZonesEqual(zones("Europe/London"), finder.lookupTimeZonesByCountry("gb")); finder = validate("\n" + " \n" + " \n" + " " + unexpectedText + " Europe/London\n" + " \n" + " \n" + "\n"); assertZonesEqual(zones("Europe/London"), finder.lookupTimeZonesByCountry("gb")); finder = validate("\n" + " \n" + " \n" + " Europe/London\n" + " " + unexpectedText + " Europe/Paris\n" + " \n" + " \n" + "\n"); assertZonesEqual(zones("Europe/London", "Europe/Paris"), finder.lookupTimeZonesByCountry("gb")); } @Test public void xmlParsing_truncatedInput() throws Exception { checkValidateThrowsParserException("\n"); checkValidateThrowsParserException("\n" + " \n"); checkValidateThrowsParserException("\n" + " \n" + " \n"); checkValidateThrowsParserException("\n" + " \n" + " \n" + " Europe/London\n"); checkValidateThrowsParserException("\n" + " \n" + " \n" + " Europe/London\n" + " \n"); checkValidateThrowsParserException("\n" + " \n" + " \n" + " Europe/London\n" + " \n" + " \n"); } @Test public void xmlParsing_unexpectedChildInTimeZoneIdThrows() throws Exception { checkValidateThrowsParserException("\n" + " \n" + " \n" + " \n" + " \n" + " \n" + "\n"); } @Test public void xmlParsing_unknownTimeZoneIdIgnored() throws Exception { TimeZoneFinder finder = validate("\n" + " \n" + " \n" + " Unknown_Id\n" + " Europe/London\n" + " \n" + " \n" + "\n"); assertZonesEqual(zones("Europe/London"), finder.lookupTimeZonesByCountry("gb")); } @Test public void xmlParsing_missingCountryCode() throws Exception { checkValidateThrowsParserException("\n" + " \n" + " \n" + " Europe/London\n" + " \n" + " \n" + "\n"); } @Test public void xmlParsing_unknownCountryReturnsNull() throws Exception { TimeZoneFinder finder = validate("\n" + " \n" + " \n" + "\n"); assertNull(finder.lookupTimeZonesByCountry("gb")); } @Test public void lookupTimeZonesByCountry_structuresAreImmutable() throws Exception { TimeZoneFinder finder = validate("\n" + " \n" + " \n" + " Europe/London\n" + " \n" + " \n" + "\n"); List gbList = finder.lookupTimeZonesByCountry("gb"); assertEquals(1, gbList.size()); assertImmutableList(gbList); assertImmutableTimeZone(gbList.get(0)); assertNull(finder.lookupTimeZonesByCountry("unknown")); } @Test public void lookupTimeZoneByCountryAndOffset_unknownCountry() throws Exception { TimeZoneFinder finder = validate("\n" + " \n" + " \n" + " Europe/London\n" + " \n" + " \n" + "\n"); // Demonstrate the arguments work for a known country. assertZoneEquals(LONDON_TZ, finder.lookupTimeZoneByCountryAndOffset("xx", LONDON_DST_OFFSET_MILLIS, true /* isDst */, WHEN_DST, null /* bias */)); // Test with an unknown country. String unknownCountryCode = "yy"; assertNull(finder.lookupTimeZoneByCountryAndOffset(unknownCountryCode, LONDON_DST_OFFSET_MILLIS, true /* isDst */, WHEN_DST, null /* bias */)); assertNull(finder.lookupTimeZoneByCountryAndOffset(unknownCountryCode, LONDON_DST_OFFSET_MILLIS, true /* isDst */, WHEN_DST, LONDON_TZ /* bias */)); } @Test public void lookupTimeZoneByCountryAndOffset_oneCandidate() throws Exception { TimeZoneFinder finder = validate("\n" + " \n" + " \n" + " Europe/London\n" + " \n" + " \n" + "\n"); // The three parameters match the configured zone: offset, isDst and when. assertZoneEquals(LONDON_TZ, finder.lookupTimeZoneByCountryAndOffset("xx", LONDON_DST_OFFSET_MILLIS, true /* isDst */, WHEN_DST, null /* bias */)); assertZoneEquals(LONDON_TZ, finder.lookupTimeZoneByCountryAndOffset("xx", LONDON_NO_DST_OFFSET_MILLIS, false /* isDst */, WHEN_NO_DST, null /* bias */)); // Some lookup failure cases where the offset, isDst and when do not match the configured // zone. TimeZone noDstMatch1 = finder.lookupTimeZoneByCountryAndOffset("xx", LONDON_DST_OFFSET_MILLIS, true /* isDst */, WHEN_NO_DST, null /* bias */); assertNull(noDstMatch1); TimeZone noDstMatch2 = finder.lookupTimeZoneByCountryAndOffset("xx", LONDON_DST_OFFSET_MILLIS, false /* isDst */, WHEN_NO_DST, null /* bias */); assertNull(noDstMatch2); TimeZone noDstMatch3 = finder.lookupTimeZoneByCountryAndOffset("xx", LONDON_NO_DST_OFFSET_MILLIS, true /* isDst */, WHEN_DST, null /* bias */); assertNull(noDstMatch3); TimeZone noDstMatch4 = finder.lookupTimeZoneByCountryAndOffset("xx", LONDON_NO_DST_OFFSET_MILLIS, true /* isDst */, WHEN_NO_DST, null /* bias */); assertNull(noDstMatch4); TimeZone noDstMatch5 = finder.lookupTimeZoneByCountryAndOffset("xx", LONDON_DST_OFFSET_MILLIS, false /* isDst */, WHEN_DST, null /* bias */); assertNull(noDstMatch5); TimeZone noDstMatch6 = finder.lookupTimeZoneByCountryAndOffset("xx", LONDON_NO_DST_OFFSET_MILLIS, false /* isDst */, WHEN_DST, null /* bias */); assertNull(noDstMatch6); // Some bias cases below. // The bias is irrelevant here: it matches what would be returned anyway. assertZoneEquals(LONDON_TZ, finder.lookupTimeZoneByCountryAndOffset("xx", LONDON_DST_OFFSET_MILLIS, true /* isDst */, WHEN_DST, LONDON_TZ /* bias */)); assertZoneEquals(LONDON_TZ, finder.lookupTimeZoneByCountryAndOffset("xx", LONDON_NO_DST_OFFSET_MILLIS, false /* isDst */, WHEN_NO_DST, LONDON_TZ /* bias */)); // A sample of a non-matching case with bias. assertNull(finder.lookupTimeZoneByCountryAndOffset("xx", LONDON_DST_OFFSET_MILLIS, true /* isDst */, WHEN_NO_DST, LONDON_TZ /* bias */)); // The bias should be ignored: it doesn't match any of the country's zones. assertZoneEquals(LONDON_TZ, finder.lookupTimeZoneByCountryAndOffset("xx", LONDON_DST_OFFSET_MILLIS, true /* isDst */, WHEN_DST, NEW_YORK_TZ /* bias */)); // The bias should still be ignored even though it matches the offset information given: // it doesn't match any of the country's configured zones. assertNull(finder.lookupTimeZoneByCountryAndOffset("xx", NEW_YORK_DST_OFFSET_MILLIS, true /* isDst */, WHEN_DST, NEW_YORK_TZ /* bias */)); } @Test public void lookupTimeZoneByCountryAndOffset_multipleNonOverlappingCandidates() throws Exception { TimeZoneFinder finder = validate("\n" + " \n" + " \n" + " America/New_York\n" + " Europe/London\n" + " \n" + " \n" + "\n"); // The three parameters match the configured zone: offset, isDst and when. assertZoneEquals(LONDON_TZ, finder.lookupTimeZoneByCountryAndOffset("xx", LONDON_DST_OFFSET_MILLIS, true /* isDst */, WHEN_DST, null /* bias */)); assertZoneEquals(LONDON_TZ, finder.lookupTimeZoneByCountryAndOffset("xx", LONDON_NO_DST_OFFSET_MILLIS, false /* isDst */, WHEN_NO_DST, null /* bias */)); assertZoneEquals(NEW_YORK_TZ, finder.lookupTimeZoneByCountryAndOffset("xx", NEW_YORK_DST_OFFSET_MILLIS, true /* isDst */, WHEN_DST, null /* bias */)); assertZoneEquals(NEW_YORK_TZ, finder.lookupTimeZoneByCountryAndOffset("xx", NEW_YORK_NO_DST_OFFSET_MILLIS, false /* isDst */, WHEN_NO_DST, null /* bias */)); // Some lookup failure cases where the offset, isDst and when do not match the configured // zone. This is a sample, not complete. TimeZone noDstMatch1 = finder.lookupTimeZoneByCountryAndOffset("xx", LONDON_DST_OFFSET_MILLIS, true /* isDst */, WHEN_NO_DST, null /* bias */); assertNull(noDstMatch1); TimeZone noDstMatch2 = finder.lookupTimeZoneByCountryAndOffset("xx", LONDON_DST_OFFSET_MILLIS, false /* isDst */, WHEN_NO_DST, null /* bias */); assertNull(noDstMatch2); TimeZone noDstMatch3 = finder.lookupTimeZoneByCountryAndOffset("xx", NEW_YORK_NO_DST_OFFSET_MILLIS, true /* isDst */, WHEN_DST, null /* bias */); assertNull(noDstMatch3); TimeZone noDstMatch4 = finder.lookupTimeZoneByCountryAndOffset("xx", NEW_YORK_NO_DST_OFFSET_MILLIS, true /* isDst */, WHEN_NO_DST, null /* bias */); assertNull(noDstMatch4); TimeZone noDstMatch5 = finder.lookupTimeZoneByCountryAndOffset("xx", LONDON_DST_OFFSET_MILLIS, false /* isDst */, WHEN_DST, null /* bias */); assertNull(noDstMatch5); TimeZone noDstMatch6 = finder.lookupTimeZoneByCountryAndOffset("xx", LONDON_NO_DST_OFFSET_MILLIS, false /* isDst */, WHEN_DST, null /* bias */); assertNull(noDstMatch6); // Some bias cases below. // The bias is irrelevant here: it matches what would be returned anyway. assertZoneEquals(LONDON_TZ, finder.lookupTimeZoneByCountryAndOffset("xx", LONDON_DST_OFFSET_MILLIS, true /* isDst */, WHEN_DST, LONDON_TZ /* bias */)); assertZoneEquals(LONDON_TZ, finder.lookupTimeZoneByCountryAndOffset("xx", LONDON_NO_DST_OFFSET_MILLIS, false /* isDst */, WHEN_NO_DST, LONDON_TZ /* bias */)); // A sample of a non-matching case with bias. assertNull(finder.lookupTimeZoneByCountryAndOffset("xx", LONDON_DST_OFFSET_MILLIS, true /* isDst */, WHEN_NO_DST, LONDON_TZ /* bias */)); // The bias should be ignored: it matches a configured zone, but the offset is wrong so // should not be considered a match. assertZoneEquals(LONDON_TZ, finder.lookupTimeZoneByCountryAndOffset("xx", LONDON_DST_OFFSET_MILLIS, true /* isDst */, WHEN_DST, NEW_YORK_TZ /* bias */)); } // This is an artificial case very similar to America/Denver and America/Phoenix in the US: both // have the same offset for 6 months of the year but diverge. Australia/Lord_Howe too. @Test public void lookupTimeZoneByCountryAndOffset_multipleOverlappingCandidates() throws Exception { // Three zones that have the same offset for some of the year. Europe/London changes // offset WHEN_DST, the others do not. TimeZoneFinder finder = validate("\n" + " \n" + " \n" + " Atlantic/Reykjavik\n" + " Europe/London\n" + " Etc/UTC\n" + " \n" + " \n" + "\n"); // This is the no-DST offset for LONDON_TZ, REYKJAVIK_TZ. UTC_TZ. final int noDstOffset = LONDON_NO_DST_OFFSET_MILLIS; // This is the DST offset for LONDON_TZ. final int dstOffset = LONDON_DST_OFFSET_MILLIS; // The three parameters match the configured zone: offset, isDst and when. assertZoneEquals(LONDON_TZ, finder.lookupTimeZoneByCountryAndOffset("xx", dstOffset, true /* isDst */, WHEN_DST, null /* bias */)); assertZoneEquals(REYKJAVIK_TZ, finder.lookupTimeZoneByCountryAndOffset("xx", noDstOffset, false /* isDst */, WHEN_NO_DST, null /* bias */)); assertZoneEquals(LONDON_TZ, finder.lookupTimeZoneByCountryAndOffset("xx", dstOffset, true /* isDst */, WHEN_DST, null /* bias */)); assertZoneEquals(REYKJAVIK_TZ, finder.lookupTimeZoneByCountryAndOffset("xx", noDstOffset, false /* isDst */, WHEN_NO_DST, null /* bias */)); assertZoneEquals(REYKJAVIK_TZ, finder.lookupTimeZoneByCountryAndOffset("xx", noDstOffset, false /* isDst */, WHEN_DST, null /* bias */)); // Some lookup failure cases where the offset, isDst and when do not match the configured // zones. TimeZone noDstMatch1 = finder.lookupTimeZoneByCountryAndOffset("xx", dstOffset, true /* isDst */, WHEN_NO_DST, null /* bias */); assertNull(noDstMatch1); TimeZone noDstMatch2 = finder.lookupTimeZoneByCountryAndOffset("xx", noDstOffset, true /* isDst */, WHEN_DST, null /* bias */); assertNull(noDstMatch2); TimeZone noDstMatch3 = finder.lookupTimeZoneByCountryAndOffset("xx", noDstOffset, true /* isDst */, WHEN_NO_DST, null /* bias */); assertNull(noDstMatch3); TimeZone noDstMatch4 = finder.lookupTimeZoneByCountryAndOffset("xx", dstOffset, false /* isDst */, WHEN_DST, null /* bias */); assertNull(noDstMatch4); // Some bias cases below. // The bias is relevant here: it overrides what would be returned naturally. assertZoneEquals(REYKJAVIK_TZ, finder.lookupTimeZoneByCountryAndOffset("xx", noDstOffset, false /* isDst */, WHEN_NO_DST, null /* bias */)); assertZoneEquals(LONDON_TZ, finder.lookupTimeZoneByCountryAndOffset("xx", noDstOffset, false /* isDst */, WHEN_NO_DST, LONDON_TZ /* bias */)); assertZoneEquals(UTC_TZ, finder.lookupTimeZoneByCountryAndOffset("xx", noDstOffset, false /* isDst */, WHEN_NO_DST, UTC_TZ /* bias */)); // The bias should be ignored: it matches a configured zone, but the offset is wrong so // should not be considered a match. assertZoneEquals(LONDON_TZ, finder.lookupTimeZoneByCountryAndOffset("xx", LONDON_DST_OFFSET_MILLIS, true /* isDst */, WHEN_DST, REYKJAVIK_TZ /* bias */)); } @Test public void consistencyTest() throws Exception { // Confirm that no new zones have been added to zones.tab without also adding them to the // configuration used to drive TimeZoneFinder. // zone.tab is a tab separated ASCII file provided by IANA and included in Android's tzdata // file. Each line contains a mapping from country code -> zone ID. The ordering used by // TimeZoneFinder is Android-specific, but we can use zone.tab to make sure we know about // all country zones. Any update to tzdata that adds, renames, or removes zones should be // reflected in the file used by TimeZoneFinder. Map> zoneTabMappings = new HashMap<>(); for (String line : ZoneInfoDB.getInstance().getZoneTab().split("\n")) { int countryCodeEnd = line.indexOf('\t', 1); int olsonIdStart = line.indexOf('\t', 4) + 1; int olsonIdEnd = line.indexOf('\t', olsonIdStart); if (olsonIdEnd == -1) { olsonIdEnd = line.length(); // Not all zone.tab lines have a comment. } String countryCode = line.substring(0, countryCodeEnd); String olsonId = line.substring(olsonIdStart, olsonIdEnd); Set zoneIds = zoneTabMappings.get(countryCode); if (zoneIds == null) { zoneIds = new HashSet<>(); zoneTabMappings.put(countryCode, zoneIds); } zoneIds.add(olsonId); } TimeZoneFinder timeZoneFinder = TimeZoneFinder.getInstance(); for (Map.Entry> countryEntry : zoneTabMappings.entrySet()) { String countryCode = countryEntry.getKey(); // Android uses lower case, IANA uses upper. countryCode = countryCode.toLowerCase(); List ianaZoneIds = countryEntry.getValue().stream().sorted() .collect(Collectors.toList()); List androidZones = timeZoneFinder.lookupTimeZonesByCountry(countryCode); List androidZoneIds = androidZones.stream().map(TimeZone::getID).sorted() .collect(Collectors.toList()); assertEquals("Android zones for " + countryCode + " do not match IANA data", ianaZoneIds, androidZoneIds); } } private void assertImmutableTimeZone(TimeZone timeZone) { try { timeZone.setRawOffset(1000); fail(); } catch (UnsupportedOperationException expected) { } } private static void assertImmutableList(List timeZones) { try { timeZones.add(null); fail(); } catch (UnsupportedOperationException expected) { } } private static void assertZoneEquals(TimeZone expected, TimeZone actual) { // TimeZone.equals() only checks the ID, but that's ok for these tests. assertEquals(expected, actual); } private static void assertZonesEqual(List expected, List actual) { // TimeZone.equals() only checks the ID, but that's ok for these tests. assertEquals(expected, actual); } private static void checkValidateThrowsParserException(String xml) throws Exception { try { validate(xml); fail(); } catch (IOException expected) { } } private static TimeZoneFinder validate(String xml) throws IOException { TimeZoneFinder timeZoneFinder = TimeZoneFinder.createInstanceForTests(xml); timeZoneFinder.validate(); return timeZoneFinder; } private static List zones(String... ids) { return Arrays.stream(ids).map(TimeZone::getTimeZone).collect(Collectors.toList()); } private String createFile(String fileContent) throws IOException { Path filePath = Files.createTempFile(testDir, null, null); Files.write(filePath, fileContent.getBytes(StandardCharsets.UTF_8)); return filePath.toString(); } private String createMissingFile() throws IOException { Path filePath = Files.createTempFile(testDir, null, null); Files.delete(filePath); return filePath.toString(); } }