1 /* 2 * Copyright 2023 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package androidx.glance.session 18 19 import com.google.common.truth.Truth.assertThat 20 import java.util.concurrent.atomic.AtomicBoolean 21 import kotlin.time.Duration 22 import kotlin.time.Duration.Companion.days 23 import kotlin.time.Duration.Companion.milliseconds 24 import kotlin.time.Duration.Companion.seconds 25 import kotlinx.coroutines.ExperimentalCoroutinesApi 26 import kotlinx.coroutines.Job 27 import kotlinx.coroutines.async 28 import kotlinx.coroutines.delay 29 import kotlinx.coroutines.launch 30 import kotlinx.coroutines.test.TestScope 31 import kotlinx.coroutines.test.currentTime 32 import kotlinx.coroutines.test.runTest 33 import org.junit.Test 34 import org.junit.runner.RunWith 35 import org.robolectric.RobolectricTestRunner 36 37 @OptIn(ExperimentalCoroutinesApi::class) 38 @RunWith(RobolectricTestRunner::class) 39 class WithTimerTest { 40 private val TestScope.timeSource <lambda>null41 get() = TimeSource { currentTime } 42 43 @Test <lambda>null44 fun completingNormallyThrowsNoException() = runTest { 45 val result = async { 46 withTimerOrNull(timeSource) { 47 startTimer(200.milliseconds) 48 delay(100.milliseconds) 49 Any() 50 } 51 } 52 assertThat(result.await()).isNotNull() 53 } 54 55 @Test <lambda>null56 fun completingNormallyDoesNotCancelParentJob() = runTest { 57 val timerSuccess = Job() 58 val parentJob = launch { 59 withTimer(timeSource) { 60 startTimer(200.milliseconds) 61 delay(100.milliseconds) 62 } 63 timerSuccess.complete() 64 delay(Duration.INFINITE) 65 } 66 timerSuccess.join() 67 assertThat(timerSuccess.isCompleted).isTrue() 68 assertThat(parentJob.isActive).isTrue() 69 parentJob.cancel() 70 } 71 72 @Test <lambda>null73 fun timeoutThrowsTimeoutCancellationException() = runTest { 74 var expected: TimeoutCancellationException? = null 75 try { 76 withTimer(timeSource) { 77 startTimer(200.milliseconds) 78 delay(1.seconds) 79 } 80 } catch (e: TimeoutCancellationException) { 81 expected = e 82 } finally { 83 assertThat(expected).isNotNull() 84 } 85 } 86 87 @Test <lambda>null88 fun doesNotTimeoutBeforeStartTimer() = runTest { 89 var unexpected: TimeoutCancellationException? = null 90 try { 91 withTimer(timeSource) { 92 delay(30.days) 93 startTimer(200.milliseconds) 94 delay(100.milliseconds) 95 } 96 } catch (e: TimeoutCancellationException) { 97 unexpected = e 98 } finally { 99 assertThat(unexpected).isNull() 100 } 101 } 102 103 @Test <lambda>null104 fun addTime() = runTest { 105 var unexpected: TimeoutCancellationException? = null 106 try { 107 withTimer(timeSource) { 108 startTimer(200.milliseconds) 109 delay(100) 110 assertThat(timeLeft).isEqualTo(100.milliseconds) 111 addTime(100.milliseconds) 112 assertThat(timeLeft).isEqualTo(200.milliseconds) 113 delay(199.milliseconds) 114 } 115 } catch (e: TimeoutCancellationException) { 116 unexpected = e 117 } finally { 118 assertThat(unexpected).isNull() 119 } 120 } 121 122 @Test <lambda>null123 fun addTimeBeforeStartTimer() = runTest { 124 var expected: IllegalStateException? = null 125 try { 126 withTimer(timeSource) { addTime(100.milliseconds) } 127 } catch (e: IllegalStateException) { 128 expected = e 129 } finally { 130 assertThat(expected).isNotNull() 131 } 132 } 133 134 @Test <lambda>null135 fun addNegativeDuration() = runTest { 136 var expected: IllegalArgumentException? = null 137 try { 138 withTimer(timeSource) { 139 startTimer(100.milliseconds) 140 addTime(-100.milliseconds) 141 } 142 } catch (e: IllegalArgumentException) { 143 expected = e 144 } finally { 145 assertThat(expected).isNotNull() 146 } 147 } 148 149 @Test <lambda>null150 fun nestedWithTimerOrNull() = runTest { 151 val checkpoint1 = AtomicBoolean(false) 152 val checkpoint2 = AtomicBoolean(false) 153 // The remaining two checkpoints should never be hit 154 val checkpoint3 = AtomicBoolean(false) 155 val checkpoint4 = AtomicBoolean(false) 156 157 val result = 158 withTimerOrNull(timeSource) { 159 // The outer timer should trigger during the delay(), and nothing after that should 160 // run. 161 startTimer(200.milliseconds) 162 checkpoint1.set(true) 163 withTimerOrNull(timeSource) { 164 startTimer(1.seconds) 165 checkpoint2.set(true) 166 delay(201.milliseconds) 167 checkpoint3.set(true) 168 } 169 checkpoint4.set(true) 170 } 171 172 assertThat(result).isNull() 173 assertThat(checkpoint1.get()).isTrue() 174 assertThat(checkpoint2.get()).isTrue() 175 assertThat(checkpoint3.get()).isFalse() 176 assertThat(checkpoint4.get()).isFalse() 177 } 178 } 179