• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
17 package com.android.libcore.timezone.tzlookup;
18 
19 import static com.android.libcore.timezone.countryzones.proto.CountryZonesFile.Country;
20 import static com.android.libcore.timezone.testing.TestUtils.assertAbsent;
21 import static com.android.libcore.timezone.testing.TestUtils.assertContains;
22 import static com.android.libcore.timezone.testing.TestUtils.createFile;
23 import static org.junit.Assert.assertEquals;
24 import static org.junit.Assert.assertFalse;
25 import static org.junit.Assert.assertTrue;
26 
27 import com.android.libcore.timezone.countryzones.proto.CountryZonesFile;
28 import com.android.libcore.timezone.testing.TestUtils;
29 import com.android.timezone.tzids.proto.TzIdsProto;
30 import com.google.protobuf.TextFormat;
31 import com.ibm.icu.util.TimeZone;
32 
33 import org.junit.After;
34 import org.junit.Before;
35 import org.junit.Test;
36 
37 import java.io.IOException;
38 import java.nio.charset.StandardCharsets;
39 import java.nio.file.Files;
40 import java.nio.file.Path;
41 import java.nio.file.Paths;
42 import java.util.ArrayList;
43 import java.util.Arrays;
44 import java.util.HashMap;
45 import java.util.List;
46 import java.util.Map;
47 import java.util.stream.Collectors;
48 
49 public class TzLookupGeneratorTest {
50 
51     private static final String INVALID_TIME_ZONE_ID = "NOT_A_VALID_ID";
52     private static final String TZDB_VERSION = TimeZone.getTZDataVersion();
53 
54     private Path tempDir;
55 
56     @Before
setUp()57     public void setUp() throws Exception {
58         tempDir = Files.createTempDirectory("TzLookupGeneratorTest");
59     }
60 
61     @After
tearDown()62     public void tearDown() throws Exception {
63         TestUtils.deleteDir(tempDir);
64     }
65 
66     @Test
invalidCountryZonesFile()67     public void invalidCountryZonesFile() throws Exception {
68         String countryZonesFile = createFile(tempDir, "THIS IS NOT A VALID FILE");
69         List<ZoneTabFile.CountryEntry> gbZoneTabEntries = createValidZoneTabEntriesGb();
70         String zoneTabFile = createZoneTabFile(gbZoneTabEntries);
71         String tzLookupFile = createTempFileName("tzlookup");
72         String tzIdsFile = createTempFileName("tzids");
73 
74         TzLookupGenerator tzLookupGenerator = new TzLookupGenerator(
75                 countryZonesFile, zoneTabFile, tzLookupFile, tzIdsFile);
76         assertFalse(tzLookupGenerator.execute(false /* validateAllIanaIdsAreMapped */));
77     }
78 
79     @Test
invalidRulesVersion()80     public void invalidRulesVersion() throws Exception {
81         CountryZonesFile.Country validGb = createValidCountryGb();
82 
83         // The IANA version won't match ICU's IANA version so we should see a failure.
84         CountryZonesFile.CountryZones badIanaVersionCountryZones =
85                 createValidCountryZones(validGb).toBuilder().setIanaVersion("2001a").build();
86         String countryZonesFile = createCountryZonesFile(badIanaVersionCountryZones);
87 
88         List<ZoneTabFile.CountryEntry> gbZoneTabEntries = createValidZoneTabEntriesGb();
89         String zoneTabFile = createZoneTabFile(gbZoneTabEntries);
90 
91         String tzLookupFile = createTempFileName("tzlookup");
92         String tzIdsFile = createTempFileName("tzids");
93 
94         TzLookupGenerator tzLookupGenerator = new TzLookupGenerator(
95                 countryZonesFile, zoneTabFile, tzLookupFile, tzIdsFile);
96         assertFalse(tzLookupGenerator.execute(false /* validateAllIanaIdsAreMapped */));
97 
98         assertFileMissing(tzLookupFile);
99         assertFileMissing(tzIdsFile);
100     }
101 
102     @Test
countryWithNoTimeZoneMappings()103     public void countryWithNoTimeZoneMappings() throws Exception {
104         // No zones found!
105         CountryZonesFile.Country gbWithoutZones =
106                 createValidCountryGb().toBuilder().clearTimeZoneMappings().build();
107         CountryZonesFile.CountryZones countryZones = createValidCountryZones(gbWithoutZones);
108         String countryZonesFile = createCountryZonesFile(countryZones);
109 
110         String zoneTabFile = createZoneTabFile(createValidZoneTabEntriesGb());
111 
112         String tzLookupFile = createTempFileName("tzlookup");
113         String tzIdsFile = createTempFileName("tzids");
114 
115         TzLookupGenerator tzLookupGenerator = new TzLookupGenerator(
116                 countryZonesFile, zoneTabFile, tzLookupFile, tzIdsFile);
117         assertFalse(tzLookupGenerator.execute(false /* validateAllIanaIdsAreMapped */));
118 
119         assertFileMissing(tzLookupFile);
120         assertFileMissing(tzIdsFile);
121     }
122 
123     @Test
countryWithDuplicateTimeZoneMappings()124     public void countryWithDuplicateTimeZoneMappings() throws Exception {
125         // Duplicate zones found!
126         CountryZonesFile.Country validCountryGb = createValidCountryGb();
127         CountryZonesFile.Country gbWithDuplicateZones =
128                 validCountryGb.toBuilder()
129                         .setDefaultTimeZoneId(validCountryGb.getTimeZoneMappings(0).getId())
130                         .addAllTimeZoneMappings(validCountryGb.getTimeZoneMappingsList())
131                         .build();
132         CountryZonesFile.CountryZones countryZones =
133                 createValidCountryZones(gbWithDuplicateZones);
134         String countryZonesFile = createCountryZonesFile(countryZones);
135 
136         String zoneTabFile = createZoneTabFile(createValidZoneTabEntriesGb());
137 
138         String tzLookupFile = createTempFileName("tzlookup");
139         String tzIdsFile = createTempFileName("tzids");
140 
141         TzLookupGenerator tzLookupGenerator = new TzLookupGenerator(
142                 countryZonesFile, zoneTabFile, tzLookupFile, tzIdsFile);
143         assertFalse(tzLookupGenerator.execute(false /* validateAllIanaIdsAreMapped */));
144 
145         assertFileMissing(tzLookupFile);
146         assertFileMissing(tzIdsFile);
147     }
148 
149     @Test
badDefaultId()150     public void badDefaultId() throws Exception {
151         // Set an invalid default.
152         CountryZonesFile.Country validGb =
153                 createValidCountryGb().toBuilder()
154                         .setDefaultTimeZoneId("NOT_A_TIMEZONE_ID")
155                         .build();
156         CountryZonesFile.CountryZones gbCountryZones = createValidCountryZones(validGb);
157         String countryZonesFile = createCountryZonesFile(gbCountryZones);
158 
159         List<ZoneTabFile.CountryEntry> gbZoneTabEntries = createValidZoneTabEntriesGb();
160         String zoneTabFile = createZoneTabFile(gbZoneTabEntries);
161 
162         String tzLookupFile = createTempFileName("tzlookup");
163         String tzIdsFile = createTempFileName("tzids");
164 
165         TzLookupGenerator tzLookupGenerator = new TzLookupGenerator(
166                 countryZonesFile, zoneTabFile, tzLookupFile, tzIdsFile);
167         assertFalse(tzLookupGenerator.execute(false /* validateAllIanaIdsAreMapped */));
168 
169         assertFileMissing(tzLookupFile);
170         assertFileMissing(tzIdsFile);
171     }
172 
173     @Test
shouldFail_whenExplicitDefaultIdBelongsToOtherCountry()174     public void shouldFail_whenExplicitDefaultIdBelongsToOtherCountry() throws Exception {
175         // Set a valid default, but to one that isn't referenced by "gb".
176         CountryZonesFile.Country validGb = createValidCountryGb().toBuilder()
177                 .setDefaultTimeZoneId(createValidCountryFr().getTimeZoneMappings(0).getId())
178                 .build();
179         CountryZonesFile.CountryZones gbCountryZones = createValidCountryZones(validGb);
180         String countryZonesFile = createCountryZonesFile(gbCountryZones);
181 
182         List<ZoneTabFile.CountryEntry> gbZoneTabEntries = createValidZoneTabEntriesGb();
183         String zoneTabFile = createZoneTabFile(gbZoneTabEntries);
184 
185         String tzLookupFile = createTempFileName("tzlookup");
186         String tzIdsFile = createTempFileName("tzids");
187 
188         TzLookupGenerator tzLookupGenerator = new TzLookupGenerator(
189                 countryZonesFile, zoneTabFile, tzLookupFile, tzIdsFile);
190         assertFalse(tzLookupGenerator.execute(false /* validateAllIanaIdsAreMapped */));
191 
192         assertFileMissing(tzLookupFile);
193         assertFileMissing(tzIdsFile);
194     }
195 
196     @Test
calculatedDefaultZone()197     public void calculatedDefaultZone() throws Exception {
198         // Ensure there's no explicit default for "gb" and there's one zone.
199         CountryZonesFile.Country validCountryGb = createValidCountryGb();
200         assertEquals(1, validCountryGb.getTimeZoneMappingsCount());
201 
202         String gbTimeZoneId = validCountryGb.getTimeZoneMappings(0).getId();
203         CountryZonesFile.Country gbWithoutDefault = validCountryGb.toBuilder()
204                 .clearDefaultTimeZoneId().build();
205         List<ZoneTabFile.CountryEntry> gbZoneTabEntries = createValidZoneTabEntriesGb();
206 
207         OutputData outputData =
208                 generateOutputData(gbWithoutDefault, gbZoneTabEntries);
209 
210         // Check gb's time zone was defaulted.
211         assertContains(outputData.tzLookupXml, "code=\"gb\" default=\"" + gbTimeZoneId + "\"");
212     }
213 
214     @Test
explicitDefaultZone()215     public void explicitDefaultZone() throws Exception {
216         // Ensure there's an explicit default for "gb" and there's one zone.
217         CountryZonesFile.Country validCountryGb = createValidCountryGb();
218         String gbTimeZoneId = validCountryGb.getTimeZoneMappings(0).getId();
219         CountryZonesFile.Country gbWithExplicitDefaultTimeZone =
220                 validCountryGb.toBuilder()
221                         .setDefaultTimeZoneId(gbTimeZoneId)
222                         .build();
223         List<ZoneTabFile.CountryEntry> gbZoneTabEntries = createValidZoneTabEntriesGb();
224 
225         OutputData outputData = generateOutputData(
226                 gbWithExplicitDefaultTimeZone, gbZoneTabEntries);
227 
228         // Check gb's time zone was defaulted.
229         assertContains(outputData.tzLookupXml, "code=\"gb\" default=\"" + gbTimeZoneId + "\"");
230     }
231 
232     @Test
countryZonesContainsNonLowercaseIsoCode()233     public void countryZonesContainsNonLowercaseIsoCode() throws Exception {
234         CountryZonesFile.Country validCountry = createValidCountryGb();
235         CountryZonesFile.Country invalidCountry =
236                 createValidCountryGb().toBuilder().setIsoCode("Gb").build();
237 
238         CountryZonesFile.CountryZones countryZones =
239                 createValidCountryZones(validCountry, invalidCountry);
240         String countryZonesFile = createCountryZonesFile(countryZones);
241 
242         List<ZoneTabFile.CountryEntry> gbZoneTabEntries = createValidZoneTabEntriesGb();
243         String zoneTabFile = createZoneTabFile(gbZoneTabEntries);
244 
245         String tzLookupFile = createTempFileName("tzlookup");
246         String tzIdsFile = createTempFileName("tzids");
247 
248         TzLookupGenerator tzLookupGenerator = new TzLookupGenerator(
249                 countryZonesFile, zoneTabFile, tzLookupFile, tzIdsFile);
250         assertFalse(tzLookupGenerator.execute(false /* validateAllIanaIdsAreMapped */));
251 
252         assertFileMissing(tzLookupFile);
253         assertFileMissing(tzIdsFile);
254     }
255 
256     @Test
countryZonesContainsDuplicate()257     public void countryZonesContainsDuplicate() throws Exception {
258         CountryZonesFile.Country validGb = createValidCountryGb();
259 
260         // The file contains "gb" twice.
261         CountryZonesFile.CountryZones duplicateGbData =
262                 createValidCountryZones(validGb, validGb);
263         String countryZonesFile = createCountryZonesFile(duplicateGbData);
264 
265         List<ZoneTabFile.CountryEntry> gbZoneTabEntries = createValidZoneTabEntriesGb();
266         String zoneTabFile = createZoneTabFile(gbZoneTabEntries);
267 
268         String tzLookupFile = createTempFileName("tzlookup");
269         String tzIdsFile = createTempFileName("tzids");
270 
271         TzLookupGenerator tzLookupGenerator = new TzLookupGenerator(
272                 countryZonesFile, zoneTabFile, tzLookupFile, tzIdsFile);
273         assertFalse(tzLookupGenerator.execute(false /* validateAllIanaIdsAreMapped */));
274 
275         assertFileMissing(tzLookupFile);
276         assertFileMissing(tzIdsFile);
277     }
278 
279     @Test
shouldNotFail_whenCountryZonesAndZoneTabCountryMismatch()280     public void shouldNotFail_whenCountryZonesAndZoneTabCountryMismatch() throws Exception {
281         // The two input files contain non-identical country ISO codes.
282         CountryZonesFile.CountryZones countryZones =
283                 createValidCountryZones(createValidCountryGb(), createValidCountryFr());
284         String countryZonesFile = createCountryZonesFile(countryZones);
285 
286         String zoneTabFile =
287                 createZoneTabFile(createValidZoneTabEntriesFr(), createValidZoneTabEntriesUs());
288         String tzLookupFile = createTempFileName("tzlookup");
289         String tzIdsFile = createTempFileName("tzids");
290 
291         TzLookupGenerator tzLookupGenerator = new TzLookupGenerator(
292                 countryZonesFile, zoneTabFile, tzLookupFile, tzIdsFile);
293         assertTrue(tzLookupGenerator.execute(false /* validateAllIanaIdsAreMapped */));
294     }
295 
296     @Test
shouldNotFail_whenCountryZonesAndZoneTabDisagreeOnZones()297     public void shouldNotFail_whenCountryZonesAndZoneTabDisagreeOnZones() throws Exception {
298         CountryZonesFile.Country gbWithWrongZones = createValidCountryGb();
299         CountryZonesFile.CountryZones countryZones = createValidCountryZones(gbWithWrongZones);
300         String countryZonesFile = createCountryZonesFile(countryZones);
301 
302         String zoneTabFile = createZoneTabFile(
303                 List.of(new ZoneTabFile.CountryEntry("GB", "Europe/Paris")));
304         String tzLookupFile = createTempFileName("tzlookup");
305         String tzIdsFile = createTempFileName("tzids");
306 
307         TzLookupGenerator tzLookupGenerator = new TzLookupGenerator(
308                 countryZonesFile, zoneTabFile, tzLookupFile, tzIdsFile);
309 
310         assertTrue(tzLookupGenerator.execute(false /* validateAllIanaIdsAreMapped */));
311     }
312 
313     @Test
duplicateEntriesInZoneTab()314     public void duplicateEntriesInZoneTab() throws Exception {
315         CountryZonesFile.Country validGbCountry = createValidCountryGb();
316         CountryZonesFile.CountryZones countryZones = createValidCountryZones(validGbCountry);
317         String countryZonesFile = createCountryZonesFile(countryZones);
318 
319         String zoneTabFileWithDupes = createZoneTabFile(
320                 createValidZoneTabEntriesGb(), createValidZoneTabEntriesGb());
321 
322         String tzLookupFile = createTempFileName("tzlookup");
323         String tzIdsFile = createTempFileName("tzids");
324 
325         TzLookupGenerator tzLookupGenerator = new TzLookupGenerator(
326                 countryZonesFile, zoneTabFileWithDupes, tzLookupFile, tzIdsFile);
327         assertFalse(tzLookupGenerator.execute(false /* validateAllIanaIdsAreMapped */));
328 
329         assertFileMissing(tzLookupFile);
330         assertFileMissing(tzIdsFile);
331     }
332 
333     @Test
incorrectOffset()334     public void incorrectOffset() throws Exception {
335         CountryZonesFile.Country validGbCountry = createValidCountryGb();
336         CountryZonesFile.Country.Builder gbWithWrongOffsetBuilder = validGbCountry.toBuilder();
337         gbWithWrongOffsetBuilder.getTimeZoneMappingsBuilder(0).setUtcOffset("20:00");
338         CountryZonesFile.Country gbWithWrongOffset = gbWithWrongOffsetBuilder.build();
339 
340         CountryZonesFile.CountryZones countryZones = createValidCountryZones(gbWithWrongOffset);
341         String countryZonesFile = createCountryZonesFile(countryZones);
342 
343         String zoneTabFile = createZoneTabFile(createValidZoneTabEntriesGb());
344 
345         String tzLookupFile = createTempFileName("tzlookup");
346         String tzIdsFile = createTempFileName("tzids");
347 
348         TzLookupGenerator tzLookupGenerator = new TzLookupGenerator(
349                 countryZonesFile, zoneTabFile, tzLookupFile, tzIdsFile);
350         assertFalse(tzLookupGenerator.execute(false /* validateAllIanaIdsAreMapped */));
351 
352         assertFileMissing(tzLookupFile);
353         assertFileMissing(tzIdsFile);
354     }
355 
356     @Test
badTimeZoneMappingId()357     public void badTimeZoneMappingId() throws Exception {
358         CountryZonesFile.Country validGbCountry = createValidCountryGb();
359         CountryZonesFile.Country.Builder gbWithBadIdBuilder = validGbCountry.toBuilder();
360         gbWithBadIdBuilder.setDefaultTimeZoneId(validGbCountry.getTimeZoneMappings(0).getId())
361                 .addTimeZoneMappingsBuilder().setId(INVALID_TIME_ZONE_ID).setUtcOffset("00:00");
362         CountryZonesFile.Country gbWithBadId = gbWithBadIdBuilder.build();
363 
364         CountryZonesFile.CountryZones countryZones = createValidCountryZones(gbWithBadId);
365         String countryZonesFile = createCountryZonesFile(countryZones);
366 
367         List<ZoneTabFile.CountryEntry> zoneTabEntriesWithBadId =
368                 new ArrayList<>(createValidZoneTabEntriesGb());
369         zoneTabEntriesWithBadId.add(new ZoneTabFile.CountryEntry("GB", INVALID_TIME_ZONE_ID));
370         String zoneTabFile = createZoneTabFile(zoneTabEntriesWithBadId);
371 
372         String tzLookupFile = createTempFileName("tzlookup");
373         String tzIdsFile = createTempFileName("tzids");
374 
375         TzLookupGenerator tzLookupGenerator = new TzLookupGenerator(
376                 countryZonesFile, zoneTabFile, tzLookupFile, tzIdsFile);
377         assertFalse(tzLookupGenerator.execute(false /* validateAllIanaIdsAreMapped */));
378 
379         assertFileMissing(tzLookupFile);
380         assertFileMissing(tzIdsFile);
381     }
382 
383     @Test
usingOldIdsInCountryTextIsValid()384     public void usingOldIdsInCountryTextIsValid() throws Exception {
385         // This simulates a case where America/Godthab has been superseded by America/Nuuk in IANA
386         // data, but Android wants to continue using America/Godthab. This is signaled as deliberate
387         // through the use of the alternativeIds in countryzones.txt.
388         String countryZonesWithOldIdText =
389                 "isoCode:\"gl\"\n"
390                 + "defaultTimeZoneId:\"America/Godthab\"\n"
391                 + "timeZoneMappings:<\n"
392                 + "  utcOffset:\"0:00\"\n"
393                 + "  id:\"America/Danmarkshavn\"\n"
394                 + "  priority:1\n"
395                 + ">\n"
396                 + "\n"
397                 + "timeZoneMappings:<\n"
398                 + "  utcOffset:\"-2:00\"\n"
399                 + "  id:\"America/Godthab\"\n"
400                 + "  alternativeIds: \"America/Nuuk\"\n"
401                 + "  priority: 17\n"
402                 + ">\n"
403                 + "\n"
404                 + "timeZoneMappings:<\n"
405                 + "  utcOffset:\"-2:00\"\n"
406                 + "  id:\"America/Scoresbysund\"\n"
407                 + "  priority:1\n"
408                 + ">\n"
409                 + "\n"
410                 + "timeZoneMappings:<\n"
411                 + "  utcOffset:\"-4:00\"\n"
412                 + "  id:\"America/Thule\"\n"
413                 + "  priority:1\n"
414                 + ">\n";
415         Country country = parseCountry(countryZonesWithOldIdText);
416         List<ZoneTabFile.CountryEntry> zoneTabWithNewIds = Arrays.asList(
417                 new ZoneTabFile.CountryEntry("GL", "America/Nuuk"),
418                 new ZoneTabFile.CountryEntry("GL", "America/Danmarkshavn"),
419                 new ZoneTabFile.CountryEntry("GL", "America/Scoresbysund"),
420                 new ZoneTabFile.CountryEntry("GL", "America/Thule")
421         );
422 
423         OutputData outputData = generateOutputData(country, zoneTabWithNewIds);
424 
425         String expectedTzLookupOutput = "<id>America/Danmarkshavn</id>\n"
426                 + "<id notafter=\"1711846800000\" repl=\"America/Godthab\">"
427                 + "America/Scoresbysund</id>\n"
428                 + "<id alts=\"America/Nuuk\">America/Godthab</id>\n"
429                 + "<id>America/Thule</id>\n";
430         String[] expectedTzLookupXmlLines = expectedTzLookupOutput.split("\\n");
431         for (String expectedTzLookupXmlLine : expectedTzLookupXmlLines) {
432             assertContains(outputData.tzLookupXml, expectedTzLookupXmlLine);
433         }
434 
435         TzIdsProto.TimeZoneIds.Builder tzIdsBuilder = TzIdsProto.TimeZoneIds
436                 .newBuilder()
437                 .setIanaVersion(TZDB_VERSION);
438 
439         TzIdsProto.CountryMapping.Builder b = TzIdsProto.CountryMapping.newBuilder()
440                 .setIsoCode("gl")
441                 .addTimeZoneIds("America/Danmarkshavn")
442                 .addTimeZoneIds("America/Godthab")
443                 .addTimeZoneIds("America/Thule");
444 
445         // Because Android lists America/Nuuk in alternativeIds in countryzones.txt, the link will
446         // be reversed from the usual.
447         addLink(b, "America/Nuuk" /* alternativeId */, "America/Godthab" /* preferredId */);
448         addReplacement(b, 1711846800000L, "America/Godthab", "America/Scoresbysund");
449 
450         tzIdsBuilder.addCountryMappings(b);
451         assertEquals(tzIdsBuilder.build(), outputData.timeZoneIds);
452     }
453 
454     @Test
alternativeIds_shouldBeUsedToFillAltsSection()455     public void alternativeIds_shouldBeUsedToFillAltsSection() throws Exception {
456         Country country = Country.newBuilder()
457                 .setIsoCode("gb")
458                 .addTimeZoneMappings(CountryZonesFile.TimeZoneMapping.newBuilder()
459                         .setId("Europe/London")
460                         .setUtcOffset("0:00")
461                         .addAlternativeIds("Europe/Belfast")
462                         .addAlternativeIds("GB")
463                         .addAlternativeIds("GB-Eire"))
464                 .build();
465 
466         List<ZoneTabFile.CountryEntry> zoneTabEntries = List.of(
467                 new ZoneTabFile.CountryEntry("GB", "Europe/London"));
468 
469         OutputData outputData = generateOutputData(country, zoneTabEntries);
470 
471         String expectedTzlookupOutput = "<country code=\"gb\" default=\"Europe/London\" "
472                 + "everutc=\"y\">\n"
473                 + "<id alts=\"Europe/Belfast,GB,GB-Eire\">Europe/London</id>\n"
474                 + "</country>";
475 
476         assertContains(outputData.tzLookupXml, expectedTzlookupOutput.split("\n"));
477 
478         TzIdsProto.TimeZoneIds.Builder tzIdsBuilder = TzIdsProto.TimeZoneIds
479                 .newBuilder()
480                 .setIanaVersion(TZDB_VERSION);
481 
482         TzIdsProto.CountryMapping.Builder b = TzIdsProto.CountryMapping.newBuilder()
483                 .setIsoCode("gb")
484                 .addTimeZoneIds("Europe/London");
485 
486         addLink(b, "Europe/Belfast" /* alternativeId */, "Europe/London" /* preferredId */);
487         addLink(b, "GB" /* alternativeId */, "Europe/London" /* preferredId */);
488         addLink(b, "GB-Eire" /* alternativeId */, "Europe/London" /* preferredId */);
489 
490         tzIdsBuilder.addCountryMappings(b);
491         assertEquals(tzIdsBuilder.build(), outputData.timeZoneIds);
492     }
493 
addLink(TzIdsProto.CountryMapping.Builder builder, String alternativeId, String preferredId)494     private static void addLink(TzIdsProto.CountryMapping.Builder builder, String alternativeId,
495             String preferredId) {
496         TzIdsProto.TimeZoneLink link =
497                 TzIdsProto.TimeZoneLink.newBuilder()
498                         .setAlternativeId(alternativeId)
499                         .setPreferredId(preferredId)
500                         .build();
501         builder.addTimeZoneLinks(link);
502     }
503 
504     @Test
everUtc_true()505     public void everUtc_true() throws Exception {
506         CountryZonesFile.Country validCountryGb = createValidCountryGb();
507         OutputData outputData = generateOutputData(
508                 validCountryGb, createValidZoneTabEntriesGb());
509 
510         // Check gb's entry contains everutc="y".
511         assertContains(outputData.tzLookupXml, "everutc=\"y\"");
512     }
513 
514     @Test
everUtc_false()515     public void everUtc_false() throws Exception {
516         CountryZonesFile.Country validCountryFr = createValidCountryFr();
517         OutputData outputData = generateOutputData(
518                 validCountryFr, createValidZoneTabEntriesFr());
519 
520         // Check fr's entry contains everutc="n".
521         assertContains(outputData.tzLookupXml, "everutc=\"n\"");
522     }
523 
524     @Test
shownInPicker_false()525     public void shownInPicker_false() throws Exception {
526         CountryZonesFile.Country countryPrototype = createValidCountryFr();
527 
528         CountryZonesFile.TimeZoneMapping.Builder timeZoneMappingBuilder =
529                 countryPrototype.getTimeZoneMappings(0).toBuilder();
530         timeZoneMappingBuilder.setShownInPicker(false);
531 
532         CountryZonesFile.Country.Builder countryBuilder = countryPrototype.toBuilder();
533         countryBuilder.setTimeZoneMappings(0, timeZoneMappingBuilder);
534         CountryZonesFile.Country country = countryBuilder.build();
535 
536         OutputData outputData = generateOutputData(
537                 country, createValidZoneTabEntriesFr());
538 
539         assertContains(outputData.tzLookupXml, "picker=\"n\"");
540     }
541 
542     @Test
shownInPicker_true()543     public void shownInPicker_true() throws Exception {
544         CountryZonesFile.Country countryPrototype = createValidCountryFr();
545 
546         CountryZonesFile.TimeZoneMapping.Builder timeZoneMappingBuilder =
547                 countryPrototype.getTimeZoneMappings(0).toBuilder();
548         timeZoneMappingBuilder.setShownInPicker(true);
549 
550         CountryZonesFile.Country.Builder countryBuilder = countryPrototype.toBuilder();
551         countryBuilder.setTimeZoneMappings(0, timeZoneMappingBuilder);
552         CountryZonesFile.Country country = countryBuilder.build();
553 
554         OutputData outputData = generateOutputData(
555                 country, createValidZoneTabEntriesFr());
556 
557         // We should not see anything "picker="y" is the implicit default.
558         assertAbsent(outputData.tzLookupXml, "picker=");
559     }
560 
561     @Test
notAfter()562     public void notAfter() throws Exception {
563         CountryZonesFile.Country country = createValidCountryUs();
564         List<ZoneTabFile.CountryEntry> zoneTabEntries = createValidZoneTabEntriesUs();
565         OutputData outputData = generateOutputData(country, zoneTabEntries);
566         String expectedTzLookupOutput =
567                 "<id>America/New_York</id>\n"
568                 + "<id notafter=\"167814000000\" repl=\"America/New_York\">America/Detroit</id>\n"
569                 + "<id notafter=\"152089200000\" repl=\"America/New_York\">America/Kentucky/Louisville</id>\n"
570                 + "<id notafter=\"972802800000\" repl=\"America/New_York\">America/Kentucky/Monticello</id>\n"
571                 + "<id notafter=\"1130652000000\" repl=\"America/New_York\">America/Indiana/Indianapolis</id>\n"
572                 + "<id notafter=\"1194159600000\" repl=\"America/New_York\">America/Indiana/Vincennes</id>\n"
573                 + "<id notafter=\"1173600000000\" repl=\"America/New_York\">America/Indiana/Winamac</id>\n"
574                 + "<id notafter=\"183535200000\" repl=\"America/Indiana/Indianapolis\">America/Indiana/Marengo</id>\n"
575                 + "<id notafter=\"247042800000\" repl=\"America/Indiana/Vincennes\">America/Indiana/Petersburg</id>\n"
576                 + "<id notafter=\"89186400000\" repl=\"America/Indiana/Indianapolis\">America/Indiana/Vevay</id>\n"
577                 + "<id>America/Chicago</id>\n"
578                 + "<id notafter=\"1143964800000\" repl=\"America/Chicago\">America/Indiana/Tell_City</id>\n"
579                 + "<id notafter=\"688546800000\" repl=\"America/Indiana/Tell_City\">America/Indiana/Knox</id>\n"
580                 + "<id notafter=\"104918400000\" repl=\"America/Chicago\">America/Menominee</id>\n"
581                 + "<id notafter=\"720000000000\" repl=\"America/Chicago\">America/North_Dakota/Center</id>\n"
582                 + "<id notafter=\"1067155200000\" repl=\"America/Chicago\">America/North_Dakota/New_Salem</id>\n"
583                 + "<id notafter=\"1289116800000\" repl=\"America/Chicago\">America/North_Dakota/Beulah</id>\n"
584                 + "<id>America/Denver</id>\n"
585                 + "<id notafter=\"129114000000\" repl=\"America/Denver\">America/Boise</id>\n"
586                 + "<id>America/Phoenix</id>\n"
587                 + "<id>America/Los_Angeles</id>\n"
588                 + "<id>America/Anchorage</id>\n"
589                 + "<id notafter=\"436359600000\" repl=\"America/Anchorage\">America/Juneau</id>\n"
590                 + "<id notafter=\"436356000000\" repl=\"America/Juneau\">America/Yakutat</id>\n"
591                 + "<id notafter=\"436363200000\" repl=\"America/Anchorage\">America/Nome</id>\n"
592                 + "<id notafter=\"1547978400000\" repl=\"America/Anchorage\">America/Metlakatla</id>\n"
593                 + "<id notafter=\"341402400000\" repl=\"America/Juneau\">America/Sitka</id>\n"
594                 + "<id>Pacific/Honolulu</id>\n"
595                 + "<id>America/Adak</id>\n";
596         String[] expectedTzLookupXmlLines = expectedTzLookupOutput.split("\\n");
597         for (String expectedTzLookupXmlLine : expectedTzLookupXmlLines) {
598             assertContains(outputData.tzLookupXml, expectedTzLookupXmlLine);
599         }
600 
601         TzIdsProto.TimeZoneIds.Builder tzIdsBuilder = TzIdsProto.TimeZoneIds
602                 .newBuilder()
603                 .setIanaVersion(TZDB_VERSION);
604 
605         TzIdsProto.CountryMapping.Builder b = TzIdsProto.CountryMapping.newBuilder()
606                 .setIsoCode("us")
607                 .addTimeZoneIds("America/New_York")
608                 .addTimeZoneIds("America/Chicago")
609                 .addTimeZoneIds("America/Denver")
610                 .addTimeZoneIds("America/Phoenix")
611                 .addTimeZoneIds("America/Los_Angeles")
612                 .addTimeZoneIds("America/Anchorage")
613                 .addTimeZoneIds("Pacific/Honolulu")
614                 .addTimeZoneIds("America/Adak");
615 
616         addReplacement(b, 167814000000L, "America/New_York", "America/Detroit");
617         addReplacement(b, 152089200000L, "America/New_York", "America/Kentucky/Louisville");
618         addReplacement(b, 972802800000L, "America/New_York", "America/Kentucky/Monticello");
619         addReplacement(b, 1130652000000L, "America/New_York", "America/Indiana/Indianapolis");
620         addReplacement(b, 1194159600000L, "America/New_York", "America/Indiana/Vincennes");
621         addReplacement(b, 1173600000000L, "America/New_York", "America/Indiana/Winamac");
622         addReplacement(b, 183535200000L, "America/Indiana/Indianapolis", "America/Indiana/Marengo");
623         addReplacement(b, 247042800000L, "America/Indiana/Vincennes", "America/Indiana/Petersburg");
624         addReplacement(b, 89186400000L, "America/Indiana/Indianapolis", "America/Indiana/Vevay");
625         addReplacement(b, 1143964800000L, "America/Chicago", "America/Indiana/Tell_City");
626         addReplacement(b, 688546800000L, "America/Indiana/Tell_City", "America/Indiana/Knox");
627         addReplacement(b, 104918400000L, "America/Chicago", "America/Menominee");
628         addReplacement(b, 720000000000L, "America/Chicago", "America/North_Dakota/Center");
629         addReplacement(b, 1067155200000L, "America/Chicago", "America/North_Dakota/New_Salem");
630         addReplacement(b, 1289116800000L, "America/Chicago", "America/North_Dakota/Beulah");
631         addReplacement(b, 129114000000L, "America/Denver", "America/Boise");
632         addReplacement(b, 436359600000L, "America/Anchorage", "America/Juneau");
633         addReplacement(b, 436356000000L, "America/Juneau", "America/Yakutat");
634         addReplacement(b, 436363200000L, "America/Anchorage", "America/Nome");
635         addReplacement(b, 1547978400000L, "America/Anchorage", "America/Metlakatla");
636         addReplacement(b, 341402400000L, "America/Juneau", "America/Sitka");
637 
638         tzIdsBuilder.addCountryMappings(b);
639         assertEquals(tzIdsBuilder.build(), outputData.timeZoneIds);
640     }
641 
642     @Test
eachAlternativeTimeZoneShouldBeAttributedToSingleCountryOnly()643     public void eachAlternativeTimeZoneShouldBeAttributedToSingleCountryOnly() throws Exception {
644         CountryZonesFile.Country gbCountry =
645                 CountryZonesFile.Country.newBuilder()
646                         .setIsoCode("gb")
647                         .addTimeZoneMappings(CountryZonesFile.TimeZoneMapping.newBuilder()
648                                 .setUtcOffset("00:00")
649                                 .setId("Europe/London")
650                                 .addAlternativeIds("Europe/Belfast"))
651                         .build();
652 
653         CountryZonesFile.Country franceCountry =
654                 CountryZonesFile.Country.newBuilder()
655                         .setIsoCode("fr")
656                         .addTimeZoneMappings(CountryZonesFile.TimeZoneMapping.newBuilder()
657                                 .setUtcOffset("1:00")
658                                 .setId("Europe/Paris")
659                                 .addAlternativeIds("Europe/Belfast"))
660                         .build();
661 
662 
663         CountryZonesFile.CountryZones countryZones =
664                 createValidCountryZones(gbCountry, franceCountry);
665         String countryZonesFile = createCountryZonesFile(countryZones);
666 
667         String zoneTabFile = createZoneTabFile();
668 
669         String tzLookupFile = createTempFileName("tzlookup");
670         String tzIdsFile = createTempFileName("tzids");
671 
672         TzLookupGenerator tzLookupGenerator = new TzLookupGenerator(
673                 countryZonesFile, zoneTabFile, tzLookupFile, tzIdsFile);
674         assertFalse(tzLookupGenerator.execute(false /* validateAllIanaIdsAreMapped */));
675 
676         assertFileMissing(tzLookupFile);
677         assertFileMissing(tzIdsFile);
678     }
679 
680     @Test
idInMappingsShouldBeAttributedToSingleCountryOnly()681     public void idInMappingsShouldBeAttributedToSingleCountryOnly() throws Exception {
682         CountryZonesFile.Country gbCountry =
683                 CountryZonesFile.Country.newBuilder()
684                         .setIsoCode("gb")
685                         .addTimeZoneMappings(CountryZonesFile.TimeZoneMapping.newBuilder()
686                                 .setUtcOffset("00:00")
687                                 .setId("Europe/London"))
688                         .build();
689 
690         CountryZonesFile.Country franceCountry =
691                 CountryZonesFile.Country.newBuilder()
692                         .setIsoCode("fr")
693                         .addTimeZoneMappings(CountryZonesFile.TimeZoneMapping.newBuilder()
694                                 .setUtcOffset("1:00")
695                                 .setId("Europe/London"))
696                         .build();
697 
698 
699         CountryZonesFile.CountryZones countryZones =
700                 createValidCountryZones(gbCountry, franceCountry);
701         String countryZonesFile = createCountryZonesFile(countryZones);
702 
703         String zoneTabFile = createZoneTabFile();
704         String tzLookupFile = createTempFileName("tzlookup");
705         String tzIdsFile = createTempFileName("tzids");
706 
707         TzLookupGenerator tzLookupGenerator = new TzLookupGenerator(
708                 countryZonesFile, zoneTabFile, tzLookupFile, tzIdsFile);
709 
710         assertFalse(tzLookupGenerator.execute(false /* validateAllIanaIdsAreMapped */));
711 
712         assertFileMissing(tzLookupFile);
713         assertFileMissing(tzIdsFile);
714     }
715 
716     @Test
idInMappingsShouldNotAppearInAlternativeNameIds()717     public void idInMappingsShouldNotAppearInAlternativeNameIds() throws Exception {
718         CountryZonesFile.Country gbCountry =
719                 CountryZonesFile.Country.newBuilder()
720                         .setIsoCode("gb")
721                         .addTimeZoneMappings(CountryZonesFile.TimeZoneMapping.newBuilder()
722                                 .setUtcOffset("00:00")
723                                 .setId("Europe/London"))
724                         .build();
725 
726         CountryZonesFile.Country franceCountry =
727                 CountryZonesFile.Country.newBuilder()
728                         .setIsoCode("fr")
729                         .addTimeZoneMappings(CountryZonesFile.TimeZoneMapping.newBuilder()
730                                 .setUtcOffset("1:00")
731                                 .setId("Europe/Paris")
732                                 .addAlternativeIds("Europe/London"))
733                         .build();
734 
735 
736         CountryZonesFile.CountryZones countryZones =
737                 createValidCountryZones(gbCountry, franceCountry);
738         String countryZonesFile = createCountryZonesFile(countryZones);
739 
740         String zoneTabFile = createZoneTabFile();
741         String tzLookupFile = createTempFileName("tzlookup");
742         String tzIdsFile = createTempFileName("tzids");
743 
744         TzLookupGenerator tzLookupGenerator = new TzLookupGenerator(
745                 countryZonesFile, zoneTabFile, tzLookupFile, tzIdsFile);
746 
747         assertFalse(tzLookupGenerator.execute(false /* validateAllIanaIdsAreMapped */));
748 
749         assertFileMissing(tzLookupFile);
750         assertFileMissing(tzIdsFile);
751     }
752 
753     @Test
shouldFail_whenIdAppearsInMultipleTimeZoneMappingWithinACountry()754     public void shouldFail_whenIdAppearsInMultipleTimeZoneMappingWithinACountry() throws Exception {
755         CountryZonesFile.Country country =
756                 CountryZonesFile.Country.newBuilder()
757                 .setIsoCode("ar")
758                 .addTimeZoneMappings(
759                         CountryZonesFile.TimeZoneMapping.newBuilder()
760                                 .setUtcOffset("-3:00")
761                                 .setId("America/Argentina/Buenos_Aires")
762                                 .addAlternativeIds("America/Buenos_Aires"))
763                 .addTimeZoneMappings(
764                         CountryZonesFile.TimeZoneMapping.newBuilder()
765                                 .setUtcOffset("-3:00")
766                                 .setId("America/Argentina/Cordoba")
767                                 .addAlternativeIds("America/Cordoba")
768                                 .addAlternativeIds("America/Buenos_Aires"))
769                 .build();
770 
771         CountryZonesFile.CountryZones countryZones = createValidCountryZones(country);
772         String countryZonesFile = createCountryZonesFile(countryZones);
773 
774         String zoneTabFile = createZoneTabFile();
775         String tzLookupFile = createTempFileName("tzlookup");
776         String tzIdsFile = createTempFileName("tzids");
777 
778         TzLookupGenerator tzLookupGenerator = new TzLookupGenerator(
779                 countryZonesFile, zoneTabFile, tzLookupFile, tzIdsFile);
780 
781         assertFalse(tzLookupGenerator.execute(false /* validateAllIanaIdsAreMapped */));
782     }
783 
784     @Test
shouldFail_whenIdIsMentionedAmongAlternativeIds()785     public void shouldFail_whenIdIsMentionedAmongAlternativeIds() throws Exception {
786         CountryZonesFile.Country country =
787                 CountryZonesFile.Country.newBuilder()
788                 .setIsoCode("gb")
789                 .addTimeZoneMappings(
790                         CountryZonesFile.TimeZoneMapping.newBuilder()
791                                 .setUtcOffset("00:00")
792                                 .setId("Europe/London")
793                                 .addAlternativeIds("Europe/London"))
794                 .build();
795 
796         CountryZonesFile.CountryZones countryZones = createValidCountryZones(country);
797         String countryZonesFile = createCountryZonesFile(countryZones);
798 
799         String zoneTabFile = createZoneTabFile();
800         String tzlookupFile = createTempFileName("tzlookup");
801         String tzIdsFile = createTempFileName("tzids");
802 
803         TzLookupGenerator tzLookupGenerator = new TzLookupGenerator(
804                 countryZonesFile, zoneTabFile, tzlookupFile, tzIdsFile);
805 
806         assertFalse(tzLookupGenerator.execute(false /* validateAllIanaIdsAreMapped */));
807     }
808 
809     @Test
shouldFail_whenAlternativeIdsListContainsDuplicates()810     public void shouldFail_whenAlternativeIdsListContainsDuplicates() throws Exception {
811         CountryZonesFile.Country country =
812                 CountryZonesFile.Country.newBuilder()
813                 .setIsoCode("gb")
814                 .addTimeZoneMappings(
815                         CountryZonesFile.TimeZoneMapping.newBuilder()
816                                 .setUtcOffset("00:00")
817                                 .setId("Europe/London")
818                                 .addAlternativeIds("Europe/Belfast")
819                                 .addAlternativeIds("GB")
820                                 .addAlternativeIds("Europe/Belfast"))
821                 .build();
822 
823         CountryZonesFile.CountryZones countryZones = createValidCountryZones(country);
824         String countryZonesFile = createCountryZonesFile(countryZones);
825 
826         String zoneTabFile = createZoneTabFile();
827         String tzlookupFile = createTempFileName("tzlookup");
828         String tzIdsFile = createTempFileName("tzids");
829 
830         TzLookupGenerator tzLookupGenerator = new TzLookupGenerator(
831                 countryZonesFile, zoneTabFile, tzlookupFile, tzIdsFile);
832 
833         assertFalse(tzLookupGenerator.execute(false /* validateAllIanaIdsAreMapped */));
834     }
835 
836     @Test
shouldFail_whenAlternativeIdsListContainsNonEquivalentTimeZoneId()837     public void shouldFail_whenAlternativeIdsListContainsNonEquivalentTimeZoneId()
838             throws Exception {
839         CountryZonesFile.Country country =
840                 CountryZonesFile.Country.newBuilder()
841                         .setIsoCode("gb")
842                         .addTimeZoneMappings(
843                                 CountryZonesFile.TimeZoneMapping.newBuilder()
844                                         .setUtcOffset("00:00")
845                                         .setId("Europe/London")
846                                         .addAlternativeIds("Europe/Belfast")
847                                         .addAlternativeIds("GB")
848                                         .addAlternativeIds("Europe/Paris"))
849                         .build();
850 
851         CountryZonesFile.CountryZones countryZones = createValidCountryZones(country);
852         String countryZonesFile = createCountryZonesFile(countryZones);
853 
854         String zoneTabFile = createZoneTabFile();
855         String tzlookupFile = createTempFileName("tzlookup");
856         String tzIdsFile = createTempFileName("tzids");
857 
858         TzLookupGenerator tzLookupGenerator = new TzLookupGenerator(
859                 countryZonesFile, zoneTabFile, tzlookupFile, tzIdsFile);
860 
861         assertFalse(tzLookupGenerator.execute(false /* validateAllIanaIdsAreMapped */));
862     }
863 
864     @Test
shouldFail_whenAnAliasComesBeforeOriginalTimeZone()865     public void shouldFail_whenAnAliasComesBeforeOriginalTimeZone() throws Exception {
866         // These time zones relationship is unlikely to change judging by the recent
867         // TZDB updates.
868         CountryZonesFile.Country country =
869                 CountryZonesFile.Country.newBuilder()
870                         .setIsoCode("de")
871                         .setDefaultTimeZoneId("Europe/Berlin")
872                         .addTimeZoneMappings(
873                                 CountryZonesFile.TimeZoneMapping.newBuilder()
874                                         .setUtcOffset("01:00")
875                                         .setId("Europe/Busingen"))
876                         .addTimeZoneMappings(
877                                 CountryZonesFile.TimeZoneMapping.newBuilder()
878                                         .setUtcOffset("01:00")
879                                         .setId("Europe/Berlin"))
880                         .build();
881 
882         CountryZonesFile.CountryZones countryZones = createValidCountryZones(country);
883         String countryZonesFile = createCountryZonesFile(countryZones);
884 
885         String zoneTabFile = createZoneTabFile(
886                 List.of(
887                         new ZoneTabFile.CountryEntry("DE", "Europe/Berlin"),
888                         new ZoneTabFile.CountryEntry("DE", "Europe/Busingen")));
889         String tzlookupFile = createTempFileName("tzlookup");
890         String tzIdsFile = createTempFileName("tzids");
891 
892         TzLookupGenerator tzLookupGenerator = new TzLookupGenerator(
893                 countryZonesFile, zoneTabFile, tzlookupFile, tzIdsFile);
894 
895         assertFalse(tzLookupGenerator.execute(false /* validateAllIanaIdsAreMapped */));
896     }
897 
898     @Test
shouldFail_whenCountryzonesMissesTimeZone()899     public void shouldFail_whenCountryzonesMissesTimeZone() throws Exception {
900         CountryZonesFile.Country gbCountry =
901                 CountryZonesFile.Country.newBuilder()
902                         .setIsoCode("gb")
903                         .addTimeZoneMappings(CountryZonesFile.TimeZoneMapping.newBuilder()
904                                 .setUtcOffset("00:00")
905                                 .setId("Europe/London")
906                                 .addAlternativeIds("Europe/Belfast"))
907                         .build();
908 
909         CountryZonesFile.CountryZones countryZones =
910                 createValidCountryZones(gbCountry);
911         String countryZonesFile = createCountryZonesFile(countryZones);
912 
913         String zoneTabFile = createZoneTabFile();
914 
915         String tzLookupFile = createTempFileName("tzlookup");
916         String tzIdsFile = createTempFileName("tzids");
917 
918         TzLookupGenerator tzLookupGenerator = new TzLookupGenerator(
919                 countryZonesFile, zoneTabFile, tzLookupFile, tzIdsFile);
920         assertFalse(tzLookupGenerator.execute(true /* validateAllIanaIdsAreMapped */));
921     }
922 
addReplacement(TzIdsProto.CountryMapping.Builder builder, long fromMillis, String replacementId, String replacedId)923     private static void addReplacement(TzIdsProto.CountryMapping.Builder builder,
924             long fromMillis, String replacementId, String replacedId) {
925         TzIdsProto.TimeZoneReplacement replacement =
926                 TzIdsProto.TimeZoneReplacement.newBuilder()
927                         .setReplacedId(replacedId)
928                         .setReplacementId(replacementId)
929                         .setFromMillis(fromMillis)
930                         .build();
931         builder.addTimeZoneReplacements(replacement);
932     }
933 
934     static class OutputData {
935         final String tzLookupXml;
936         final TzIdsProto.TimeZoneIds timeZoneIds;
937 
OutputData(String tzLookupXml, TzIdsProto.TimeZoneIds timeZoneIds)938         OutputData(String tzLookupXml, TzIdsProto.TimeZoneIds timeZoneIds) {
939             this.tzLookupXml = tzLookupXml;
940             this.timeZoneIds = timeZoneIds;
941         }
942     }
943 
generateOutputData(CountryZonesFile.Country country, List<ZoneTabFile.CountryEntry> zoneTabEntries)944     private OutputData generateOutputData(CountryZonesFile.Country country,
945             List<ZoneTabFile.CountryEntry> zoneTabEntries) throws Exception {
946 
947         CountryZonesFile.CountryZones countryZones = createValidCountryZones(country);
948         String countryZonesFile = createCountryZonesFile(countryZones);
949 
950         String zoneTabFile = createZoneTabFile(zoneTabEntries);
951 
952         String tzLookupFile = createTempFileName("tzlookup");
953         String tzIdsFile = createTempFileName("tzids");
954 
955         TzLookupGenerator tzLookupGenerator = new TzLookupGenerator(
956                 countryZonesFile, zoneTabFile, tzLookupFile, tzIdsFile);
957         assertTrue(tzLookupGenerator.execute(false /* validateAllIanaIdsAreMapped */));
958 
959         Path tzLookupFilePath = checkFileExists(tzLookupFile);
960         String tzLookupXml = readFileToString(tzLookupFilePath);
961 
962         Path tzIdsFilePath = checkFileExists(tzIdsFile);
963         String timeZoneIdsText = readFileToString(tzIdsFilePath);
964         TzIdsProto.TimeZoneIds.Builder timeZoneIdsBuilder =
965                 TzIdsProto.TimeZoneIds.newBuilder();
966         TextFormat.merge(timeZoneIdsText, timeZoneIdsBuilder);
967 
968         return new OutputData(tzLookupXml, timeZoneIdsBuilder.build());
969     }
970 
generateTzLookupXmlExpectFailure(CountryZonesFile.Country country, List<ZoneTabFile.CountryEntry> zoneTabEntries)971     private void generateTzLookupXmlExpectFailure(CountryZonesFile.Country country,
972             List<ZoneTabFile.CountryEntry> zoneTabEntries) throws Exception {
973 
974         CountryZonesFile.CountryZones countryZones = createValidCountryZones(country);
975         String countryZonesFile = createCountryZonesFile(countryZones);
976 
977         String zoneTabFile = createZoneTabFile(zoneTabEntries);
978 
979         String tzLookupFile = createTempFileName("tzlookup");
980         String tzIdsFile = createTempFileName("tzids");
981 
982         TzLookupGenerator tzLookupGenerator = new TzLookupGenerator(
983                 countryZonesFile, zoneTabFile, tzLookupFile, tzIdsFile);
984         assertFalse(tzLookupGenerator.execute(false /* validateAllIanaIdsAreMapped */));
985     }
986 
readFileToString(Path file)987     private static String readFileToString(Path file) throws IOException {
988         return new String(Files.readAllBytes(file), StandardCharsets.UTF_8);
989     }
990 
createZoneTabFile(List<ZoneTabFile.CountryEntry>.... zoneTabEntriesLists)991     private String createZoneTabFile(List<ZoneTabFile.CountryEntry>... zoneTabEntriesLists)
992             throws Exception {
993         List<List<ZoneTabFile.CountryEntry>> entries = Arrays.asList(zoneTabEntriesLists);
994         List<String> lines = entries.stream()
995                 .flatMap(List::stream)
996                 .map(country -> country.isoCode + "\tIgnored\t" + country.olsonId)
997                 .collect(Collectors.toList());
998         return TestUtils.createFile(tempDir, lines.toArray(new String[0]));
999     }
1000 
createCountryZonesFile(CountryZonesFile.CountryZones countryZones)1001     private String createCountryZonesFile(CountryZonesFile.CountryZones countryZones) throws Exception {
1002         return TestUtils.createFile(tempDir, TextFormat.printToString(countryZones));
1003     }
1004 
createValidCountryZones( CountryZonesFile.Country... countries)1005     private static CountryZonesFile.CountryZones createValidCountryZones(
1006             CountryZonesFile.Country... countries) {
1007         CountryZonesFile.CountryZones.Builder builder =
1008                 CountryZonesFile.CountryZones.newBuilder()
1009                         .setIanaVersion(TZDB_VERSION);
1010         for (CountryZonesFile.Country country : countries) {
1011             builder.addCountries(country);
1012         }
1013         return builder.build();
1014     }
1015 
createValidCountryGb()1016     private static CountryZonesFile.Country createValidCountryGb() {
1017         return CountryZonesFile.Country.newBuilder()
1018                 .setIsoCode("gb")
1019                 .addTimeZoneMappings(CountryZonesFile.TimeZoneMapping.newBuilder()
1020                         .setUtcOffset("00:00")
1021                         .setId("Europe/London"))
1022                 .build();
1023     }
1024 
createValidCountryUs()1025     private static CountryZonesFile.Country createValidCountryUs() throws Exception {
1026         // This country demonstrates most interesting algorithm behavior. This is copied verbatim
1027         // from countryzones.txt.
1028         String usText =
1029                 "  isoCode:\"us\"\n"
1030                 + "  defaultTimeZoneId:\"America/New_York\"\n"
1031                 + "  timeZoneMappings:<\n"
1032                 + "    utcOffset:\"-5:00\"\n"
1033                 + "    id:\"America/New_York\"\n"
1034                 + "    priority:10\n"
1035                 + "  >\n"
1036                 + "  timeZoneMappings:<\n"
1037                 + "    utcOffset:\"-5:00\"\n"
1038                 + "    id:\"America/Detroit\"\n"
1039                 + "  >\n"
1040                 + "  timeZoneMappings:<\n"
1041                 + "    utcOffset:\"-5:00\"\n"
1042                 + "    id:\"America/Kentucky/Louisville\"\n"
1043                 + "  >\n"
1044                 + "  timeZoneMappings:<\n"
1045                 + "    utcOffset:\"-5:00\"\n"
1046                 + "    id:\"America/Kentucky/Monticello\"\n"
1047                 + "  >\n"
1048                 + "  timeZoneMappings:<\n"
1049                 + "    utcOffset:\"-5:00\"\n"
1050                 + "    id:\"America/Indiana/Indianapolis\"\n"
1051                 + "    priority:9\n"
1052                 + "  >\n"
1053                 + "  timeZoneMappings:<\n"
1054                 + "    utcOffset:\"-5:00\"\n"
1055                 + "    id:\"America/Indiana/Vincennes\"\n"
1056                 + "    priority:9\n"
1057                 + "  >\n"
1058                 + "  timeZoneMappings:<\n"
1059                 + "    utcOffset:\"-5:00\"\n"
1060                 + "    id:\"America/Indiana/Winamac\"\n"
1061                 + "  >\n"
1062                 + "  timeZoneMappings:<\n"
1063                 + "    utcOffset:\"-5:00\"\n"
1064                 + "    id:\"America/Indiana/Marengo\"\n"
1065                 + "  >\n"
1066                 + "  timeZoneMappings:<\n"
1067                 + "    utcOffset:\"-5:00\"\n"
1068                 + "    id:\"America/Indiana/Petersburg\"\n"
1069                 + "  >\n"
1070                 + "  timeZoneMappings:<\n"
1071                 + "    utcOffset:\"-5:00\"\n"
1072                 + "    id:\"America/Indiana/Vevay\"\n"
1073                 + "  >\n"
1074                 + "  timeZoneMappings:<\n"
1075                 + "    utcOffset:\"-6:00\"\n"
1076                 + "    id:\"America/Chicago\"\n"
1077                 + "    priority:10\n"
1078                 + "  >\n"
1079                 + "  timeZoneMappings:<\n"
1080                 + "    utcOffset:\"-6:00\"\n"
1081                 + "    id:\"America/Indiana/Tell_City\"\n"
1082                 + "    priority:9\n"
1083                 + "  >\n"
1084                 + "  timeZoneMappings:<\n"
1085                 + "    utcOffset:\"-6:00\"\n"
1086                 + "    id:\"America/Indiana/Knox\"\n"
1087                 + "  >\n"
1088                 + "  timeZoneMappings:<\n"
1089                 + "    utcOffset:\"-6:00\"\n"
1090                 + "    id:\"America/Menominee\"\n"
1091                 + "  >\n"
1092                 + "  timeZoneMappings:<\n"
1093                 + "    utcOffset:\"-6:00\"\n"
1094                 + "    id:\"America/North_Dakota/Center\"\n"
1095                 + "  >\n"
1096                 + "  timeZoneMappings:<\n"
1097                 + "    utcOffset:\"-6:00\"\n"
1098                 + "    id:\"America/North_Dakota/New_Salem\"\n"
1099                 + "  >\n"
1100                 + "  timeZoneMappings:<\n"
1101                 + "    utcOffset:\"-6:00\"\n"
1102                 + "    id:\"America/North_Dakota/Beulah\"\n"
1103                 + "  >\n"
1104                 + "  timeZoneMappings:<\n"
1105                 + "    utcOffset:\"-7:00\"\n"
1106                 + "    id:\"America/Denver\"\n"
1107                 + "    priority:9\n"
1108                 + "  >\n"
1109                 + "  timeZoneMappings:<\n"
1110                 + "    utcOffset:\"-7:00\"\n"
1111                 + "    id:\"America/Boise\"\n"
1112                 + "  >\n"
1113                 + "  timeZoneMappings:<\n"
1114                 + "    utcOffset:\"-7:00\"\n"
1115                 + "    id:\"America/Phoenix\"\n"
1116                 + "    priority:10\n"
1117                 + "  >\n"
1118                 + "  timeZoneMappings:<\n"
1119                 + "    utcOffset:\"-8:00\"\n"
1120                 + "    id:\"America/Los_Angeles\"\n"
1121                 + "  >\n"
1122                 + "  timeZoneMappings:<\n"
1123                 + "    utcOffset:\"-9:00\"\n"
1124                 + "    id:\"America/Anchorage\"\n"
1125                 + "    priority:10\n"
1126                 + "  >\n"
1127                 + "  timeZoneMappings:<\n"
1128                 + "    utcOffset:\"-9:00\"\n"
1129                 + "    id:\"America/Juneau\"\n"
1130                 + "    priority:9\n"
1131                 + "  >\n"
1132                 + "  timeZoneMappings:<\n"
1133                 + "    utcOffset:\"-9:00\"\n"
1134                 + "    id:\"America/Yakutat\"\n"
1135                 + "  >\n"
1136                 + "  timeZoneMappings:<\n"
1137                 + "    utcOffset:\"-9:00\"\n"
1138                 + "    id:\"America/Nome\"\n"
1139                 + "  >\n"
1140                 + "  timeZoneMappings:<\n"
1141                 + "    utcOffset:\"-9:00\"\n"
1142                 + "    id:\"America/Metlakatla\"\n"
1143                 + "  >\n"
1144                 + "  timeZoneMappings:<\n"
1145                 + "    utcOffset:\"-9:00\"\n"
1146                 + "    id:\"America/Sitka\"\n"
1147                 + "  >\n"
1148                 + "  timeZoneMappings:<\n"
1149                 + "    utcOffset:\"-10:00\"\n"
1150                 + "    id:\"Pacific/Honolulu\"\n"
1151                 + "    priority:10\n"
1152                 + "  >\n"
1153                 + "  timeZoneMappings:<\n"
1154                 + "    utcOffset:\"-10:00\"\n"
1155                 + "    id:\"America/Adak\"\n"
1156                 + "  >\n";
1157         return parseCountry(usText);
1158     }
1159 
createValidCountryFr()1160     private static CountryZonesFile.Country createValidCountryFr() {
1161         return CountryZonesFile.Country.newBuilder()
1162                 .setIsoCode("fr")
1163                 .addTimeZoneMappings(CountryZonesFile.TimeZoneMapping.newBuilder()
1164                         .setUtcOffset("01:00")
1165                         .setId("Europe/Paris"))
1166                 .build();
1167     }
1168 
createValidZoneTabEntriesGb()1169     private static List<ZoneTabFile.CountryEntry> createValidZoneTabEntriesGb() {
1170         return Arrays.asList(new ZoneTabFile.CountryEntry("GB", "Europe/London"));
1171     }
1172 
createValidZoneTabEntriesUs()1173     private static List<ZoneTabFile.CountryEntry> createValidZoneTabEntriesUs() {
1174         return Arrays.asList(
1175                 new ZoneTabFile.CountryEntry("US", "America/New_York"),
1176                 new ZoneTabFile.CountryEntry("US", "America/Detroit"),
1177                 new ZoneTabFile.CountryEntry("US", "America/Kentucky/Louisville"),
1178                 new ZoneTabFile.CountryEntry("US", "America/Kentucky/Monticello"),
1179                 new ZoneTabFile.CountryEntry("US", "America/Indiana/Indianapolis"),
1180                 new ZoneTabFile.CountryEntry("US", "America/Indiana/Vincennes"),
1181                 new ZoneTabFile.CountryEntry("US", "America/Indiana/Winamac"),
1182                 new ZoneTabFile.CountryEntry("US", "America/Indiana/Marengo"),
1183                 new ZoneTabFile.CountryEntry("US", "America/Indiana/Petersburg"),
1184                 new ZoneTabFile.CountryEntry("US", "America/Indiana/Vevay"),
1185                 new ZoneTabFile.CountryEntry("US", "America/Chicago"),
1186                 new ZoneTabFile.CountryEntry("US", "America/Indiana/Tell_City"),
1187                 new ZoneTabFile.CountryEntry("US", "America/Indiana/Knox"),
1188                 new ZoneTabFile.CountryEntry("US", "America/Menominee"),
1189                 new ZoneTabFile.CountryEntry("US", "America/North_Dakota/Center"),
1190                 new ZoneTabFile.CountryEntry("US", "America/North_Dakota/New_Salem"),
1191                 new ZoneTabFile.CountryEntry("US", "America/North_Dakota/Beulah"),
1192                 new ZoneTabFile.CountryEntry("US", "America/Denver"),
1193                 new ZoneTabFile.CountryEntry("US", "America/Boise"),
1194                 new ZoneTabFile.CountryEntry("US", "America/Phoenix"),
1195                 new ZoneTabFile.CountryEntry("US", "America/Los_Angeles"),
1196                 new ZoneTabFile.CountryEntry("US", "America/Anchorage"),
1197                 new ZoneTabFile.CountryEntry("US", "America/Juneau"),
1198                 new ZoneTabFile.CountryEntry("US", "America/Sitka"),
1199                 new ZoneTabFile.CountryEntry("US", "America/Metlakatla"),
1200                 new ZoneTabFile.CountryEntry("US", "America/Yakutat"),
1201                 new ZoneTabFile.CountryEntry("US", "America/Nome"),
1202                 new ZoneTabFile.CountryEntry("US", "America/Adak"),
1203                 new ZoneTabFile.CountryEntry("US", "Pacific/Honolulu"));
1204     }
1205 
createValidZoneTabEntriesFr()1206     private static List<ZoneTabFile.CountryEntry> createValidZoneTabEntriesFr() {
1207         return Arrays.asList(new ZoneTabFile.CountryEntry("FR", "Europe/Paris"));
1208     }
1209 
1210     /** Returns a file name for a file that does not exist. */
createTempFileName(String fileNamePrefix)1211     private String createTempFileName(String fileNamePrefix) throws IOException {
1212         Path tempFile = Files.createTempFile(tempDir, fileNamePrefix, null /* suffix */);
1213         Files.delete(tempFile);
1214         return tempFile.toString();
1215     }
1216 
parseCountry(String text)1217     private static Country parseCountry(String text) throws Exception {
1218         Country.Builder builder = Country.newBuilder();
1219         TextFormat.getParser().merge(text, builder);
1220         return builder.build();
1221     }
1222 
checkFileExists(String fileName)1223     private static Path checkFileExists(String fileName) {
1224         Path filePath = Paths.get(fileName);
1225         assertTrue("File " + filePath + " unexpectedly missing", Files.exists(filePath));
1226         return filePath;
1227     }
1228 
assertFileMissing(String fileName)1229     private static void assertFileMissing(String fileName) throws IOException {
1230         Path filePath = Paths.get(fileName);
1231         assertFalse("File " + filePath + " unexpectedly exists", Files.exists(filePath));
1232     }
1233 }
1234