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