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    *      https://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  import java.util.concurrent.atomic.AtomicInteger;
34  
35  import org.apache.commons.lang3.AbstractLangTest;
36  import org.apache.commons.lang3.ArraySorter;
37  import org.apache.commons.lang3.JavaVersion;
38  import org.apache.commons.lang3.LocaleUtils;
39  import org.apache.commons.lang3.SystemUtils;
40  import org.junit.jupiter.api.AfterAll;
41  import org.junit.jupiter.api.Test;
42  import org.junit.jupiter.params.ParameterizedTest;
43  import org.junit.jupiter.params.provider.MethodSource;
44  import org.junitpioneer.jupiter.DefaultLocale;
45  import org.junitpioneer.jupiter.DefaultTimeZone;
46  import org.junitpioneer.jupiter.ReadsDefaultLocale;
47  import org.junitpioneer.jupiter.ReadsDefaultTimeZone;
48  
49  /**
50   * Tests {@link FastDateParser}.
51   */
52  /* Make test reproducible */ @DefaultLocale(language = "en")
53  /* Make test reproducible */ @DefaultTimeZone(TimeZones.GMT_ID)
54  /* Make test reproducible */ @ReadsDefaultLocale
55  /* Make test reproducible */ @ReadsDefaultTimeZone
56  class FastDateParser_TimeZoneStrategyTest extends AbstractLangTest {
57  
58      private static final List<Locale> Java11Failures = new ArrayList<>();
59      private static final List<Locale> Java17Failures = new ArrayList<>();
60      private static final AtomicInteger fails = new AtomicInteger();
61  
62      @AfterAll
63      public static void afterAll() {
64          if (!Java17Failures.isEmpty()) {
65              System.err.printf("Actual failures on Java 17+: %,d%n%s%n", Java17Failures.size(), Java17Failures);
66          }
67          if (!Java11Failures.isEmpty()) {
68              System.err.printf("Actual failures on Java 11: %,d%n%s%n", Java11Failures.size(), Java11Failures);
69          }
70      }
71  
72      private String[][] getZoneStringsSorted(final Locale locale) {
73          return ArraySorter.sort(DateFormatSymbols.getInstance(locale).getZoneStrings(), Comparator.comparing(array -> array[0]));
74      }
75  
76      @Test
77      void testLang1219() throws ParseException {
78          final FastDateParser parser = new FastDateParser("dd.MM.yyyy HH:mm:ss z", TimeZone.getDefault(), Locale.GERMAN);
79          final Date summer = parser.parse("26.10.2014 02:00:00 MESZ");
80          final Date standard = parser.parse("26.10.2014 02:00:00 MEZ");
81          assertNotEquals(summer.getTime(), standard.getTime());
82      }
83  
84      @ParameterizedTest
85      @MethodSource("org.apache.commons.lang3.LocaleUtils#availableLocaleList()")
86      void testTimeZoneStrategy_DateFormatSymbols(final Locale locale) {
87          testTimeZoneStrategyPattern_DateFormatSymbols_getZoneStrings(locale);
88      }
89  
90      @ParameterizedTest
91      @MethodSource("org.apache.commons.lang3.LocaleUtils#availableLocaleList()")
92      void testTimeZoneStrategy_TimeZone(final Locale locale) {
93          testTimeZoneStrategyPattern_TimeZone_getAvailableIDs(locale);
94      }
95  
96      private void testTimeZoneStrategyPattern(final String languageTag, final String source) throws ParseException {
97          final Locale locale = Locale.forLanguageTag(languageTag);
98          final TimeZone timeZone = TimeZone.getTimeZone("Etc/UTC");
99          assumeFalse(LocaleUtils.isLanguageUndetermined(locale), () -> toFailureMessage(locale, languageTag, timeZone));
100         assumeTrue(LocaleUtils.isAvailableLocale(locale), () -> toFailureMessage(locale, languageTag, timeZone));
101         final FastDateParser parser = new FastDateParser("z", timeZone, locale);
102         parser.parse(source);
103         testTimeZoneStrategyPattern_TimeZone_getAvailableIDs(locale);
104     }
105 
106     private void testTimeZoneStrategyPattern_DateFormatSymbols_getZoneStrings(final Locale locale) {
107         Objects.requireNonNull(locale, "locale");
108         assumeFalse(LocaleUtils.isLanguageUndetermined(locale), () -> toFailureMessage(locale, null, null));
109         assumeTrue(LocaleUtils.isAvailableLocale(locale), () -> toFailureMessage(locale, null, null));
110 
111         final String[][] zones = getZoneStringsSorted(locale);
112         for (final String[] zone : zones) {
113             for (int zIndex = 1; zIndex < zone.length; ++zIndex) {
114                 final String tzDisplay = zone[zIndex];
115                 if (tzDisplay == null) {
116                     break;
117                 }
118                 final TimeZone timeZone = TimeZone.getDefault();
119                 final FastDateParser parser = new FastDateParser("z", timeZone, locale);
120                 // An exception will be thrown and the test will fail if parsing isn't successful
121                 try {
122                     parser.parse(tzDisplay);
123                 } catch (final ParseException e) {
124                     // Hack Start
125                     // See failures on GitHub Actions builds for Java 17.
126                     final String localeStr = locale.toString();
127                     if (SystemUtils.isJavaVersionAtLeast(JavaVersion.JAVA_17)
128                             && (localeStr.contains("_") || "Coordinated Universal Time".equals(tzDisplay)
129                                     || "sommartid – Atyrau".equals(tzDisplay))) {
130                         Java17Failures.add(locale);
131                         // Mark as an assumption failure instead of a hard fail
132                         System.err.printf(
133                                 "[%,d][%s] Java %s %s - Mark as an assumption failure instead of a hard fail: locale = '%s', parse = '%s'%n",
134                                 fails.incrementAndGet(),
135                                 Thread.currentThread().getName(),
136                                 SystemUtils.JAVA_VENDOR,
137                                 SystemUtils.JAVA_VM_VERSION,
138                                 localeStr, tzDisplay);
139                         assumeTrue(false, localeStr);
140                         continue;
141                     }
142                     if (SystemUtils.IS_JAVA_11
143                             && (localeStr.contains("_") || "Coordinated Universal Time".equals(tzDisplay))) {
144                         Java11Failures.add(locale);
145                         // Mark as an assumption failure instead of a hard fail
146                         System.err.printf(
147                                 "[%,d][%s] Java %s %s - Mark as an assumption failure instead of a hard fail: locale = '%s', parse = '%s'%n",
148                                 fails.incrementAndGet(),
149                                 Thread.currentThread().getName(),
150                                 SystemUtils.JAVA_VENDOR,
151                                 SystemUtils.JAVA_VM_VERSION,
152                                 localeStr, tzDisplay);
153                         assumeTrue(false, localeStr);
154                         continue;
155                     }
156                     // Hack End
157                     fail(String.format("%s: with locale = %s, zIndex = %,d, tzDisplay = '%s', parser = '%s'", e,
158                             localeStr, zIndex, tzDisplay, parser), e);
159                 }
160             }
161         }
162     }
163 
164     /**
165      * Breaks randomly on GitHub for Locale "pt_PT", TimeZone "Etc/UTC" if we do not check if the Locale's language is "undetermined".
166      *
167      * @throws ParseException
168      */
169     private void testTimeZoneStrategyPattern_TimeZone_getAvailableIDs(final Locale locale) {
170         Objects.requireNonNull(locale, "locale");
171         assumeFalse(LocaleUtils.isLanguageUndetermined(locale), () -> toFailureMessage(locale, null, null));
172         assumeTrue(LocaleUtils.isAvailableLocale(locale), () -> toFailureMessage(locale, null, null));
173         for (final String id : ArraySorter.sort(TimeZone.getAvailableIDs())) {
174             final TimeZone timeZone = TimeZone.getTimeZone(id);
175             final String displayName = timeZone.getDisplayName(locale);
176             final FastDateParser parser = new FastDateParser("z", timeZone, locale);
177             try {
178                 parser.parse(displayName);
179             } catch (final ParseException e) {
180                 // Missing "Zulu" or something else in broken JDK's GH builds?
181                 // Call LocaleUtils again
182                 fail(String.format("%s: with id = '%s', displayName = '%s', %s, parser = '%s'", e, id, displayName,
183                         toFailureMessage(locale, null, timeZone), parser.toStringAll()), e);
184             }
185         }
186     }
187 
188     @Test
189     void testTimeZoneStrategyPattern_zh_HK_Hans() throws ParseException {
190         testTimeZoneStrategyPattern("zh_HK_#Hans", "?????????");
191     }
192 
193     /**
194      * Breaks randomly on GitHub for Locale "pt_PT", TimeZone "Etc/UTC" if we do not check if the Locale's language is "undetermined".
195      *
196      * <pre>{@code
197      * java.text.ParseException: Unparseable date: Horário do Meridiano de Greenwich: with tzDefault =
198      * sun.util.calendar.ZoneInfo[id="Etc/UTC",offset=0,dstSavings=0,useDaylight=false,transitions=0,lastRule=null], locale = pt_LU, zones[][] size = '601',
199      * zone[] size = '7', zIndex = 3, tzDisplay = 'Horário do Meridiano de Greenwich'
200      * }</pre>
201      *
202      * @throws ParseException Test failure
203      */
204     @Test
205     void testTimeZoneStrategyPatternPortugal() throws ParseException {
206         testTimeZoneStrategyPattern("pt_PT", "Horário do Meridiano de Greenwich");
207     }
208 
209     /**
210      * Breaks randomly on GitHub for Locale "sr_ME_#Cyrl", TimeZone "Etc/UTC" if we do not check if the Locale's language is "undetermined".
211      *
212      * <pre>{@code
213      * java.text.ParseException: Unparseable date: Srednje vreme po Griniču: with tzDefault = sun.util.calendar.ZoneInfo[id="Etc/UTC",
214      * offset=0,dstSavings=0,useDaylight=false,transitions=0,lastRule=null], locale = sr_ME_#Cyrl, zones[][] size = '601',
215      * zone[] size = '7', zIndex = 3, tzDisplay = 'Srednje vreme po Griniču'
216      * }</pre>
217      *
218      * @throws ParseException Test failure
219      */
220     @Test
221     void testTimeZoneStrategyPatternSuriname() throws ParseException {
222         testTimeZoneStrategyPattern("sr_ME_#Cyrl", "Srednje vreme po Griniču");
223     }
224 
225     private String toFailureMessage(final Locale locale, final String languageTag, final TimeZone timeZone) {
226         return String.format("locale = %s, languageTag = '%s', isAvailableLocale = %s, isLanguageUndetermined = %s, timeZone = %s", languageTag, locale,
227                 LocaleUtils.isAvailableLocale(locale), LocaleUtils.isLanguageUndetermined(locale), TimeZones.toTimeZone(timeZone));
228     }
229 }