View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.lang3.time;
18  
19  import static org.junit.jupiter.api.Assertions.assertNotEquals;
20  import static org.junit.jupiter.api.Assertions.fail;
21  import static org.junit.jupiter.api.Assumptions.assumeFalse;
22  import static org.junit.jupiter.api.Assumptions.assumeTrue;
23  
24  import java.text.DateFormatSymbols;
25  import java.text.ParseException;
26  import java.util.ArrayList;
27  import java.util.Comparator;
28  import java.util.Date;
29  import java.util.List;
30  import java.util.Locale;
31  import java.util.Objects;
32  import java.util.TimeZone;
33  
34  import org.apache.commons.lang3.AbstractLangTest;
35  import org.apache.commons.lang3.ArraySorter;
36  import org.apache.commons.lang3.JavaVersion;
37  import org.apache.commons.lang3.LocaleUtils;
38  import org.apache.commons.lang3.SystemUtils;
39  import org.junit.jupiter.api.AfterAll;
40  import org.junit.jupiter.api.Test;
41  import org.junit.jupiter.params.ParameterizedTest;
42  import org.junit.jupiter.params.provider.MethodSource;
43  import org.junitpioneer.jupiter.DefaultLocale;
44  import org.junitpioneer.jupiter.DefaultTimeZone;
45  import org.junitpioneer.jupiter.ReadsDefaultLocale;
46  import org.junitpioneer.jupiter.ReadsDefaultTimeZone;
47  
48  /* Make test reproducible */ @DefaultLocale(language = "en")
49  /* Make test reproducible */ @DefaultTimeZone(TimeZones.GMT_ID)
50  /* Make test reproducible */ @ReadsDefaultLocale
51  /* Make test reproducible */ @ReadsDefaultTimeZone
52  public class FastDateParser_TimeZoneStrategyTest extends AbstractLangTest {
53  
54      private static final List<Locale> Java11Failures = new ArrayList<>();
55      private static final List<Locale> Java17Failures = new ArrayList<>();
56  
57      @AfterAll
58      public static void afterAll() {
59          if (!Java17Failures.isEmpty()) {
60              System.err.printf("Actual failures on Java 17: %,d%n%s%n", Java17Failures.size(), Java17Failures);
61          }
62          if (!Java11Failures.isEmpty()) {
63              System.err.printf("Actual failures on Java 11: %,d%n%s%n", Java11Failures.size(), Java11Failures);
64          }
65      }
66  
67      public static Locale[] getAvailableLocalesSorted() {
68          return ArraySorter.sort(Locale.getAvailableLocales(), Comparator.comparing(Locale::toString));
69      }
70  
71      @Test
72      public void testLang1219() throws ParseException {
73          final FastDateParser parser = new FastDateParser("dd.MM.yyyy HH:mm:ss z", TimeZone.getDefault(), Locale.GERMAN);
74          final Date summer = parser.parse("26.10.2014 02:00:00 MESZ");
75          final Date standard = parser.parse("26.10.2014 02:00:00 MEZ");
76          assertNotEquals(summer.getTime(), standard.getTime());
77      }
78  
79      @ParameterizedTest
80      @MethodSource("org.apache.commons.lang3.time.FastDateParser_TimeZoneStrategyTest#getAvailableLocalesSorted")
81      public void testTimeZoneStrategy_DateFormatSymbols(final Locale locale) {
82          testTimeZoneStrategyPattern_DateFormatSymbols_getZoneStrings(locale);
83      }
84  
85      @ParameterizedTest
86      @MethodSource("org.apache.commons.lang3.time.FastDateParser_TimeZoneStrategyTest#getAvailableLocalesSorted")
87      public void testTimeZoneStrategy_TimeZone(final Locale locale) {
88          testTimeZoneStrategyPattern_TimeZone_getAvailableIDs(locale);
89      }
90  
91      private void testTimeZoneStrategyPattern(final String languageTag, final String source) throws ParseException {
92          final Locale locale = Locale.forLanguageTag(languageTag);
93          assumeFalse(LocaleUtils.isLanguageUndetermined(locale), () -> toFailureMessage(locale, languageTag));
94          assumeTrue(LocaleUtils.isAvailableLocale(locale), () -> toFailureMessage(locale, languageTag));
95          final TimeZone tzDefault = TimeZone.getTimeZone("Etc/UTC");
96          final FastDateParser parser = new FastDateParser("z", tzDefault, locale);
97          parser.parse(source);
98          testTimeZoneStrategyPattern_TimeZone_getAvailableIDs(locale);
99      }
100 
101     private void testTimeZoneStrategyPattern_DateFormatSymbols_getZoneStrings(final Locale locale) {
102         Objects.requireNonNull(locale, "locale");
103         assumeFalse(LocaleUtils.isLanguageUndetermined(locale), () -> toFailureMessage(locale, null));
104         assumeTrue(LocaleUtils.isAvailableLocale(locale), () -> toFailureMessage(locale, null));
105 
106         final String[][] zones = ArraySorter.sort(DateFormatSymbols.getInstance(locale).getZoneStrings(),
107                 Comparator.comparing(array -> array[0]));
108         for (final String[] zone : zones) {
109             for (int zIndex = 1; zIndex < zone.length; ++zIndex) {
110                 final String tzDisplay = zone[zIndex];
111                 if (tzDisplay == null) {
112                     break;
113                 }
114                 final TimeZone timeZone = TimeZone.getDefault();
115                 final FastDateParser parser = new FastDateParser("z", timeZone, locale);
116                 // An exception will be thrown and the test will fail if parsing isn't successful
117                 try {
118                     parser.parse(tzDisplay);
119                 } catch (ParseException e) {
120                     // Hack Start
121                     // See failures on GitHub Actions builds for Java 17.
122                     final String localeStr = locale.toString();
123                     if (SystemUtils.isJavaVersionAtLeast(JavaVersion.JAVA_17)
124                             && (localeStr.contains("_") || "Coordinated Universal Time".equals(tzDisplay)
125                                     || "sommartid – Atyrau".equals(tzDisplay))) {
126                         Java17Failures.add(locale);
127                         // Mark as an assumption failure instead of a hard fail
128                         System.err.printf(
129                                 "Java %s - Mark as an assumption failure instead of a hard fail: locale = '%s', parse = '%s'%n",
130                                 SystemUtils.JAVA_VERSION,
131                                 localeStr, tzDisplay);
132                         assumeTrue(false, localeStr);
133                         continue;
134                     } else if (SystemUtils.IS_JAVA_11
135                             && (localeStr.contains("_") || "Coordinated Universal Time".equals(tzDisplay))) {
136                         Java11Failures.add(locale);
137                         // Mark as an assumption failure instead of a hard fail
138                         System.err.printf(
139                                 "Java %s %s - Mark as an assumption failure instead of a hard fail: locale = '%s', parse = '%s'%n",
140                                 SystemUtils.JAVA_VENDOR,
141                                 SystemUtils.JAVA_VERSION,
142                                 localeStr, tzDisplay);
143                         assumeTrue(false, localeStr);
144                         continue;
145                     }
146                     // Hack End
147                     fail(String.format("%s: with locale = %s, zIndex = %,d, tzDisplay = '%s', parser = '%s'", e,
148                             localeStr, zIndex, tzDisplay, parser.toString()), e);
149                 }
150             }
151         }
152     }
153 
154     /**
155      * Breaks randomly on GitHub for Locale "pt_PT", TimeZone "Etc/UTC" if we do not check if the Locale's language is "undetermined".
156      *
157      * @throws ParseException
158      */
159     private void testTimeZoneStrategyPattern_TimeZone_getAvailableIDs(final Locale locale) {
160         Objects.requireNonNull(locale, "locale");
161         assumeFalse(LocaleUtils.isLanguageUndetermined(locale), () -> toFailureMessage(locale, null));
162         assumeTrue(LocaleUtils.isAvailableLocale(locale), () -> toFailureMessage(locale, null));
163 
164         for (final String id : ArraySorter.sort(TimeZone.getAvailableIDs())) {
165             final TimeZone timeZone = TimeZone.getTimeZone(id);
166             final FastDateParser parser = new FastDateParser("z", timeZone, locale);
167             final String displayName = timeZone.getDisplayName(locale);
168             try {
169                 parser.parse(displayName);
170             } catch (ParseException e) {
171                 // Missing "Zulu" or something else in broken JDK's GH builds?
172                 fail(String.format("%s: with locale = %s, id = '%s', timeZone = %s, displayName = '%s', parser = '%s'", e, locale, id, timeZone, displayName,
173                         parser.toStringAll()), e);
174             }
175         }
176     }
177 
178     /**
179      * Breaks randomly on GitHub for Locale "pt_PT", TimeZone "Etc/UTC" if we do not check if the Locale's language is "undetermined".
180      *
181      * <pre>{@code
182      * java.text.ParseException: Unparseable date: Horário do Meridiano de Greenwich: with tzDefault =
183      * sun.util.calendar.ZoneInfo[id="Etc/UTC",offset=0,dstSavings=0,useDaylight=false,transitions=0,lastRule=null], locale = pt_LU, zones[][] size = '601',
184      * zone[] size = '7', zIndex = 3, tzDisplay = 'Horário do Meridiano de Greenwich'
185      * }</pre>
186      *
187      * @throws ParseException Test failure
188      */
189     @Test
190     public void testTimeZoneStrategyPatternPortugal() throws ParseException {
191         testTimeZoneStrategyPattern("pt_PT", "Horário do Meridiano de Greenwich");
192     }
193 
194     /**
195      * Breaks randomly on GitHub for Locale "sr_ME_#Cyrl", TimeZone "Etc/UTC" if we do not check if the Locale's language is "undetermined".
196      *
197      * <pre>{@code
198      * java.text.ParseException: Unparseable date: Srednje vreme po Griniču: with tzDefault = sun.util.calendar.ZoneInfo[id="Etc/UTC",
199      * offset=0,dstSavings=0,useDaylight=false,transitions=0,lastRule=null], locale = sr_ME_#Cyrl, zones[][] size = '601',
200      * zone[] size = '7', zIndex = 3, tzDisplay = 'Srednje vreme po Griniču'
201      * }</pre>
202      *
203      * @throws ParseException Test failure
204      */
205     @Test
206     public void testTimeZoneStrategyPatternSuriname() throws ParseException {
207         testTimeZoneStrategyPattern("sr_ME_#Cyrl", "Srednje vreme po Griniču");
208     }
209 
210     private String toFailureMessage(final Locale locale, final String languageTag) {
211         return String.format("locale = %s, languageTag = '%s', isAvailableLocale = %s, isLanguageUndetermined = %s", languageTag, locale,
212                 LocaleUtils.isAvailableLocale(locale), LocaleUtils.isLanguageUndetermined(locale));
213     }
214 }