| 1 | import context |
| 2 | import sync |
| 3 | import time |
| 4 | import x.async as xasync |
| 5 | |
| 6 | @[heap] |
| 7 | struct ControlledDeadlineParent { |
| 8 | deadline_at time.Time |
| 9 | mut: |
| 10 | done_ch chan int |
| 11 | mutex &sync.Mutex = sync.new_mutex() |
| 12 | err_value IError = none |
| 13 | } |
| 14 | |
| 15 | fn 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 | |
| 23 | fn (ctx &ControlledDeadlineParent) deadline() ?time.Time { |
| 24 | return ctx.deadline_at |
| 25 | } |
| 26 | |
| 27 | fn (ctx &ControlledDeadlineParent) value(key context.Key) ?context.Any { |
| 28 | _ = key |
| 29 | return none |
| 30 | } |
| 31 | |
| 32 | fn (mut ctx ControlledDeadlineParent) done() chan int { |
| 33 | ctx.mutex.lock() |
| 34 | done := ctx.done_ch |
| 35 | ctx.mutex.unlock() |
| 36 | return done |
| 37 | } |
| 38 | |
| 39 | fn (mut ctx ControlledDeadlineParent) err() IError { |
| 40 | ctx.mutex.lock() |
| 41 | err := ctx.err_value |
| 42 | ctx.mutex.unlock() |
| 43 | return err |
| 44 | } |
| 45 | |
| 46 | fn (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 | |
| 55 | fn 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 | |
| 63 | fn 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 | |
| 74 | fn 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 | |
| 87 | fn 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 | |
| 105 | fn 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 | |
| 123 | fn 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 | |
| 132 | fn 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 | |
| 143 | fn 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 | |
| 154 | fn 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 | |
| 163 | fn 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 | |
| 182 | fn 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 | |
| 200 | fn 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 | |
| 217 | fn 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 | |
| 234 | fn 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 | |
| 280 | fn 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 | |