v2 / vlib / x / async / timeout_test.v
295 lines · 267 sloc · 6.83 KB · 15fb60b77ea6073658aa8355b247f2e1ae03b714
Raw
1import context
2import sync
3import time
4import x.async as xasync
5
6@[heap]
7struct ControlledDeadlineParent {
8 deadline_at time.Time
9mut:
10 done_ch chan int
11 mutex &sync.Mutex = sync.new_mutex()
12 err_value IError = none
13}
14
15fn new_controlled_deadline_parent(deadline_at time.Time) &ControlledDeadlineParent {
16 return &ControlledDeadlineParent{
17 deadline_at: deadline_at
18 done_ch: chan int{}
19 mutex: sync.new_mutex()
20 }
21}
22
23fn (ctx &ControlledDeadlineParent) deadline() ?time.Time {
24 return ctx.deadline_at
25}
26
27fn (ctx &ControlledDeadlineParent) value(key context.Key) ?context.Any {
28 _ = key
29 return none
30}
31
32fn (mut ctx ControlledDeadlineParent) done() chan int {
33 ctx.mutex.lock()
34 done := ctx.done_ch
35 ctx.mutex.unlock()
36 return done
37}
38
39fn (mut ctx ControlledDeadlineParent) err() IError {
40 ctx.mutex.lock()
41 err := ctx.err_value
42 ctx.mutex.unlock()
43 return err
44}
45
46fn (mut ctx ControlledDeadlineParent) close_with_error(err IError) {
47 ctx.mutex.lock()
48 ctx.err_value = err
49 if !ctx.done_ch.closed {
50 ctx.done_ch.close()
51 }
52 ctx.mutex.unlock()
53}
54
55fn context_error_or_closed_without_error(mut ctx context.Context) ! {
56 err := ctx.err()
57 if err !is none {
58 return err
59 }
60 return error('context closed without error')
61}
62
63fn test_with_timeout_returns_timeout_error() {
64 xasync.with_timeout(5 * time.millisecond, fn (mut ctx context.Context) ! {
65 _ = ctx
66 time.sleep(50 * time.millisecond)
67 }) or {
68 assert err.msg() == 'async: timeout'
69 return
70 }
71 assert false
72}
73
74fn test_with_timeout_returns_without_waiting_for_ignored_cancellation() {
75 stopwatch := time.new_stopwatch()
76 xasync.with_timeout(10 * time.millisecond, fn (mut ctx context.Context) ! {
77 _ = ctx
78 time.sleep(250 * time.millisecond)
79 }) or {
80 assert err.msg() == 'async: timeout'
81 assert stopwatch.elapsed() < 150 * time.millisecond
82 return
83 }
84 assert false
85}
86
87fn test_with_timeout_zero_duration_expires_immediately() {
88 xasync.with_timeout(0 * time.millisecond, fn (mut ctx context.Context) ! {
89 done := ctx.done()
90 select {
91 _ := <-done {
92 return context_error_or_closed_without_error(mut ctx)
93 }
94 1 * time.second {
95 return
96 }
97 }
98 }) or {
99 assert err.msg() == 'async: timeout'
100 return
101 }
102 assert false
103}
104
105fn test_with_timeout_negative_duration_is_already_expired() {
106 xasync.with_timeout(-1 * time.millisecond, fn (mut ctx context.Context) ! {
107 done := ctx.done()
108 select {
109 _ := <-done {
110 return context_error_or_closed_without_error(mut ctx)
111 }
112 1 * time.second {
113 return
114 }
115 }
116 }) or {
117 assert err.msg() == 'async: timeout'
118 return
119 }
120 assert false
121}
122
123fn test_with_timeout_success_before_timeout() {
124 seen := chan int{cap: 1}
125 xasync.with_timeout(1 * time.second, fn [seen] (mut ctx context.Context) ! {
126 _ = ctx
127 seen <- 7
128 })!
129 assert <-seen == 7
130}
131
132fn test_with_timeout_returns_job_error() {
133 xasync.with_timeout(1 * time.second, fn (mut ctx context.Context) ! {
134 _ = ctx
135 return error('job failed')
136 }) or {
137 assert err.msg() == 'job failed'
138 return
139 }
140 assert false
141}
142
143fn test_with_timeout_preserves_job_error_matching_context_message_before_timeout() {
144 xasync.with_timeout(1 * time.second, fn (mut ctx context.Context) ! {
145 _ = ctx
146 return error('context deadline exceeded')
147 }) or {
148 assert err.msg() == 'context deadline exceeded'
149 return
150 }
151 assert false
152}
153
154fn test_with_timeout_refuses_nil_job() {
155 nil_job := unsafe { xasync.JobFn(nil) }
156 xasync.with_timeout(1 * time.second, nil_job) or {
157 assert err.msg() == 'async: job function is nil'
158 return
159 }
160 assert false
161}
162
163fn test_with_timeout_returns_timeout_for_cooperative_job() {
164 mut timed_out := false
165 xasync.with_timeout(50 * time.millisecond, fn (mut ctx context.Context) ! {
166 done := ctx.done()
167 select {
168 _ := <-done {
169 return context_error_or_closed_without_error(mut ctx)
170 }
171 1 * time.second {
172 return error('cooperative timeout job was not canceled')
173 }
174 }
175 }) or {
176 assert err.msg() == 'async: timeout'
177 timed_out = true
178 }
179 assert timed_out
180}
181
182fn test_with_timeout_context_uses_parent_context() {
183 mut parent_ctx, cancel := xasync.with_cancel()
184 cancel()
185 xasync.with_timeout_context(parent_ctx, 1 * time.second, fn (mut ctx context.Context) ! {
186 done := ctx.done()
187 select {
188 _ := <-done {
189 return context_error_or_closed_without_error(mut ctx)
190 }
191 1 * time.second {}
192 }
193 }) or {
194 assert err.msg() == 'context canceled'
195 return
196 }
197 assert false
198}
199
200fn test_with_timeout_context_preserves_already_expired_parent_deadline() {
201 mut background := context.background()
202 parent_ctx, parent_cancel := context.with_timeout(mut background, -1 * time.millisecond)
203 defer {
204 parent_cancel()
205 }
206
207 xasync.with_timeout_context(parent_ctx, 1 * time.second, fn (mut ctx context.Context) ! {
208 _ = ctx
209 return error('job should not run with an already expired parent')
210 }) or {
211 assert err.msg() == 'context deadline exceeded'
212 return
213 }
214 assert false
215}
216
217fn test_with_timeout_context_preserves_parent_deadline_that_expires_first() {
218 mut background := context.background()
219 parent_ctx, parent_cancel := context.with_timeout(mut background, 10 * time.millisecond)
220 defer {
221 parent_cancel()
222 }
223
224 xasync.with_timeout_context(parent_ctx, 1 * time.second, fn (mut ctx context.Context) ! {
225 _ = ctx
226 time.sleep(100 * time.millisecond)
227 }) or {
228 assert err.msg() == 'context deadline exceeded'
229 return
230 }
231 assert false
232}
233
234fn test_with_timeout_context_waits_for_controlled_parent_done_and_returns_exact_parent_error() {
235 mut parent_ctx := new_controlled_deadline_parent(time.now().add(-1 * time.second))
236 started := chan bool{cap: 1}
237 release := chan bool{cap: 1}
238 result := chan string{cap: 1}
239
240 caller := spawn fn [mut parent_ctx, started, release, result] () {
241 xasync.with_timeout_context(context.Context(parent_ctx), 1 * time.second, fn [started, release] (mut ctx context.Context) ! {
242 _ = ctx
243 started <- true
244 _ := <-release
245 }) or {
246 result <- err.msg()
247 return
248 }
249 result <- 'ok'
250 }()
251
252 select {
253 did_start := <-started {
254 assert did_start
255 }
256 1 * time.second {
257 assert false, 'with_timeout_context returned before starting the job for a pending parent'
258 }
259 }
260 select {
261 msg := <-result {
262 assert false, 'with_timeout_context returned before parent done closed: ${msg}'
263 }
264 else {}
265 }
266
267 parent_ctx.close_with_error(error('controlled parent deadline'))
268 select {
269 msg := <-result {
270 assert msg == 'controlled parent deadline'
271 }
272 1 * time.second {
273 assert false, 'with_timeout_context did not return after controlled parent closed'
274 }
275 }
276 release <- true
277 caller.wait()
278}
279
280fn test_with_timeout_context_uses_own_timeout_before_parent_deadline() {
281 mut background := context.background()
282 parent_ctx, parent_cancel := context.with_timeout(mut background, 1 * time.second)
283 defer {
284 parent_cancel()
285 }
286
287 xasync.with_timeout_context(parent_ctx, 10 * time.millisecond, fn (mut ctx context.Context) ! {
288 _ = ctx
289 time.sleep(100 * time.millisecond)
290 }) or {
291 assert err.msg() == 'async: timeout'
292 return
293 }
294 assert false
295}
296