-
Notifications
You must be signed in to change notification settings - Fork 320
Generate test fixture dates that are serializable and in the future #6653
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
base: develop
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| /* | ||
| * Copyright (c) 2014-2026 Stream.io Inc. All rights reserved. | ||
| * | ||
| * Licensed under the Stream License; | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * https://github.com/GetStream/stream-chat-android/blob/main/LICENSE | ||
| * | ||
| * 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. | ||
| */ | ||
|
|
||
| package io.getstream.chat.android.client.internal.offline.repository.database.converter | ||
|
|
||
| import androidx.test.ext.junit.runners.AndroidJUnit4 | ||
| import io.getstream.chat.android.client.internal.offline.repository.database.converter.internal.ReminderInfoConverter | ||
| import io.getstream.chat.android.client.internal.offline.repository.domain.message.internal.ReminderInfoEntity | ||
| import io.getstream.chat.android.randomDate | ||
| import io.getstream.chat.android.randomDateOrNull | ||
| import org.amshove.kluent.shouldBeEqualTo | ||
| import org.junit.Test | ||
| import org.junit.runner.RunWith | ||
| import java.util.Date | ||
|
|
||
| /** | ||
| * Runs under Robolectric because the dates are serialised by the API 26+ formatter, which is stricter about the year | ||
| * than the [java.text.SimpleDateFormat] fallback used below it. | ||
| */ | ||
| @RunWith(AndroidJUnit4::class) | ||
| internal class ReminderInfoConverterTest { | ||
|
|
||
| private val converter = ReminderInfoConverter() | ||
|
|
||
| @Test | ||
| fun `a reminder built from the date fixtures survives the round trip`() { | ||
| repeat(ROUNDS) { | ||
| val reminder = ReminderInfoEntity( | ||
| remindAt = randomDateOrNull(), | ||
| createdAt = randomDate(), | ||
| updatedAt = randomDate(), | ||
| ) | ||
|
|
||
| converter.stringToReminderInfo(converter.reminderInfoToString(reminder)) shouldBeEqualTo reminder | ||
| } | ||
| } | ||
|
|
||
| @Test | ||
| fun `a reminder at the last serializable instant survives the round trip`() { | ||
| // Guards the upper bound the date fixtures generate up to: past it the year no longer fits the wire format, | ||
| // and a reminder comes back holding a different date, or fails to be read back at all. | ||
| val lastSerializable = Date(LAST_SERIALIZABLE_DATE_MILLIS) | ||
| val reminder = ReminderInfoEntity( | ||
| remindAt = lastSerializable, | ||
| createdAt = lastSerializable, | ||
| updatedAt = lastSerializable, | ||
| ) | ||
|
|
||
| converter.stringToReminderInfo(converter.reminderInfoToString(reminder)) shouldBeEqualTo reminder | ||
| } | ||
|
|
||
| private companion object { | ||
| // Enough draws that a fixture generating unserializable dates again fails here, rather than | ||
| // intermittently somewhere downstream. | ||
| const val ROUNDS = 5_000 | ||
| const val LAST_SERIALIZABLE_DATE_MILLIS = 253_402_300_799_999L // 9999-12-31T23:59:59.999Z | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| /* | ||
| * Copyright (c) 2014-2026 Stream.io Inc. All rights reserved. | ||
| * | ||
| * Licensed under the Stream License; | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * https://github.com/GetStream/stream-chat-android/blob/main/LICENSE | ||
| * | ||
| * 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. | ||
| */ | ||
|
|
||
| package io.getstream.chat.android | ||
|
|
||
| import org.amshove.kluent.shouldBeNull | ||
| import org.junit.jupiter.api.Test | ||
| import java.util.Date | ||
|
|
||
| /** | ||
| * The two properties the rest of the suite leans on. Both have broken before, and because the generator is random, | ||
| * breaking either shows up as an unrelated test failing once in a few hundred CI runs rather than as a failure here. | ||
| */ | ||
| internal class RandomDateTest { | ||
|
|
||
| @Test | ||
| fun `a generated date is always in the future`() { | ||
| // Production code that keeps only future dates, live locations for one, drops a fixture date that is already | ||
| // in the past, leaving the test asserting on an empty result. | ||
| val now = Date() | ||
|
|
||
| val past = List(ROUNDS) { randomDate() }.filter { it.before(now) } | ||
|
|
||
| past.firstOrNull().shouldBeNull() | ||
| } | ||
|
|
||
| @Test | ||
| fun `a generated date is always serializable`() { | ||
| // The ISO-8601 formatter writes a four digit year, so a later date is written as a different date, or as one | ||
| // that cannot be read back at all. | ||
| val lastSerializable = Date(LAST_SERIALIZABLE_DATE_MILLIS) | ||
|
|
||
| val unserializable = List(ROUNDS) { randomDate() }.filter { it.after(lastSerializable) } | ||
|
|
||
| unserializable.firstOrNull().shouldBeNull() | ||
| } | ||
|
|
||
| @Test | ||
| fun `a date generated after another one is serializable too`() { | ||
| val lastSerializable = Date(LAST_SERIALIZABLE_DATE_MILLIS) | ||
|
|
||
| val unserializable = List(ROUNDS) { randomDateAfter(randomDate()) }.filter { it.after(lastSerializable) } | ||
|
|
||
| unserializable.firstOrNull().shouldBeNull() | ||
| } | ||
|
|
||
| private companion object { | ||
| const val ROUNDS = 100_000 | ||
| const val LAST_SERIALIZABLE_DATE_MILLIS = 253_402_300_799_999L // 9999-12-31T23:59:59.999Z | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -773,10 +773,23 @@ public fun randomFiles( | |
| creationFunction: (Int) -> File = { randomFile() }, | ||
| ): List<File> = (1..size).map(creationFunction) | ||
|
|
||
| /** | ||
| * The window generated dates are drawn from. | ||
| * | ||
| * The upper bound is the last instant the SDK can serialise: the ISO-8601 formatter writes a four digit year, so | ||
| * anything past year 9999 is written as a different date, or as one that cannot be read back at all. | ||
| * | ||
| * The lower bound keeps generated dates in the future, which is what callers already relied on. Drawing from the whole | ||
| * positive `Long` range put every date millions of years ahead, so production code that keeps only future dates, live | ||
| * locations for one, never saw a generated date fall behind the clock. Use [randomDateBefore] for a date in the past. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Small thing: "Use randomDateBefore for a date in the past" only works if you pass Date(). randomDateBefore(randomDate()) gives a year 3000 or later date now, and randomDateBefore(Date()) only goes back about 25 days, since it subtracts at most Int.MAX_VALUE - 1 ms. Maybe write it as randomDateBefore(Date())? Not blocking. |
||
| */ | ||
| private const val MIN_GENERATED_DATE_MILLIS: Long = 32_503_680_000_000L // 3000-01-01T00:00:00.000Z | ||
| private const val MAX_GENERATED_DATE_MILLIS: Long = 253_402_300_799_999L // 9999-12-31T23:59:59.999Z | ||
|
|
||
| public fun randomDateOrNull(): Date? = randomDate().takeIf { randomBoolean() } | ||
| public fun randomDate(): Date = Date(positiveRandomLong()) | ||
| public fun randomDate(): Date = Date(randomLongBetween(MIN_GENERATED_DATE_MILLIS, MAX_GENERATED_DATE_MILLIS)) | ||
| public fun randomDateBefore(date: Date): Date = Date(date.time - positiveRandomInt()) | ||
| public fun randomDateAfter(date: Date): Date = Date(randomLongBetween(date.time)) | ||
| public fun randomDateAfter(date: Date): Date = Date(randomLongBetween(date.time, MAX_GENERATED_DATE_MILLIS)) | ||
|
|
||
| public fun createDate( | ||
| year: Int = positiveRandomInt(), | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.