From 265b02e03d8cbb5281e66d8726054d9f4ddaa79a Mon Sep 17 00:00:00 2001 From: Morten Haraldsen Date: Thu, 25 Jan 2024 13:37:29 +0100 Subject: [PATCH] Cleanup tests and create data driven test based off sample json file. --- pom.xml | 32 +- .../java/com/ethlo/time/CorrectnessTest.java | 479 ------------------ ...sTest.java => EthloITULeapSecondTest.java} | 2 +- .../ethlo/time/ExternalParameterizedTest.java | 104 ++++ .../java/com/ethlo/time/FormatterTest.java | 71 +++ src/test/java/com/ethlo/time/ITUTest.java | 23 +- .../java/com/ethlo/time/LeapSecondTest.java | 103 ++++ .../com/ethlo/time/TemporalAccessorTest.java | 101 ++++ src/test/java/com/ethlo/time/TestParam.java | 79 +++ .../time/fuzzer/ParseDateTimeFuzzTest.java | 2 +- src/test/resources/test-data.json | 228 +++++++++ 11 files changed, 728 insertions(+), 496 deletions(-) delete mode 100644 src/test/java/com/ethlo/time/CorrectnessTest.java rename src/test/java/com/ethlo/time/{EthloITUCorrectnessTest.java => EthloITULeapSecondTest.java} (94%) create mode 100644 src/test/java/com/ethlo/time/ExternalParameterizedTest.java create mode 100644 src/test/java/com/ethlo/time/FormatterTest.java create mode 100644 src/test/java/com/ethlo/time/LeapSecondTest.java create mode 100644 src/test/java/com/ethlo/time/TemporalAccessorTest.java create mode 100644 src/test/java/com/ethlo/time/TestParam.java create mode 100644 src/test/resources/test-data.json diff --git a/pom.xml b/pom.xml index 7749fe6..9b5227e 100644 --- a/pom.xml +++ b/pom.xml @@ -23,7 +23,7 @@ limitations under the License. com.ethlo.time bundle itu - 1.8.0 + 1.9.0-SNAPSHOT Internet Time Utility Extremely fast date-time parser and formatter - RFC 3339 (ISO 8601 profile) and W3C format @@ -42,30 +42,36 @@ limitations under the License. - - org.junit.jupiter - junit-jupiter-engine - 5.9.2 - test - org.assertj assertj-core 3.24.2 test - - org.junit.jupiter - junit-jupiter-params - 5.10.1 - test - com.code-intelligence jazzer-junit 0.22.1 test + + com.fasterxml.jackson.core + jackson-databind + 2.16.1 + test + + + org.slf4j + slf4j-api + 2.0.11 + test + + + ch.qos.logback + logback-classic + 1.4.14 + test + diff --git a/src/test/java/com/ethlo/time/CorrectnessTest.java b/src/test/java/com/ethlo/time/CorrectnessTest.java deleted file mode 100644 index d04a052..0000000 --- a/src/test/java/com/ethlo/time/CorrectnessTest.java +++ /dev/null @@ -1,479 +0,0 @@ -package com.ethlo.time; - -/*- - * #%L - * Internet Time Utility - * %% - * Copyright (C) 2017 Morten Haraldsen (ethlo) - * %% - * 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. - * #L% - */ - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import java.time.DateTimeException; -import java.time.OffsetDateTime; -import java.time.temporal.ChronoField; -import java.time.temporal.TemporalAccessor; - -import org.junit.jupiter.api.Tag; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; - -@Tag("CorrectnessTest") -public abstract class CorrectnessTest extends AbstractTest -{ - @ParameterizedTest - @ValueSource(strings = { - "2017-02-21T15:27:39Z", "2017-02-21T15:27:39.123Z", - "2017-02-21T15:27:39.123456Z", "2017-02-21T15:27:39.123456789Z", - "2017-02-21T15:27:39+00:00", "2017-02-21T15:27:39.123+00:00", - "2017-02-21T15:27:39.123456+00:00", "2017-02-21T15:27:39.123456789+00:00", - "2017-02-21T15:27:39.1+00:00", "2017-02-21T15:27:39.12+00:00", - "2017-02-21T15:27:39.123+00:00", "2017-02-21T15:27:39.1234+00:00", - "2017-02-21T15:27:39.12345+00:00", "2017-02-21T15:27:39.123456+00:00", - "2017-02-21T15:27:39.1234567+00:00", "2017-02-21T15:27:39.12345678+00:00", - "2017-02-21T15:27:39.123456789+00:00" - }) - void testValid(String valid) - { - final OffsetDateTime result = parser.parseDateTime(valid); - assertThat(result).isNotNull(); - } - - @ParameterizedTest - @ValueSource(strings = { - "2017-02-21T15", - "2017-02-21T15Z", - "2017-02-21T15:27", - "2017-02-21T15:27Z", - "2017-02-21T15:27:19~10:00", - "2017-02-21T15:27:39.+00:00", // No fractions after dot - "2017-02-21T15:27:39", // No timezone - "2017-02-21T15:27:39.123", - "2017-02-21T15:27:39.123456", - "2017-02-21T15:27:39.123456789", - "2017-02-21T15:27:39+0000", - "2017-02-21T15:27:39.123+0000", - "201702-21T15:27:39.123456+0000", - "20170221T15:27:39.123456789+0000"}) - void testInvalid(final String invalid) - { - assertThrows(DateTimeException.class, () -> parser.parseDateTime(invalid)); - } - - @Test - void testParseLeapSecondUTC() - { - verifyLeapSecondDateTime("1990-12-31T23:59:60Z", "1991-01-01T00:00:00Z", true); - } - - @Test - void testParseDoubleLeapSecondUTC() - { - assertThrows(DateTimeException.class, () -> verifyLeapSecondDateTime("1990-12-31T23:59:61Z", "1991-01-01T00:00:01Z", true)); - } - - private void verifyLeapSecondDateTime(String input, String expectedInUtc, boolean isVerifiedLeapSecond) - { - final LeapSecondException exc = getLeapSecondsException(input); - assertThat(exc.isVerifiedValidLeapYearMonth()).isEqualTo(isVerifiedLeapSecond); - assertThat(formatter.formatUtc(exc.getNearestDateTime())).isEqualTo(expectedInUtc); - assertThat(exc.getSecondsInMinute()).isEqualTo(60); - } - - @Test - void testParseLeapSecondPST() - { - verifyLeapSecondDateTime("1990-12-31T15:59:60-08:00", "1991-01-01T00:00:00Z", true); - } - - @Test - void parseWithFragmentsNoTimezone() - { - final DateTimeException exception = assertThrows(DateTimeException.class, () -> parser.parseDateTime("2017-12-21T12:20:45.987")); - assertThat(exception.getMessage()).isEqualTo("No timezone information: 2017-12-21T12:20:45.987"); - } - - @Test - void parseWithFragmentsNonDigit() - { - final DateTimeException exception = assertThrows(DateTimeException.class, () -> parser.parseDateTime("2017-12-21T12:20:45.9b7Z")); - assertThat(exception.getMessage()).isEqualTo("Invalid character starting at position 21: 2017-12-21T12:20:45.9b7Z"); - } - - @Test - void testParseLeapSecondUTCJune() - { - final String leapSecondUTC = "1992-06-30T23:59:60Z"; - verifyLeapSecondDateTime(leapSecondUTC, "1992-07-01T00:00:00Z", true); - } - - @Test - void testParseLeapSecondPSTJune() - { - verifyLeapSecondDateTime("1992-06-30T15:59:60-08:00", "1992-07-01T00:00:00Z", true); - } - - @Test - void testParseLeapSecond() - { - verifyLeapSecondDateTime("1990-12-31T15:59:60-08:00", "1991-01-01T00:00:00Z", true); - } - - @Test - void testParseLeapSecondPotentiallyCorrect() - { - verifyLeapSecondDateTime("2032-06-30T15:59:60-08:00", "2032-07-01T00:00:00Z", false); - } - - private LeapSecondException getLeapSecondsException(final String dateTime) - { - try - { - parser.parseDateTime(dateTime); - throw new IllegalArgumentException("Should have thrown LeapSecondException"); - } - catch (LeapSecondException exc) - { - return exc; - } - } - - @Test - void testFormat1() - { - final String s = "2017-02-21T15:27:39.0000000"; - final DateTimeException exception = assertThrows(DateTimeException.class, () -> parser.parseDateTime(s)); - assertThat(exception.getMessage()).isEqualTo("No timezone information: 2017-02-21T15:27:39.0000000"); - } - - @Test - void testFormat2() - { - final String s = "2017-02-21T15:27:39.000+30:00"; - final DateTimeException exception = assertThrows(DateTimeException.class, () -> parser.parseDateTime(s)); - assertThat(exception.getMessage()).isEqualTo("Zone offset hours not in valid range: value 30 is not in the range -18 to 18"); - } - - @Test - void testFormat3() - { - final String s = "2017-02-21T10:00:00.000+12:00"; - final OffsetDateTime date = parser.parseDateTime(s); - assertThat(formatter.formatUtcMilli(date)).isEqualTo("2017-02-20T22:00:00.000Z"); - } - - @Test - void testInvalidNothingAfterFractionalSeconds() - { - final String s = "2017-02-21T10:00:00.12345"; - final DateTimeException exception = assertThrows(DateTimeException.class, () -> parser.parseDateTime(s)); - assertThat(exception.getMessage()).isEqualTo("No timezone information: 2017-02-21T10:00:00.12345"); - } - - @Test - void parseWithoutSeconds() - { - final DateTimeException exception = assertThrows(DateTimeException.class, () -> parser.parseDateTime("2017-12-21T12:20Z")); - assertThat(exception.getMessage()).isEqualTo("No SECOND field found"); - } - - @Test - void testFormat4() - { - final String s = "2017-02-21T15:00:00.123Z"; - final OffsetDateTime date = parser.parseDateTime(s); - assertThat(formatter.formatUtcMilli(date)).isEqualTo("2017-02-21T15:00:00.123Z"); - } - - @Test - void testParseMoreThanNanoResolutionFails() - { - final DateTimeException exception = assertThrows(DateTimeException.class, () -> parser.parseDateTime("2017-02-21T15:00:00.1234567891Z")); - assertThat(exception.getMessage()).isEqualTo("Too many fraction digits: 2017-02-21T15:00:00.1234567891Z"); - } - - @Test - void testParseMonthOutOfBounds() - { - final DateTimeException exception = assertThrows(DateTimeException.class, () -> parser.parseDateTime("2017-13-21T15:00:00Z")); - assertThat(exception.getMessage()).isEqualTo("Invalid value for MonthOfYear (valid values 1 - 12): 13"); - } - - @Test - void testParseDayOutOfBounds() - { - final DateTimeException exception = assertThrows(DateTimeException.class, () -> parser.parseDateTime("2017-11-32T15:00:00Z")); - assertThat(exception.getMessage()).isEqualTo("Invalid value for DayOfMonth (valid values 1 - 28/31): 32"); - } - - @Test - void testParseHourOutOfBounds() - { - final DateTimeException exception = assertThrows(DateTimeException.class, () -> parser.parseDateTime("2017-12-21T24:00:00Z")); - assertThat(exception.getMessage()).isEqualTo("Invalid value for HourOfDay (valid values 0 - 23): 24"); - } - - @Test - void testParseMinuteOfBounds() - { - final DateTimeException exception = assertThrows(DateTimeException.class, () -> parser.parseDateTime("2017-12-21T23:60:00Z")); - assertThat(exception.getMessage()).isEqualTo("Invalid value for MinuteOfHour (valid values 0 - 59): 60"); - } - - @Test - void testParseSecondOutOfBounds() - { - final DateTimeException exception = assertThrows(DateTimeException.class, () -> parser.parseDateTime("2017-12-21T23:00:61Z")); - assertThat(exception.getMessage()).isEqualTo("Invalid value for SecondOfMinute (valid values 0 - 59): 61"); - } - - @Test - void testFormatMoreThanNanoResolutionFails() - { - final OffsetDateTime d = parser.parseDateTime("2017-02-21T15:00:00.123456789Z"); - final int fractionDigits = 10; - final DateTimeException exception = assertThrows(DateTimeException.class, () -> formatter.formatUtc(d, fractionDigits)); - assertThat(exception.getMessage()).isEqualTo("Maximum supported number of fraction digits in second is 9, got 10"); - } - - @Test - void testFormatUtc() - { - final String s = "2017-02-21T15:09:03.123456789Z"; - final OffsetDateTime date = parser.parseDateTime(s); - final String expected = "2017-02-21T15:09:03Z"; - final String actual = formatter.formatUtc(date); - assertThat(actual).isEqualTo(expected); - } - - @Test - void testFormatUtcMilli() - { - final String s = "2017-02-21T15:00:00.123456789Z"; - final OffsetDateTime date = parser.parseDateTime(s); - assertThat(formatter.formatUtcMilli(date)).isEqualTo("2017-02-21T15:00:00.123Z"); - } - - @Test - void testFormatUtcMicro() - { - final String s = "2017-02-21T15:00:00.123456789Z"; - final OffsetDateTime date = parser.parseDateTime(s); - assertThat(formatter.formatUtcMicro(date)).isEqualTo("2017-02-21T15:00:00.123456Z"); - } - - @Test - void testFormatUtcNano() - { - final String s = "2017-02-21T15:00:00.987654321Z"; - final OffsetDateTime date = parser.parseDateTime(s); - assertThat(formatter.formatUtcNano(date)).isEqualTo(s); - } - - @Test - void testFormat4TrailingNoise() - { - final String s = "2017-02-21T15:00:00.123ZGGG"; - final DateTimeException exception = assertThrows(DateTimeException.class, () -> parser.parseDateTime(s)); - assertThat(exception.getMessage()).isEqualTo("Trailing junk data after position 24: 2017-02-21T15:00:00.123ZGGG"); - } - - @Test - void testFormat5() - { - final String s = "2017-02-21T15:27:39.123+13:00"; - final OffsetDateTime date = parser.parseDateTime(s); - assertThat(formatter.formatUtcMilli(date)).isEqualTo("2017-02-21T02:27:39.123Z"); - } - - @Test - void testParseEmptyString() - { - final DateTimeException exception = assertThrows(DateTimeException.class, () -> parser.parseDateTime("")); - assertThat(exception.getMessage()).isEqualTo("Unexpected end of expression at position 0: ''"); - } - - @Test - void testParseNull() - { - final NullPointerException exception = assertThrows(NullPointerException.class, () -> parser.parseDateTime(null)); - assertThat(exception.getMessage()).isEqualTo("text cannot be null"); - } - - @Test - void testRfcExample() - { - // 1994-11-05T08:15:30-05:00 corresponds to November 5, 1994, 8:15:30 am, US Eastern Standard Time/ - // 1994-11-05T13:15:30Z corresponds to the same instant. - final String a = "1994-11-05T08:15:30-05:00"; - final String b = "1994-11-05T13:15:30Z"; - final OffsetDateTime dA = parser.parseDateTime(a); - final OffsetDateTime dB = parser.parseDateTime(b); - assertThat(formatter.formatUtc(dA)).isEqualTo(formatter.formatUtc(dB)); - } - - @Test - void testBadSeparator() - { - final String a = "1994 11-05T08:15:30-05:00"; - assertThrows(DateTimeException.class, () -> parser.parseDateTime(a)); - } - - @Test - void testParseNonDigit() - { - final String a = "199g-11-05T08:15:30-05:00"; - final DateTimeException exception = assertThrows(DateTimeException.class, () -> parser.parseDateTime(a)); - assertThat(exception.getMessage()).isEqualTo("Character g is not a digit"); - } - - @Test - void testInvalidDateTimeSeparator() - { - final String a = "1994-11-05X08:15:30-05:00"; - final DateTimeException exception = assertThrows(DateTimeException.class, () -> parser.parseDateTime(a)); - assertThat(exception.getMessage()).isEqualTo("Expected character [T, t, ] at position 11 '1994-11-05X08:15:30-05:00'"); - } - - @Test - void testLowerCaseTseparator() - { - final String a = "1994-11-05t08:15:30z"; - assertThat(parser.parseDateTime(a)).isNotNull(); - } - - @Test - void testSpaceAsSeparator() - { - assertThat(parser.parseDateTime("1994-11-05 08:15:30z")).isNotNull(); - } - - @Test - void testMilitaryOffset() - { - assertThrows(DateTimeException.class, () -> parser.parseDateTime("2017-02-21T15:27:39+0000")); - } - - @Test - void testParseUnknownLocalOffsetConvention() - { - final DateTimeException exception = assertThrows(DateTimeException.class, () -> parser.parseDateTime("2017-02-21T15:27:39-00:00")); - assertThat(exception.getMessage()).isEqualTo("Unknown 'Local Offset Convention' date-time not allowed"); - } - - @Test - void testParseLowercaseZ() - { - assertThat(parser.parseDateTime("2017-02-21T15:27:39.000z")).isEqualTo(OffsetDateTime.parse("2017-02-21T15:27:39.000z")); - } - - @Test - void testTemporalAccessorYear() - { - final TemporalAccessor parsed = ITU.parseLenient("2017"); - assertThat(parsed.isSupported(ChronoField.YEAR)).isTrue(); - assertThat(parsed.isSupported(ChronoField.MONTH_OF_YEAR)).isFalse(); - assertThat(parsed.isSupported(ChronoField.DAY_OF_MONTH)).isFalse(); - assertThat(parsed.isSupported(ChronoField.HOUR_OF_DAY)).isFalse(); - assertThat(parsed.isSupported(ChronoField.MINUTE_OF_HOUR)).isFalse(); - assertThat(parsed.isSupported(ChronoField.SECOND_OF_MINUTE)).isFalse(); - assertThat(parsed.isSupported(ChronoField.NANO_OF_SECOND)).isFalse(); - assertThat(parsed.getLong(ChronoField.YEAR)).isEqualTo(2017); - } - - @Test - void testTemporalAccessorMonth() - { - final TemporalAccessor parsed = ITU.parseLenient("2017-01"); - assertThat(parsed.isSupported(ChronoField.YEAR)).isTrue(); - assertThat(parsed.isSupported(ChronoField.MONTH_OF_YEAR)).isTrue(); - assertThat(parsed.isSupported(ChronoField.DAY_OF_MONTH)).isFalse(); - assertThat(parsed.isSupported(ChronoField.HOUR_OF_DAY)).isFalse(); - assertThat(parsed.isSupported(ChronoField.MINUTE_OF_HOUR)).isFalse(); - assertThat(parsed.isSupported(ChronoField.SECOND_OF_MINUTE)).isFalse(); - assertThat(parsed.isSupported(ChronoField.NANO_OF_SECOND)).isFalse(); - - assertThat(parsed.getLong(ChronoField.MONTH_OF_YEAR)).isEqualTo(1); - } - - @Test - void testTemporalAccessorDay() - { - final TemporalAccessor parsed = ITU.parseLenient("2017-01-27"); - assertThat(parsed.isSupported(ChronoField.YEAR)).isTrue(); - assertThat(parsed.isSupported(ChronoField.MONTH_OF_YEAR)).isTrue(); - assertThat(parsed.isSupported(ChronoField.DAY_OF_MONTH)).isTrue(); - assertThat(parsed.isSupported(ChronoField.HOUR_OF_DAY)).isFalse(); - assertThat(parsed.isSupported(ChronoField.MINUTE_OF_HOUR)).isFalse(); - assertThat(parsed.isSupported(ChronoField.SECOND_OF_MINUTE)).isFalse(); - assertThat(parsed.isSupported(ChronoField.NANO_OF_SECOND)).isFalse(); - - assertThat(parsed.getLong(ChronoField.DAY_OF_MONTH)).isEqualTo(27); - } - - @Test - void testTemporalAccessorMinute() - { - final TemporalAccessor parsed = ITU.parseLenient("2017-01-27T15:34"); - assertThat(parsed.isSupported(ChronoField.YEAR)).isTrue(); - assertThat(parsed.isSupported(ChronoField.MONTH_OF_YEAR)).isTrue(); - assertThat(parsed.isSupported(ChronoField.DAY_OF_MONTH)).isTrue(); - assertThat(parsed.isSupported(ChronoField.HOUR_OF_DAY)).isTrue(); - assertThat(parsed.isSupported(ChronoField.MINUTE_OF_HOUR)).isTrue(); - assertThat(parsed.isSupported(ChronoField.SECOND_OF_MINUTE)).isFalse(); - assertThat(parsed.isSupported(ChronoField.NANO_OF_SECOND)).isFalse(); - - assertThat(parsed.getLong(ChronoField.MINUTE_OF_HOUR)).isEqualTo(34); - } - - @Test - void testTemporalAccessorSecond() - { - final TemporalAccessor parsed = ITU.parseLenient("2017-01-27T15:34:49"); - assertThat(parsed.isSupported(ChronoField.YEAR)).isTrue(); - assertThat(parsed.isSupported(ChronoField.MONTH_OF_YEAR)).isTrue(); - assertThat(parsed.isSupported(ChronoField.DAY_OF_MONTH)).isTrue(); - assertThat(parsed.isSupported(ChronoField.HOUR_OF_DAY)).isTrue(); - assertThat(parsed.isSupported(ChronoField.MINUTE_OF_HOUR)).isTrue(); - assertThat(parsed.isSupported(ChronoField.SECOND_OF_MINUTE)).isTrue(); - assertThat(parsed.isSupported(ChronoField.NANO_OF_SECOND)).isFalse(); - - assertThat(parsed.getLong(ChronoField.SECOND_OF_MINUTE)).isEqualTo(49); - } - - @Test - void testTemporalAccessorNanos() - { - final TemporalAccessor parsed = ITU.parseLenient("2017-01-27T15:34:49.987654321"); - assertThat(parsed.isSupported(ChronoField.YEAR)).isTrue(); - assertThat(parsed.isSupported(ChronoField.MONTH_OF_YEAR)).isTrue(); - assertThat(parsed.isSupported(ChronoField.DAY_OF_MONTH)).isTrue(); - assertThat(parsed.isSupported(ChronoField.HOUR_OF_DAY)).isTrue(); - assertThat(parsed.isSupported(ChronoField.MINUTE_OF_HOUR)).isTrue(); - assertThat(parsed.isSupported(ChronoField.SECOND_OF_MINUTE)).isTrue(); - assertThat(parsed.isSupported(ChronoField.NANO_OF_SECOND)).isTrue(); - - assertThat(parsed.getLong(ChronoField.NANO_OF_SECOND)).isEqualTo(987654321); - } - - @Test - void testParseLeapSecondWhenNoTimeOffsetPresent() - { - ITU.parseLenient("3011-10-02T22:00:60.003"); - } -} diff --git a/src/test/java/com/ethlo/time/EthloITUCorrectnessTest.java b/src/test/java/com/ethlo/time/EthloITULeapSecondTest.java similarity index 94% rename from src/test/java/com/ethlo/time/EthloITUCorrectnessTest.java rename to src/test/java/com/ethlo/time/EthloITULeapSecondTest.java index 0c10fa4..e08c1cc 100644 --- a/src/test/java/com/ethlo/time/EthloITUCorrectnessTest.java +++ b/src/test/java/com/ethlo/time/EthloITULeapSecondTest.java @@ -24,7 +24,7 @@ import com.ethlo.time.internal.Rfc3339; import com.ethlo.time.internal.Rfc3339Formatter; -public class EthloITUCorrectnessTest extends CorrectnessTest +public class EthloITULeapSecondTest extends LeapSecondTest { @Override protected Rfc3339 getParser() diff --git a/src/test/java/com/ethlo/time/ExternalParameterizedTest.java b/src/test/java/com/ethlo/time/ExternalParameterizedTest.java new file mode 100644 index 0000000..87077be --- /dev/null +++ b/src/test/java/com/ethlo/time/ExternalParameterizedTest.java @@ -0,0 +1,104 @@ +package com.ethlo.time; + +/*- + * #%L + * Internet Time Utility + * %% + * Copyright (C) 2017 - 2024 Morten Haraldsen (ethlo) + * %% + * 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. + * #L% + */ + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; +import java.time.DateTimeException; +import java.time.Instant; +import java.time.format.DateTimeParseException; +import java.time.temporal.TemporalAccessor; +import java.util.List; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + +public class ExternalParameterizedTest +{ + private static final Logger logger = LoggerFactory.getLogger(ExternalParameterizedTest.class); + + @ParameterizedTest + @MethodSource("fromFile") + void testAll(TestParam param) + { + try + { + TemporalAccessor result; + if (param.isLenient()) + { + result = ITU.parseLenient(param.getInput()); + } + else + { + result = ITU.parseDateTime(param.getInput()); + } + + // Compare to Java's parser result + final Instant expected = Instant.parse(param.getInput()); + if (result instanceof DateTime) + { + assertThat(((DateTime) result).toInstant()).isEqualTo(expected); + } + else + { + assertThat(Instant.from(result)).isEqualTo(expected); + } + } + catch (DateTimeException exc) + { + if (param.getError() != null) + { + // expected an error, check if matching + assertThat(exc).hasMessage(param.getError()); + + if (param.getErrorIndex() != -1) + { + assertThat(exc).isInstanceOf(DateTimeParseException.class); + final DateTimeParseException dateTimeParseException = (DateTimeParseException) exc; + assertThat(dateTimeParseException.getErrorIndex()).isEqualTo(param.getErrorIndex()); + } + } + else + { + throw exc; + } + } + + } + + public static List fromFile() throws IOException + { + final List result = new ObjectMapper() + .enable(JsonParser.Feature.ALLOW_COMMENTS) + .readValue(ExternalParameterizedTest.class.getResource("/test-data.json"), new TypeReference>() + { + }); + logger.info("Loaded {} test-cases", result.size()); + return result; + } +} diff --git a/src/test/java/com/ethlo/time/FormatterTest.java b/src/test/java/com/ethlo/time/FormatterTest.java new file mode 100644 index 0000000..fefb96a --- /dev/null +++ b/src/test/java/com/ethlo/time/FormatterTest.java @@ -0,0 +1,71 @@ +package com.ethlo.time; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.time.DateTimeException; +import java.time.OffsetDateTime; + +import org.junit.jupiter.api.Test; + +public class FormatterTest +{ + @Test + void testFormat3() + { + final String s = "2017-02-21T10:00:00.000+12:00"; + final OffsetDateTime date = ITU.parseDateTime(s); + assertThat(ITU.formatUtcMilli(date)).isEqualTo("2017-02-20T22:00:00.000Z"); + } + + @Test + void testFormatMoreThanNanoResolutionFails() + { + final OffsetDateTime d = ITU.parseDateTime("2017-02-21T15:00:00.123456789Z"); + final int fractionDigits = 10; + final DateTimeException exception = assertThrows(DateTimeException.class, () -> ITU.formatUtc(d, fractionDigits)); + assertThat(exception.getMessage()).isEqualTo("Maximum supported number of fraction digits in second is 9, got 10"); + } + + @Test + void testFormatUtc() + { + final String s = "2017-02-21T15:09:03.123456789Z"; + final OffsetDateTime date = ITU.parseDateTime(s); + final String expected = "2017-02-21T15:09:03Z"; + final String actual = ITU.formatUtc(date); + assertThat(actual).isEqualTo(expected); + } + + @Test + void testFormatUtcMilli() + { + final String s = "2017-02-21T15:00:00.123456789Z"; + final OffsetDateTime date = ITU.parseDateTime(s); + assertThat(ITU.formatUtcMilli(date)).isEqualTo("2017-02-21T15:00:00.123Z"); + } + + @Test + void testFormatUtcMicro() + { + final String s = "2017-02-21T15:00:00.123456789Z"; + final OffsetDateTime date = ITU.parseDateTime(s); + assertThat(ITU.formatUtcMicro(date)).isEqualTo("2017-02-21T15:00:00.123456Z"); + } + + @Test + void testFormatUtcNano() + { + final String s = "2017-02-21T15:00:00.987654321Z"; + final OffsetDateTime date = ITU.parseDateTime(s); + assertThat(ITU.formatUtcNano(date)).isEqualTo(s); + } + + @Test + void testFormat5() + { + final String s = "2017-02-21T15:27:39.123+13:00"; + final OffsetDateTime date = ITU.parseDateTime(s); + assertThat(ITU.formatUtcMilli(date)).isEqualTo("2017-02-21T02:27:39.123Z"); + } +} diff --git a/src/test/java/com/ethlo/time/ITUTest.java b/src/test/java/com/ethlo/time/ITUTest.java index a90dafb..4555487 100644 --- a/src/test/java/com/ethlo/time/ITUTest.java +++ b/src/test/java/com/ethlo/time/ITUTest.java @@ -36,12 +36,10 @@ import java.time.Year; import java.time.YearMonth; import java.time.ZoneOffset; -import java.time.format.DateTimeParseException; import java.time.temporal.Temporal; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; @Tag("CorrectnessTest") public class ITUTest @@ -303,4 +301,25 @@ public void handle(final Year year) })); assertThat(exc).hasMessage("Expected character [T, t, ] at position 11 '2017-03-05G'"); } + + @Test + void testParseNull() + { + final NullPointerException exception = assertThrows(NullPointerException.class, () -> ITU.parseDateTime(null)); + assertThat(exception.getMessage()).isEqualTo("text cannot be null"); + } + + @Test + void testRfcExample() + { + // 1994-11-05T08:15:30-05:00 corresponds to November 5, 1994, 8:15:30 am, US Eastern Standard Time/ + // 1994-11-05T13:15:30Z corresponds to the same instant. + final String a = "1994-11-05T08:15:30-05:00"; + final String b = "1994-11-05T13:15:30Z"; + final OffsetDateTime dA = ITU.parseDateTime(a); + final OffsetDateTime dB = ITU.parseDateTime(b); + assertThat(ITU.formatUtc(dA)).isEqualTo(ITU.formatUtc(dB)); + } + + } diff --git a/src/test/java/com/ethlo/time/LeapSecondTest.java b/src/test/java/com/ethlo/time/LeapSecondTest.java new file mode 100644 index 0000000..5d0c281 --- /dev/null +++ b/src/test/java/com/ethlo/time/LeapSecondTest.java @@ -0,0 +1,103 @@ +package com.ethlo.time; + +/*- + * #%L + * Internet Time Utility + * %% + * Copyright (C) 2017 Morten Haraldsen (ethlo) + * %% + * 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. + * #L% + */ + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.time.DateTimeException; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +@Tag("CorrectnessTest") +public abstract class LeapSecondTest extends AbstractTest +{ + @Test + void testParseLeapSecondUTC() + { + verifyLeapSecondDateTime("1990-12-31T23:59:60Z", "1991-01-01T00:00:00Z", true); + } + + @Test + void testParseDoubleLeapSecondUTC() + { + assertThrows(DateTimeException.class, () -> verifyLeapSecondDateTime("1990-12-31T23:59:61Z", "1991-01-01T00:00:01Z", true)); + } + + private void verifyLeapSecondDateTime(String input, String expectedInUtc, boolean isVerifiedLeapSecond) + { + final LeapSecondException exc = getLeapSecondsException(input); + assertThat(exc.isVerifiedValidLeapYearMonth()).isEqualTo(isVerifiedLeapSecond); + assertThat(formatter.formatUtc(exc.getNearestDateTime())).isEqualTo(expectedInUtc); + assertThat(exc.getSecondsInMinute()).isEqualTo(60); + } + + @Test + void testParseLeapSecondPST() + { + verifyLeapSecondDateTime("1990-12-31T15:59:60-08:00", "1991-01-01T00:00:00Z", true); + } + + @Test + void testParseLeapSecondUTCJune() + { + final String leapSecondUTC = "1992-06-30T23:59:60Z"; + verifyLeapSecondDateTime(leapSecondUTC, "1992-07-01T00:00:00Z", true); + } + + @Test + void testParseLeapSecondPSTJune() + { + verifyLeapSecondDateTime("1992-06-30T15:59:60-08:00", "1992-07-01T00:00:00Z", true); + } + + @Test + void testParseLeapSecond() + { + verifyLeapSecondDateTime("1990-12-31T15:59:60-08:00", "1991-01-01T00:00:00Z", true); + } + + @Test + void testParseLeapSecondPotentiallyCorrect() + { + verifyLeapSecondDateTime("2032-06-30T15:59:60-08:00", "2032-07-01T00:00:00Z", false); + } + + @Test + void testParseLenientPotentialLeapSecond() + { + ITU.parseLenient("2011-10-02T22:00:60.003"); + } + + private LeapSecondException getLeapSecondsException(final String dateTime) + { + try + { + parser.parseDateTime(dateTime); + throw new IllegalArgumentException("Should have thrown LeapSecondException"); + } + catch (LeapSecondException exc) + { + return exc; + } + } +} diff --git a/src/test/java/com/ethlo/time/TemporalAccessorTest.java b/src/test/java/com/ethlo/time/TemporalAccessorTest.java new file mode 100644 index 0000000..a50bccd --- /dev/null +++ b/src/test/java/com/ethlo/time/TemporalAccessorTest.java @@ -0,0 +1,101 @@ +package com.ethlo.time; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.temporal.ChronoField; +import java.time.temporal.TemporalAccessor; + +import org.junit.jupiter.api.Test; + +public class TemporalAccessorTest +{ + @Test + void testTemporalAccessorYear() + { + final TemporalAccessor parsed = ITU.parseLenient("2017"); + assertThat(parsed.isSupported(ChronoField.YEAR)).isTrue(); + assertThat(parsed.isSupported(ChronoField.MONTH_OF_YEAR)).isFalse(); + assertThat(parsed.isSupported(ChronoField.DAY_OF_MONTH)).isFalse(); + assertThat(parsed.isSupported(ChronoField.HOUR_OF_DAY)).isFalse(); + assertThat(parsed.isSupported(ChronoField.MINUTE_OF_HOUR)).isFalse(); + assertThat(parsed.isSupported(ChronoField.SECOND_OF_MINUTE)).isFalse(); + assertThat(parsed.isSupported(ChronoField.NANO_OF_SECOND)).isFalse(); + assertThat(parsed.getLong(ChronoField.YEAR)).isEqualTo(2017); + } + + @Test + void testTemporalAccessorMonth() + { + final TemporalAccessor parsed = ITU.parseLenient("2017-01"); + assertThat(parsed.isSupported(ChronoField.YEAR)).isTrue(); + assertThat(parsed.isSupported(ChronoField.MONTH_OF_YEAR)).isTrue(); + assertThat(parsed.isSupported(ChronoField.DAY_OF_MONTH)).isFalse(); + assertThat(parsed.isSupported(ChronoField.HOUR_OF_DAY)).isFalse(); + assertThat(parsed.isSupported(ChronoField.MINUTE_OF_HOUR)).isFalse(); + assertThat(parsed.isSupported(ChronoField.SECOND_OF_MINUTE)).isFalse(); + assertThat(parsed.isSupported(ChronoField.NANO_OF_SECOND)).isFalse(); + + assertThat(parsed.getLong(ChronoField.MONTH_OF_YEAR)).isEqualTo(1); + } + + @Test + void testTemporalAccessorDay() + { + final TemporalAccessor parsed = ITU.parseLenient("2017-01-27"); + assertThat(parsed.isSupported(ChronoField.YEAR)).isTrue(); + assertThat(parsed.isSupported(ChronoField.MONTH_OF_YEAR)).isTrue(); + assertThat(parsed.isSupported(ChronoField.DAY_OF_MONTH)).isTrue(); + assertThat(parsed.isSupported(ChronoField.HOUR_OF_DAY)).isFalse(); + assertThat(parsed.isSupported(ChronoField.MINUTE_OF_HOUR)).isFalse(); + assertThat(parsed.isSupported(ChronoField.SECOND_OF_MINUTE)).isFalse(); + assertThat(parsed.isSupported(ChronoField.NANO_OF_SECOND)).isFalse(); + + assertThat(parsed.getLong(ChronoField.DAY_OF_MONTH)).isEqualTo(27); + } + + @Test + void testTemporalAccessorMinute() + { + final TemporalAccessor parsed = ITU.parseLenient("2017-01-27T15:34"); + assertThat(parsed.isSupported(ChronoField.YEAR)).isTrue(); + assertThat(parsed.isSupported(ChronoField.MONTH_OF_YEAR)).isTrue(); + assertThat(parsed.isSupported(ChronoField.DAY_OF_MONTH)).isTrue(); + assertThat(parsed.isSupported(ChronoField.HOUR_OF_DAY)).isTrue(); + assertThat(parsed.isSupported(ChronoField.MINUTE_OF_HOUR)).isTrue(); + assertThat(parsed.isSupported(ChronoField.SECOND_OF_MINUTE)).isFalse(); + assertThat(parsed.isSupported(ChronoField.NANO_OF_SECOND)).isFalse(); + + assertThat(parsed.getLong(ChronoField.MINUTE_OF_HOUR)).isEqualTo(34); + } + + @Test + void testTemporalAccessorSecond() + { + final TemporalAccessor parsed = ITU.parseLenient("2017-01-27T15:34:49"); + assertThat(parsed.isSupported(ChronoField.YEAR)).isTrue(); + assertThat(parsed.isSupported(ChronoField.MONTH_OF_YEAR)).isTrue(); + assertThat(parsed.isSupported(ChronoField.DAY_OF_MONTH)).isTrue(); + assertThat(parsed.isSupported(ChronoField.HOUR_OF_DAY)).isTrue(); + assertThat(parsed.isSupported(ChronoField.MINUTE_OF_HOUR)).isTrue(); + assertThat(parsed.isSupported(ChronoField.SECOND_OF_MINUTE)).isTrue(); + assertThat(parsed.isSupported(ChronoField.NANO_OF_SECOND)).isFalse(); + + assertThat(parsed.getLong(ChronoField.SECOND_OF_MINUTE)).isEqualTo(49); + } + + @Test + void testTemporalAccessorNanos() + { + final TemporalAccessor parsed = ITU.parseLenient("2017-01-27T15:34:49.987654321"); + assertThat(parsed.isSupported(ChronoField.YEAR)).isTrue(); + assertThat(parsed.isSupported(ChronoField.MONTH_OF_YEAR)).isTrue(); + assertThat(parsed.isSupported(ChronoField.DAY_OF_MONTH)).isTrue(); + assertThat(parsed.isSupported(ChronoField.HOUR_OF_DAY)).isTrue(); + assertThat(parsed.isSupported(ChronoField.MINUTE_OF_HOUR)).isTrue(); + assertThat(parsed.isSupported(ChronoField.SECOND_OF_MINUTE)).isTrue(); + assertThat(parsed.isSupported(ChronoField.NANO_OF_SECOND)).isTrue(); + + assertThat(parsed.getLong(ChronoField.NANO_OF_SECOND)).isEqualTo(987654321); + } + +} diff --git a/src/test/java/com/ethlo/time/TestParam.java b/src/test/java/com/ethlo/time/TestParam.java new file mode 100644 index 0000000..f73e928 --- /dev/null +++ b/src/test/java/com/ethlo/time/TestParam.java @@ -0,0 +1,79 @@ +package com.ethlo.time; + +/*- + * #%L + * Internet Time Utility + * %% + * Copyright (C) 2017 - 2024 Morten Haraldsen (ethlo) + * %% + * 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. + * #L% + */ + +import java.beans.ConstructorProperties; + +public class TestParam +{ + private final String input; + private final boolean lenient; + private final String error; + private final int errorIndex; + private final String note; + + @ConstructorProperties(value = {"input", "lenient", "error", "error_index", "note"}) + public TestParam(String input, boolean lenient, String error, Integer errorIndex, String note) + { + this.input = input; + this.lenient = lenient; + this.error = error; + this.errorIndex = errorIndex != null ? errorIndex : -1; + this.note = note; + } + + public String getInput() + { + return input; + } + + public boolean isLenient() + { + return lenient; + } + + public String getError() + { + return error; + } + + public int getErrorIndex() + { + return errorIndex; + } + + @Override + public String toString() + { + return "TestParam{" + + "input='" + input + '\'' + + ", lenient=" + lenient + + ", error='" + error + '\'' + + ", errorOffset=" + errorIndex + '\'' + + ", note=" + note + + '}'; + } + + public String getNote() + { + return note; + } +} diff --git a/src/test/java/com/ethlo/time/fuzzer/ParseDateTimeFuzzTest.java b/src/test/java/com/ethlo/time/fuzzer/ParseDateTimeFuzzTest.java index 312c4cb..9cdc19c 100644 --- a/src/test/java/com/ethlo/time/fuzzer/ParseDateTimeFuzzTest.java +++ b/src/test/java/com/ethlo/time/fuzzer/ParseDateTimeFuzzTest.java @@ -32,7 +32,7 @@ void parse(FuzzedDataProvider data) { try { - ITU.parseDateTime(data.consumeRemainingAsAsciiString()); + ITU.parseDateTime(data.consumeRemainingAsString()); } catch (DateTimeException ignored) { diff --git a/src/test/resources/test-data.json b/src/test/resources/test-data.json new file mode 100644 index 0000000..8d7bd80 --- /dev/null +++ b/src/test/resources/test-data.json @@ -0,0 +1,228 @@ +/*- + * #%L + * Internet Time Utility + * %% + * Copyright (C) 2017 - 2024 Morten Haraldsen (ethlo) + * %% + * 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. + * #L% + */ +[ + { + "input": "2020-22-12T12:11.56+04:30", + "error": "Unexpected character at position 16: ." + }, + { + "input": "2017-02-21T15:27:39Z" + }, + { + "input": "2017-02-21T15:27:39.123Z" + }, + { + "input": "2017-02-21T15:27:39.123456Z" + }, + { + "input": "2017-02-21T15:27:39.123456789Z" + }, + { + "input": "2017-02-21T15:27:39+00:00" + }, + { + "input": "2017-02-21T15:27:39.123+00:00" + }, + { + "input": "2017-02-21T15:27:39.123456+00:00" + }, + { + "input": "2017-02-21T15:27:39.123456789+00:00" + }, + { + "input": "2017-02-21T15:27:39.1+00:00" + }, + { + "input": "2017-02-21T15:27:39.12+00:00" + }, + { + "input": "2017-02-21T15:27:39.123+00:00" + }, + { + "input": "2017-02-21T15:27:39.1234+00:00" + }, + { + "input": "2017-02-21T15:27:39.12345+00:00" + }, + { + "input": "2017-02-21T15:27:39.123456+00:00" + }, + { + "input": "2017-02-21T15:27:39.1234567+00:00" + }, + { + "input": "2017-02-21T15:27:39.12345678+00:00" + }, + { + "input": "2017-02-21T15:27:39.123456789+00:00" + }, + { + "input": "2017-02-21T15", + "error": "Unexpected end of input: 2017-02-21T15" + }, + { + "input": "2017-02-21T15Z", + "error": "Expected character : at position 14 '2017-02-21T15Z'" + }, + { + "input": "2017-02-21T15:27", + "error": "No SECOND field found" + }, + { + "input": "2017-02-21T15:27Z", + "error": "No SECOND field found" + }, + { + "input": "2017-02-21T15:27:19~10:00", + "error": "Unexpected character at position 19: 2017-02-21T15:27:19~10:00" + }, + { + "input": "2017-02-21T15:27:39.+00:00", + "error": "Must have at least 1 fraction digit" + }, + { + "input": "2017-02-21T15:27:39", + "error": "No timezone information: 2017-02-21T15:27:39" + }, + { + "input": "2017-02-21T15:27:39.123", + "error": "No timezone information: 2017-02-21T15:27:39.123" + }, + { + "input": "2017-02-21T15:27:39.123456", + "error": "No timezone information: 2017-02-21T15:27:39.123456" + }, + { + "input": "2017-02-21T15:27:39.123456789", + "error": "No timezone information: 2017-02-21T15:27:39.123456789" + }, + { + "input": "2017-02-21T15:27:39+0000", + "error": "Invalid timezone offset: 2017-02-21T15:27:39+0000" + }, + { + "input": "2017-02-21T15:27:39.123+0000", + "error": "Invalid timezone offset: 2017-02-21T15:27:39.123+0000" + }, + { + "input": "201702-21T15:27:39.123456+0000", + "error": "Expected character - at position 5 '201702-21T15:27:39.123456+0000'", + "error_index": 4 + }, + { + "input": "20170221T15:27:39.123456789+0000", + "error": "Expected character - at position 5 '20170221T15:27:39.123456789+0000'", + "error_index": 4 + }, + { + "input": "2017-12-21T12:20:45.987", + "error": "No timezone information: 2017-12-21T12:20:45.987" + }, + { + "input": "2017-12-21T12:20:45.9b7Z", + "error": "Invalid character starting at position 21: 2017-12-21T12:20:45.9b7Z", + "error_index": 21 + }, + { + "input": "2017-02-21T15:27:39.0000000", + "error": "No timezone information: 2017-02-21T15:27:39.0000000" + }, + { + "input": "2017-02-21T15:27:39.000+30:00", + "error": "Zone offset hours not in valid range: value 30 is not in the range -18 to 18" + }, + { + "input": "2017-02-21T15:00:00.1234567891Z", + "error": "Too many fraction digits: 2017-02-21T15:00:00.1234567891Z" + }, + { + "input": "2017-13-21T15:00:00Z", + "error": "Invalid value for MonthOfYear (valid values 1 - 12): 13" + }, + { + "input": "2017-11-32T15:00:00Z", + "error": "Invalid value for DayOfMonth (valid values 1 - 28/31): 32" + }, + { + "input": "2017-12-21T24:00:00Z", + "error": "Invalid value for HourOfDay (valid values 0 - 23): 24" + }, + { + "input": "2017-12-21T23:60:00Z", + "error": "Invalid value for MinuteOfHour (valid values 0 - 59): 60" + }, + { + "input": "2017-12-21T23:00:61Z", + "error": "Invalid value for SecondOfMinute (valid values 0 - 59): 61" + }, + { + "input": "2020-12-31T22:22:2", + "error": "Unexpected end of input: 2020-12-31T22:22:2", + "error_index": 16 + }, + { + "input": "1994 11-05T08:15:30-05:00", + "error": "Expected character - at position 5 '1994 11-05T08:15:30-05:00'", + "error_index": 4, + "note": "Invalid separator between year and month" + }, + { + "input": "199g-11-05T08:15:30-05:00", + "error": "Character g is not a digit", + "note": "Non-digit in year" + }, + { + "input": "1994-11-05X08:15:30-05:00", + "error": "Expected character [T, t, ] at position 11 '1994-11-05X08:15:30-05:00'", + "note": "invalid date/time separator" + }, + { + "input": "1994-11-05t08:15:30z", + "note": "Lowercase 'z' as UTC timezone" + }, + { + "input": "1994-11-05 08:15:30Z", + "error": "Text '1994-11-05 08:15:30Z' could not be parsed at index 10", + "note": "Space as date/time separator" + }, + { + "input": "2017-02-21T15:27:39+0000", + "error": "Invalid timezone offset: 2017-02-21T15:27:39+0000", + "note": "Military format offset" + }, + { + "input": "2017-02-21T15:27:39-00:00", + "error": "Unknown 'Local Offset Convention' date-time not allowed" + }, + { + "input": "", + "error": "Unexpected end of expression at position 0: ''" + }, + { + "input": "2017-02-21T15:00:00.123ZGGG", + "error": "Trailing junk data after position 24: 2017-02-21T15:00:00.123ZGGG" + }, + /** LENIENT **/ + { + "input": "2020-12-31T22:22:2", + "error": "Unexpected end of input: 2020-12-31T22:22:2", + "lenient": true + } +]