Skip to content

Common: add tests for exceptions and clamping #18

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions core/commonMain/src/Instant.kt
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ public expect class Instant : Comparable<Instant> {
* @throws DateTimeFormatException if the text cannot be parsed or the boundaries of [Instant] are exceeded.
*/
fun parse(isoString: String): Instant

internal val MIN: Instant
internal val MAX: Instant
}
}

Expand Down
3 changes: 3 additions & 0 deletions core/commonMain/src/LocalDate.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ public expect class LocalDate : Comparable<LocalDate> {
* @throws DateTimeFormatException if the text cannot be parsed or the boundaries of [LocalDate] are exceeded.
*/
public fun parse(isoString: String): LocalDate

internal val MIN: LocalDate
internal val MAX: LocalDate
}

/**
Expand Down
3 changes: 3 additions & 0 deletions core/commonMain/src/LocalDateTime.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ public expect class LocalDateTime : Comparable<LocalDateTime> {
* exceeded.
*/
public fun parse(isoString: String): LocalDateTime

internal val MIN: LocalDateTime
internal val MAX: LocalDateTime
}

/**
Expand Down
181 changes: 181 additions & 0 deletions core/commonTest/src/InstantTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -287,4 +287,185 @@ class InstantTest {
assertEquals("+19999-12-31T23:59:59.000000009Z", LocalDateTime(19999, 12, 31, 23, 59, 59, 9).toInstant(TimeZone.UTC).toString())
}

private val largePositiveLongs = listOf(Long.MAX_VALUE, Long.MAX_VALUE - 1, Long.MAX_VALUE - 50)
private val largeNegativeLongs = listOf(Long.MIN_VALUE, Long.MIN_VALUE + 1, Long.MIN_VALUE + 50)
@OptIn(ExperimentalTime::class)
private val largePositiveInstants = listOf(Instant.MAX, Instant.MAX - 1.seconds, Instant.MAX - 50.seconds)
@OptIn(ExperimentalTime::class)
private val largeNegativeInstants = listOf(Instant.MIN, Instant.MIN + 1.seconds, Instant.MIN + 50.seconds)
private val smallInstants = listOf(
Instant.fromEpochMilliseconds(0),
Instant.fromEpochMilliseconds(1003),
Instant.fromEpochMilliseconds(253112)
)

@Test
fun testParsingThrowing() {
assertFailsWith<DateTimeFormatException> { Instant.parse("x") }
assertFailsWith<DateTimeFormatException> { Instant.parse("12020-12-31T23:59:59.000000000Z") }
// this string represents an Instant that is currently larger than Instant.MAX any of the implementations:
assertFailsWith<DateTimeFormatException> { Instant.parse("+1000000001-12-31T23:59:59.000000000Z") }
}

@ExperimentalTime
@Test
fun testConstructorAccessorClamping() {
// toEpochMilliseconds()/fromEpochMilliseconds()
// assuming that ranges of Long (representing a number of milliseconds) and Instant are not just overlapping,
// but one is included in the other.
if (Instant.MAX.epochSeconds > Long.MAX_VALUE / 1000) {
/* Any number of milliseconds in Long is representable as an Instant */
for (instant in largePositiveInstants) {
assertEquals(Long.MAX_VALUE, instant.toEpochMilliseconds(), "$instant")
}
for (instant in largeNegativeInstants) {
assertEquals(Long.MIN_VALUE, instant.toEpochMilliseconds(), "$instant")
}
for (milliseconds in largePositiveLongs + largeNegativeLongs) {
assertEquals(milliseconds, Instant.fromEpochMilliseconds(milliseconds).toEpochMilliseconds(),
"$milliseconds")
}
} else {
/* Any Instant is representable as a number of milliseconds in Long */
for (milliseconds in largePositiveLongs) {
assertEquals(Instant.MAX, Instant.fromEpochMilliseconds(milliseconds), "$milliseconds")
}
for (milliseconds in largeNegativeLongs) {
assertEquals(Instant.MIN, Instant.fromEpochMilliseconds(milliseconds), "$milliseconds")
}
for (instant in largePositiveInstants + smallInstants + largeNegativeInstants) {
assertEquals(instant.epochSeconds,
Instant.fromEpochMilliseconds(instant.toEpochMilliseconds()).epochSeconds, "$instant")
}
}
// fromEpochSeconds
// On all platforms Long.MAX_VALUE of seconds is not a valid instant.
for (seconds in largePositiveLongs) {
assertEquals(Instant.MAX, Instant.fromEpochSeconds(seconds, 35))
}
for (seconds in largeNegativeLongs) {
assertEquals(Instant.MIN, Instant.fromEpochSeconds(seconds, 35))
}
for (instant in largePositiveInstants + smallInstants + largeNegativeInstants) {
assertEquals(instant, Instant.fromEpochSeconds(instant.epochSeconds, instant.nanosecondsOfSecond.toLong()))
}
}

@OptIn(ExperimentalTime::class)
@Test
fun testArithmeticClamping() {
for (instant in smallInstants + largeNegativeInstants + largePositiveInstants) {
assertEquals(Instant.MAX, instant + Duration.INFINITE)
}
for (instant in smallInstants + largeNegativeInstants + largePositiveInstants) {
assertEquals(Instant.MIN, instant - Duration.INFINITE)
}
assertEquals(Instant.MAX, (Instant.MAX - 4.seconds) + 5.seconds)
assertEquals(Instant.MIN, (Instant.MIN + 10.seconds) - 12.seconds)
}

@OptIn(ExperimentalTime::class)
@Test
fun testCalendarArithmeticThrowing() {
val maxValidInstant = LocalDateTime.MAX.toInstant(TimeZone.UTC)
val minValidInstant = LocalDateTime.MIN.toInstant(TimeZone.UTC)

// Instant.plus(DateTimePeriod(), TimeZone)
// Arithmetic overflow
for (instant in smallInstants + largeNegativeInstants + largePositiveInstants) {
assertFailsWith<DateTimeArithmeticException>("$instant") {
instant.plus(DateTimePeriod(seconds = Long.MAX_VALUE), TimeZone.UTC)
}
assertFailsWith<DateTimeArithmeticException>("$instant") {
instant.plus(DateTimePeriod(seconds = Long.MIN_VALUE), TimeZone.UTC)
}
}
// Overflowing a LocalDateTime in input
maxValidInstant.plus(DateTimePeriod(nanoseconds = -1), TimeZone.UTC)
minValidInstant.plus(DateTimePeriod(nanoseconds = 1), TimeZone.UTC)
assertFailsWith<DateTimeArithmeticException> {
(maxValidInstant + 1.nanoseconds).plus(DateTimePeriod(nanoseconds = -2), TimeZone.UTC)
}
assertFailsWith<DateTimeArithmeticException> {
(minValidInstant - 1.nanoseconds).plus(DateTimePeriod(nanoseconds = 2), TimeZone.UTC)
}
// Overflowing a LocalDateTime in result
assertFailsWith<DateTimeArithmeticException> {
maxValidInstant.plus(DateTimePeriod(nanoseconds = 1), TimeZone.UTC)
}
assertFailsWith<DateTimeArithmeticException> {
minValidInstant.plus(DateTimePeriod(nanoseconds = -1), TimeZone.UTC)
}
// Overflowing a LocalDateTime in intermediate computations
assertFailsWith<DateTimeArithmeticException> {
maxValidInstant.plus(DateTimePeriod(seconds = 1, nanoseconds = -1_000_000_001), TimeZone.UTC)
}
assertFailsWith<DateTimeArithmeticException> {
maxValidInstant.plus(DateTimePeriod(hours = 1, minutes = -61), TimeZone.UTC)
}
assertFailsWith<DateTimeArithmeticException> {
maxValidInstant.plus(DateTimePeriod(days = 1, hours = -48), TimeZone.UTC)
}

// Instant.plus(Long, DateTimeUnit, TimeZone)
// Arithmetic overflow
for (instant in smallInstants + largeNegativeInstants + largePositiveInstants) {
assertFailsWith<DateTimeArithmeticException>("$instant") {
instant.plus(Long.MAX_VALUE, DateTimeUnit.SECOND, TimeZone.UTC)
}
assertFailsWith<DateTimeArithmeticException>("$instant") {
instant.plus(Long.MIN_VALUE, DateTimeUnit.SECOND, TimeZone.UTC)
}
assertFailsWith<DateTimeArithmeticException>("$instant") {
instant.plus(Long.MAX_VALUE, DateTimeUnit.YEAR, TimeZone.UTC)
}
assertFailsWith<DateTimeArithmeticException>("$instant") {
instant.plus(Long.MIN_VALUE, DateTimeUnit.YEAR, TimeZone.UTC)
}
}
// Overflowing a LocalDateTime in input
maxValidInstant.plus(-1, DateTimeUnit.NANOSECOND, TimeZone.UTC)
minValidInstant.plus(1, DateTimeUnit.NANOSECOND, TimeZone.UTC)
assertFailsWith<DateTimeArithmeticException> {
(maxValidInstant + 1.nanoseconds).plus(-2, DateTimeUnit.NANOSECOND, TimeZone.UTC)
}
assertFailsWith<DateTimeArithmeticException> {
(minValidInstant - 1.nanoseconds).plus(2, DateTimeUnit.NANOSECOND, TimeZone.UTC)
}
// Overflowing a LocalDateTime in result
assertFailsWith<DateTimeArithmeticException> {
maxValidInstant.plus(1, DateTimeUnit.NANOSECOND, TimeZone.UTC)
}
assertFailsWith<DateTimeArithmeticException> {
maxValidInstant.plus(1, DateTimeUnit.YEAR, TimeZone.UTC)
}
assertFailsWith<DateTimeArithmeticException> {
minValidInstant.plus(-1, DateTimeUnit.NANOSECOND, TimeZone.UTC)
}
assertFailsWith<DateTimeArithmeticException> {
minValidInstant.plus(-1, DateTimeUnit.YEAR, TimeZone.UTC)
}

// Instant.periodUntil
maxValidInstant.periodUntil(minValidInstant, TimeZone.UTC)
assertFailsWith<DateTimeArithmeticException> {
(maxValidInstant + 1.nanoseconds).periodUntil(minValidInstant, TimeZone.UTC)
}
assertFailsWith<DateTimeArithmeticException> {
maxValidInstant.periodUntil(minValidInstant - 1.nanoseconds, TimeZone.UTC)
}

// Instant.until
// Arithmetic overflow
assertEquals(Long.MAX_VALUE, minValidInstant.until(maxValidInstant, DateTimeUnit.NANOSECOND, TimeZone.UTC))
assertEquals(Long.MIN_VALUE, maxValidInstant.until(minValidInstant, DateTimeUnit.NANOSECOND, TimeZone.UTC))
// Overflowing a LocalDateTime in input
assertFailsWith<DateTimeArithmeticException> {
(maxValidInstant + 1.nanoseconds).until(maxValidInstant, DateTimeUnit.NANOSECOND, TimeZone.UTC)
}
assertFailsWith<DateTimeArithmeticException> {
maxValidInstant.until(maxValidInstant + 1.nanoseconds, DateTimeUnit.NANOSECOND, TimeZone.UTC)
}
}

}
57 changes: 43 additions & 14 deletions core/commonTest/src/LocalDateTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,13 @@ class LocalDateTest {
checkParsedComponents("2019-10-01", 2019, 10, 1, 2, 274)
checkParsedComponents("2016-02-29", 2016, 2, 29, 1, 60)
checkParsedComponents("2017-10-01", 2017, 10, 1, 7, 274)
assertFailsWith<Throwable> { LocalDate.parse("102017-10-01") }
assertFailsWith<Throwable> { LocalDate.parse("2017--10-01") }
assertFailsWith<Throwable> { LocalDate.parse("2017-+10-01") }
assertFailsWith<Throwable> { LocalDate.parse("2017-10-+01") }
assertFailsWith<Throwable> { LocalDate.parse("2017-10--01") }
assertFailsWith<DateTimeFormatException> { LocalDate.parse("102017-10-01") }
assertFailsWith<DateTimeFormatException> { LocalDate.parse("2017--10-01") }
assertFailsWith<DateTimeFormatException> { LocalDate.parse("2017-+10-01") }
assertFailsWith<DateTimeFormatException> { LocalDate.parse("2017-10-+01") }
assertFailsWith<DateTimeFormatException> { LocalDate.parse("2017-10--01") }
// this date is currently larger than the largest representable one any of the platforms:
assertFailsWith<DateTimeFormatException> { LocalDate.parse("+1000000000-10-01") }
}

@Test
Expand All @@ -65,7 +67,7 @@ class LocalDateTest {

checkComponents(LocalDate.parse("2016-01-31") + DatePeriod(months = 1), 2016, 2, 29)

// assertFailsWith<IllegalArgumentException> { startDate + CalendarPeriod(hours = 7) } // won't compile
// assertFailsWith<IllegalArgumentException> { startDate + DateTimePeriod(hours = 7) } // won't compile
// assertFailsWith<IllegalArgumentException> { startDate.plus(7, ChronoUnit.MINUTE) } // won't compile
}

Expand Down Expand Up @@ -136,15 +138,42 @@ class LocalDateTest {

@Test
fun invalidDate() {
assertFailsWith<Throwable> { LocalDate(2007, 2, 29) }
assertFailsWith<IllegalArgumentException> { LocalDate(2007, 2, 29) }
LocalDate(2008, 2, 29)
assertFailsWith<Throwable> { LocalDate(2007, 4, 31) }
assertFailsWith<Throwable> { LocalDate(2007, 1, 0) }
assertFailsWith<Throwable> { LocalDate(2007,1, 32) }
assertFailsWith<Throwable> { LocalDate(Int.MIN_VALUE, 1, 1) }
assertFailsWith<Throwable> { LocalDate(2007, 1, 32) }
assertFailsWith<Throwable> { LocalDate(2007, 0, 1) }
assertFailsWith<Throwable> { LocalDate(2007, 13, 1) }
assertFailsWith<IllegalArgumentException> { LocalDate(2007, 4, 31) }
assertFailsWith<IllegalArgumentException> { LocalDate(2007, 1, 0) }
assertFailsWith<IllegalArgumentException> { LocalDate(2007,1, 32) }
assertFailsWith<IllegalArgumentException> { LocalDate(Int.MIN_VALUE, 1, 1) }
assertFailsWith<IllegalArgumentException> { LocalDate(2007, 1, 32) }
assertFailsWith<IllegalArgumentException> { LocalDate(2007, 0, 1) }
assertFailsWith<IllegalArgumentException> { LocalDate(2007, 13, 1) }
}

@Test
fun testArithmeticThrowing() {
// LocalDate.plus(Long, DateTimeUnit)
LocalDate.MAX.plus(-1, DateTimeUnit.DAY)
LocalDate.MIN.plus(1, DateTimeUnit.DAY)
// Arithmetic overflow
assertFailsWith<DateTimeArithmeticException> { LocalDate.MAX.plus(Long.MAX_VALUE, DateTimeUnit.YEAR) }
assertFailsWith<DateTimeArithmeticException> { LocalDate.MAX.plus(Long.MAX_VALUE - 2, DateTimeUnit.YEAR) }
assertFailsWith<DateTimeArithmeticException> { LocalDate.MIN.plus(Long.MIN_VALUE, DateTimeUnit.YEAR) }
assertFailsWith<DateTimeArithmeticException> { LocalDate.MIN.plus(Long.MIN_VALUE + 2, DateTimeUnit.YEAR) }
assertFailsWith<DateTimeArithmeticException> { LocalDate.MIN.plus(Long.MAX_VALUE, DateTimeUnit.DAY) }
// Exceeding the boundaries of LocalDate
assertFailsWith<DateTimeArithmeticException> { LocalDate.MAX.plus(1, DateTimeUnit.YEAR) }
assertFailsWith<DateTimeArithmeticException> { LocalDate.MIN.plus(-1, DateTimeUnit.YEAR) }

// LocalDate.plus(DatePeriod)
LocalDate.MAX.plus(DatePeriod(years = -2, months = 12, days = 31))
// Exceeding the boundaries in result
assertFailsWith<DateTimeArithmeticException> {
LocalDate.MAX.plus(DatePeriod(years = -2, months = 24, days = 1))
}
// Exceeding the boundaries in intermediate computations
assertFailsWith<DateTimeArithmeticException> {
LocalDate.MAX.plus(DatePeriod(years = -2, months = 25, days = -1000))
}
}

}
22 changes: 14 additions & 8 deletions core/commonTest/src/LocalDateTimeTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ class LocalDateTimeTest {
checkParsedComponents("2019-10-01T18:43:15", 2019, 10, 1, 18, 43, 15, 0, 2, 274)
checkParsedComponents("2019-10-01T18:12", 2019, 10, 1, 18, 12, 0, 0, 2, 274)

assertFailsWith<DateTimeFormatException> { LocalDateTime.parse("x") }
assertFailsWith<DateTimeFormatException> { "+1000000000-03-26T04:00:00".toLocalDateTime() }

/* Based on the ThreeTenBp project.
* Copyright (c) 2007-present, Stephen Colebourne & Michael Nascimento Santos
*/
Expand All @@ -41,6 +44,8 @@ class LocalDateTimeTest {

val diff = with(TimeZone.UTC) { ldt2.toInstant() - ldt1.toInstant() }
assertEquals(1.hours + 7.minutes - 15.seconds + 400100.microseconds, diff)
assertFailsWith<DateTimeArithmeticException> { (Instant.MAX - 3.days).toLocalDateTime(TimeZone.UTC) }
assertFailsWith<DateTimeArithmeticException> { (Instant.MIN + 6.hours).toLocalDateTime(TimeZone.UTC) }
}

@OptIn(ExperimentalTime::class)
Expand All @@ -57,6 +62,7 @@ class LocalDateTimeTest {
val instant = Instant.parse("2019-10-01T18:43:15.100500Z")
val datetime = instant.toLocalDateTime(TimeZone.UTC)
checkComponents(datetime, 2019, 10, 1, 18, 43, 15, 100500000)
assertFailsWith<DateTimeArithmeticException> { Instant.MAX.toLocalDateTime(TimeZone.UTC) }
}

@Test
Expand Down Expand Up @@ -99,14 +105,14 @@ class LocalDateTimeTest {
fun localTime(hour: Int, minute: Int, second: Int = 0, nanosecond: Int = 0): LocalDateTime =
LocalDateTime(2020, 1, 1, hour, minute, second, nanosecond)
localTime(23, 59)
assertFailsWith<Throwable> { localTime(-1, 0) }
assertFailsWith<Throwable> { localTime(24, 0) }
assertFailsWith<Throwable> { localTime(0, -1) }
assertFailsWith<Throwable> { localTime(0, 60) }
assertFailsWith<Throwable> { localTime(0, 0, -1) }
assertFailsWith<Throwable> { localTime(0, 0, 60) }
assertFailsWith<Throwable> { localTime(0, 0, 0, -1) }
assertFailsWith<Throwable> { localTime(0, 0, 0, 1_000_000_000) }
assertFailsWith<IllegalArgumentException> { localTime(-1, 0) }
assertFailsWith<IllegalArgumentException> { localTime(24, 0) }
assertFailsWith<IllegalArgumentException> { localTime(0, -1) }
assertFailsWith<IllegalArgumentException> { localTime(0, 60) }
assertFailsWith<IllegalArgumentException> { localTime(0, 0, -1) }
assertFailsWith<IllegalArgumentException> { localTime(0, 0, 60) }
assertFailsWith<IllegalArgumentException> { localTime(0, 0, 0, -1) }
assertFailsWith<IllegalArgumentException> { localTime(0, 0, 0, 1_000_000_000) }
}

}
Expand Down
4 changes: 2 additions & 2 deletions core/commonTest/src/TimeZoneTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@ class TimeZoneTest {
assertEquals("Europe/Moscow", tzm.id)
// TODO: Check known offsets from UTC for particular moments

// TODO: assert exception type?
assertFails { TimeZone.of("Mars/Standard") }
assertFailsWith<IllegalTimeZoneException> { TimeZone.of("Mars/Standard") }
assertFailsWith<IllegalTimeZoneException> { TimeZone.of("UTC+X") }

}

Expand Down
3 changes: 3 additions & 0 deletions core/jsMain/src/Instant.kt
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ public actual class Instant internal constructor(internal val value: jtInstant)

actual fun fromEpochSeconds(epochSeconds: Long, nanosecondAdjustment: Long): Instant =
Instant(jtInstant.ofEpochSecond(epochSeconds, nanosecondAdjustment))

internal actual val MIN: Instant = Instant(jtInstant.MIN)
internal actual val MAX: Instant = Instant(jtInstant.MAX)
}
}

Expand Down
3 changes: 3 additions & 0 deletions core/jsMain/src/LocalDate.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ public actual class LocalDate internal constructor(internal val value: jtLocalDa
public actual fun parse(isoString: String): LocalDate {
return jtLocalDate.parse(isoString).let(::LocalDate)
}

internal actual val MIN: LocalDate = LocalDate(jtLocalDate.MIN)
internal actual val MAX: LocalDate = LocalDate(jtLocalDate.MAX)
}

public actual constructor(year: Int, monthNumber: Int, dayOfMonth: Int) :
Expand Down
3 changes: 3 additions & 0 deletions core/jsMain/src/LocalDateTime.kt
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ public actual class LocalDateTime internal constructor(internal val value: jtLoc
public actual fun parse(isoString: String): LocalDateTime {
return jtLocalDateTime.parse(isoString).let(::LocalDateTime)
}

internal actual val MIN: LocalDateTime = LocalDateTime(jtLocalDateTime.MIN)
internal actual val MAX: LocalDateTime = LocalDateTime(jtLocalDateTime.MAX)
}

}
Expand Down
3 changes: 3 additions & 0 deletions core/jvmMain/src/Instant.kt
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ public actual class Instant internal constructor(internal val value: jtInstant)

actual fun fromEpochSeconds(epochSeconds: Long, nanosecondAdjustment: Long): Instant =
Instant(jtInstant.ofEpochSecond(epochSeconds, nanosecondAdjustment))

internal actual val MIN: Instant = Instant(jtInstant.MIN)
internal actual val MAX: Instant = Instant(jtInstant.MAX)
}
}

Expand Down
Loading