synctest 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Synctest
4
+ class Error < StandardError
5
+ end
6
+
7
+ class NotInBubbleError < Error
8
+ end
9
+
10
+ class NestedBubbleError < Error
11
+ end
12
+
13
+ class ConcurrentWaitError < Error
14
+ end
15
+
16
+ class DeadlockError < Error
17
+ end
18
+
19
+ class StalledError < Error
20
+ end
21
+
22
+ class IsolationError < Error
23
+ end
24
+
25
+ class UnsupportedOperationError < Error
26
+ end
27
+ end
@@ -0,0 +1,608 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Synctest
4
+ # Process-wide shims which dispatch to a thread-local bubble. Outside a
5
+ # bubble, every method immediately delegates to Ruby's original behavior.
6
+ module Patches
7
+ module KernelPatch
8
+ def sleep(duration = nil)
9
+ bubble = Synctest.current_bubble
10
+ return super unless bubble
11
+
12
+ bubble.sleep(duration)
13
+ end
14
+ end
15
+
16
+ module KernelSingletonPatch
17
+ def sleep(duration = nil)
18
+ bubble = Synctest.current_bubble
19
+ return super unless bubble
20
+
21
+ bubble.sleep(duration)
22
+ end
23
+ end
24
+
25
+ module TimeSingletonPatch
26
+ def now(**keywords)
27
+ bubble = Synctest.current_bubble
28
+ return super unless bubble
29
+ return super unless keywords.empty? || keywords.keys == [:in]
30
+
31
+ value = bubble.wall_time
32
+ zone = keywords[:in]
33
+ zone ? value.getlocal(zone) : value
34
+ end
35
+
36
+ def new(*arguments, **keywords)
37
+ bubble = Synctest.current_bubble
38
+ if bubble && arguments.empty? && (keywords.empty? || keywords.keys == [:in])
39
+ value = bubble.wall_time
40
+ zone = keywords[:in]
41
+ return zone ? value.getlocal(zone) : value
42
+ end
43
+
44
+ super
45
+ end
46
+ end
47
+
48
+ module ProcessSingletonPatch
49
+ def clock_gettime(clock_id, unit = :float_second)
50
+ bubble = Synctest.current_bubble
51
+ return super unless bubble
52
+
53
+ case clock_id
54
+ when Process::CLOCK_MONOTONIC
55
+ bubble.monotonic_time(unit)
56
+ when Process::CLOCK_REALTIME
57
+ bubble.realtime(unit)
58
+ else
59
+ super
60
+ end
61
+ end
62
+ end
63
+
64
+ module ThreadSingletonPatch
65
+ def new(*arguments, **keywords, &block)
66
+ bubble = Synctest.current_bubble
67
+ reserved = false
68
+ return super unless bubble && block
69
+
70
+ bubble.reserve_child
71
+ reserved = true
72
+ created = false
73
+ thread = super do |*block_arguments, **block_keywords|
74
+ Synctest.set_current_bubble(bubble)
75
+ bubble.attach_child(Thread.current)
76
+ Thread.current.report_on_exception = false
77
+ error = nil
78
+
79
+ begin
80
+ block.call(*block_arguments, **block_keywords)
81
+ rescue Exception => exception # rubocop:disable Lint/RescueException
82
+ error = exception
83
+ raise
84
+ ensure
85
+ bubble.child_finished(Thread.current, error)
86
+ Synctest.set_current_bubble(nil)
87
+ end
88
+ end
89
+ created = true
90
+ thread.instance_variable_set(Synctest::OBJECT_BUBBLE_IVAR, bubble)
91
+ bubble.await_child_attachment(thread)
92
+ thread
93
+ ensure
94
+ bubble&.cancel_child_reservation if reserved && !created
95
+ end
96
+
97
+ def start(*arguments, **keywords, &block)
98
+ return super unless Synctest.current_bubble
99
+
100
+ new(*arguments, **keywords, &block)
101
+ end
102
+
103
+ def fork(*arguments, **keywords, &block)
104
+ return super unless Synctest.current_bubble
105
+
106
+ new(*arguments, **keywords, &block)
107
+ end
108
+
109
+ def stop
110
+ bubble = Synctest.current_bubble
111
+ return super unless bubble
112
+
113
+ bubble.stop
114
+ end
115
+
116
+ def kill(thread)
117
+ return super unless thread.is_a?(Thread)
118
+
119
+ owner = thread.thread_variable_get(Synctest::THREAD_BUBBLE_KEY) || Synctest.bubble_for(thread)
120
+ return super unless owner
121
+
122
+ thread.kill
123
+ end
124
+ end
125
+
126
+ module ThreadPatch
127
+ def join(timeout = nil)
128
+ owner = Synctest.bubble_for(self)
129
+ bubble = owner ? Synctest.bubble_for_operation(self) : root_bubble_for_join
130
+ return super unless bubble
131
+
132
+ bubble.join(self, timeout)
133
+ end
134
+
135
+ def value
136
+ owner = Synctest.bubble_for(self)
137
+ bubble = owner ? Synctest.bubble_for_operation(self) : root_bubble_for_join
138
+ return super unless bubble
139
+ unless owner
140
+ if equal?(Thread.current)
141
+ Kernel.raise ThreadError, "Target thread must not be current thread"
142
+ end
143
+
144
+ Kernel.raise UnsupportedOperationError, "Thread#value cannot target the root of its own Synctest bubble"
145
+ end
146
+
147
+ bubble.join(self, nil)
148
+ Synctest::Raw::THREAD_VALUE.bind_call(self)
149
+ end
150
+
151
+ def wakeup
152
+ bubble = Synctest.bubble_for_thread_operation(self)
153
+ return super unless bubble
154
+ return self if bubble.wake_thread(self)
155
+
156
+ super
157
+ end
158
+
159
+ def run
160
+ bubble = Synctest.bubble_for_thread_operation(self)
161
+ return super unless bubble
162
+ return self if bubble.wake_thread(self)
163
+
164
+ super
165
+ end
166
+
167
+ def kill
168
+ if thread_variable_get(Synctest::THREAD_BUBBLE_KEY) || Synctest.bubble_for(self)
169
+ bubble = Synctest.bubble_for_thread_operation(self)
170
+ bubble.interrupt_thread(self)
171
+ end
172
+ super
173
+ end
174
+
175
+ def terminate
176
+ if thread_variable_get(Synctest::THREAD_BUBBLE_KEY) || Synctest.bubble_for(self)
177
+ bubble = Synctest.bubble_for_thread_operation(self)
178
+ bubble.interrupt_thread(self)
179
+ end
180
+ super
181
+ end
182
+
183
+ def exit
184
+ if thread_variable_get(Synctest::THREAD_BUBBLE_KEY) || Synctest.bubble_for(self)
185
+ bubble = Synctest.bubble_for_thread_operation(self)
186
+ bubble.interrupt_thread(self)
187
+ end
188
+ super
189
+ end
190
+
191
+ def raise(*arguments)
192
+ if thread_variable_get(Synctest::THREAD_BUBBLE_KEY) || Synctest.bubble_for(self)
193
+ bubble = Synctest.bubble_for_thread_operation(self)
194
+ bubble.interrupt_thread(self)
195
+ end
196
+ super
197
+ end
198
+
199
+ private
200
+
201
+ def root_bubble_for_join
202
+ current = Synctest.current_bubble
203
+ owner = thread_variable_get(Synctest::THREAD_BUBBLE_KEY)
204
+ return unless owner&.active?
205
+ return owner if owner.equal?(current)
206
+
207
+ Kernel.raise IsolationError, "Thread belongs to a different Synctest bubble" if current
208
+ end
209
+ end
210
+
211
+ module MutexPatch
212
+ def initialize(...)
213
+ super
214
+ Synctest.associate(self)
215
+ end
216
+
217
+ def lock
218
+ bubble = Synctest.bubble_for_operation(self)
219
+ return super unless bubble
220
+
221
+ bubble.mutex_lock(self)
222
+ end
223
+
224
+ def try_lock
225
+ bubble = Synctest.bubble_for_operation(self)
226
+ return super unless bubble
227
+
228
+ bubble.mutex_try_lock(self)
229
+ end
230
+
231
+ def unlock
232
+ bubble = Synctest.bubble_for_operation(self)
233
+ return super unless bubble
234
+
235
+ bubble.mutex_unlock(self)
236
+ end
237
+
238
+ def owned?
239
+ bubble = Synctest.bubble_for_operation(self)
240
+ return super unless bubble
241
+
242
+ bubble.mutex_owned?(self)
243
+ end
244
+
245
+ def synchronize
246
+ bubble = Synctest.bubble_for_operation(self)
247
+ return super unless bubble
248
+
249
+ locked = false
250
+ begin
251
+ bubble.mutex_lock(self)
252
+ locked = true
253
+ yield
254
+ ensure
255
+ bubble.mutex_unlock(self) if locked
256
+ end
257
+ end
258
+
259
+ def sleep(timeout = nil)
260
+ bubble = Synctest.bubble_for_operation(self)
261
+ return super unless bubble
262
+
263
+ bubble.mutex_sleep(self, timeout)
264
+ end
265
+ end
266
+
267
+ module ConditionVariablePatch
268
+ def initialize(...)
269
+ super
270
+ Synctest.associate(self)
271
+ end
272
+
273
+ def wait(mutex, timeout = nil)
274
+ bubble = Synctest.bubble_for_operation(self)
275
+ return super unless bubble
276
+ unless Synctest.bubble_for(mutex).equal?(bubble)
277
+ raise IsolationError, "a bubbled ConditionVariable requires a Mutex from the same bubble"
278
+ end
279
+
280
+ bubble.condition_wait(self, mutex, timeout)
281
+ end
282
+
283
+ def signal
284
+ bubble = Synctest.bubble_for_operation(self)
285
+ return super unless bubble
286
+
287
+ bubble.condition_signal(self, broadcast: false)
288
+ end
289
+
290
+ def broadcast
291
+ bubble = Synctest.bubble_for_operation(self)
292
+ return super unless bubble
293
+
294
+ bubble.condition_signal(self, broadcast: true)
295
+ end
296
+ end
297
+
298
+ module MonitorPatch
299
+ def initialize(...)
300
+ super
301
+ Synctest.associate(self)
302
+ end
303
+
304
+ def enter
305
+ bubble = Synctest.bubble_for_operation(self)
306
+ return super unless bubble
307
+
308
+ bubble.monitor_enter(self)
309
+ nil
310
+ end
311
+
312
+ def mon_enter
313
+ bubble = Synctest.bubble_for_operation(self)
314
+ return super unless bubble
315
+
316
+ bubble.monitor_enter(self)
317
+ nil
318
+ end
319
+
320
+ def try_enter
321
+ bubble = Synctest.bubble_for_operation(self)
322
+ return super unless bubble
323
+
324
+ bubble.monitor_try_enter(self)
325
+ end
326
+
327
+ def mon_try_enter
328
+ bubble = Synctest.bubble_for_operation(self)
329
+ return super unless bubble
330
+
331
+ bubble.monitor_try_enter(self)
332
+ end
333
+
334
+ def try_mon_enter
335
+ bubble = Synctest.bubble_for_operation(self)
336
+ return super unless bubble
337
+
338
+ bubble.monitor_try_enter(self)
339
+ end
340
+
341
+ def exit
342
+ bubble = Synctest.bubble_for_operation(self)
343
+ return super unless bubble
344
+
345
+ bubble.monitor_exit(self)
346
+ nil
347
+ end
348
+
349
+ def mon_exit
350
+ bubble = Synctest.bubble_for_operation(self)
351
+ return super unless bubble
352
+
353
+ bubble.monitor_exit(self)
354
+ nil
355
+ end
356
+
357
+ def mon_owned?
358
+ bubble = Synctest.bubble_for_operation(self)
359
+ return super unless bubble
360
+
361
+ bubble.monitor_owned?(self)
362
+ end
363
+
364
+ def synchronize
365
+ bubble = Synctest.bubble_for_operation(self)
366
+ return super unless bubble
367
+
368
+ entered = false
369
+ begin
370
+ bubble.monitor_enter(self)
371
+ entered = true
372
+ yield
373
+ ensure
374
+ bubble.monitor_exit(self) if entered
375
+ end
376
+ end
377
+
378
+ def mon_synchronize(&block)
379
+ bubble = Synctest.bubble_for_operation(self)
380
+ return super unless bubble
381
+
382
+ synchronize(&block)
383
+ end
384
+
385
+ def wait_for_cond(condition, timeout)
386
+ bubble = Synctest.bubble_for_operation(self)
387
+ return super unless bubble
388
+
389
+ bubble.monitor_wait(self, condition, timeout)
390
+ end
391
+ end
392
+
393
+ module QueuePatch
394
+ def initialize(...)
395
+ super
396
+ Synctest.associate(self)
397
+ end
398
+
399
+ def pop(non_block = false, timeout: nil)
400
+ bubble = Synctest.bubble_for_operation(self)
401
+ return super unless bubble
402
+
403
+ bubble.queue_pop(self, non_block, timeout)
404
+ end
405
+
406
+ def shift(non_block = false, timeout: nil)
407
+ bubble = Synctest.bubble_for_operation(self)
408
+ return super unless bubble
409
+
410
+ bubble.queue_pop(self, non_block, timeout)
411
+ end
412
+
413
+ def deq(non_block = false, timeout: nil)
414
+ bubble = Synctest.bubble_for_operation(self)
415
+ return super unless bubble
416
+
417
+ bubble.queue_pop(self, non_block, timeout)
418
+ end
419
+
420
+ def push(object)
421
+ bubble = Synctest.bubble_for_operation(self)
422
+ return super unless bubble
423
+
424
+ bubble.queue_push(self, object)
425
+ end
426
+
427
+ def enq(object)
428
+ bubble = Synctest.bubble_for_operation(self)
429
+ return super unless bubble
430
+
431
+ bubble.queue_push(self, object)
432
+ end
433
+
434
+ def <<(object)
435
+ bubble = Synctest.bubble_for_operation(self)
436
+ return super unless bubble
437
+
438
+ bubble.queue_push(self, object)
439
+ end
440
+
441
+ def close
442
+ bubble = Synctest.bubble_for_operation(self)
443
+ return super unless bubble
444
+
445
+ bubble.queue_close(self)
446
+ end
447
+
448
+ def clear
449
+ bubble = Synctest.bubble_for_operation(self)
450
+ return super unless bubble
451
+
452
+ bubble.queue_clear(self)
453
+ end
454
+
455
+ def num_waiting
456
+ bubble = Synctest.bubble_for_operation(self)
457
+ return super unless bubble
458
+
459
+ bubble.queue_num_waiting(self, super)
460
+ end
461
+ end
462
+
463
+ module SizedQueuePatch
464
+ def initialize(...)
465
+ super
466
+ Synctest.associate(self)
467
+ end
468
+
469
+ def pop(non_block = false, timeout: nil)
470
+ bubble = Synctest.bubble_for_operation(self)
471
+ return super unless bubble
472
+
473
+ bubble.queue_pop(self, non_block, timeout)
474
+ end
475
+
476
+ def shift(non_block = false, timeout: nil)
477
+ bubble = Synctest.bubble_for_operation(self)
478
+ return super unless bubble
479
+
480
+ bubble.queue_pop(self, non_block, timeout)
481
+ end
482
+
483
+ def deq(non_block = false, timeout: nil)
484
+ bubble = Synctest.bubble_for_operation(self)
485
+ return super unless bubble
486
+
487
+ bubble.queue_pop(self, non_block, timeout)
488
+ end
489
+
490
+ def push(object, non_block = false, timeout: nil)
491
+ bubble = Synctest.bubble_for_operation(self)
492
+ return super unless bubble
493
+
494
+ bubble.sized_queue_push(self, object, non_block, timeout)
495
+ end
496
+
497
+ def enq(object, non_block = false, timeout: nil)
498
+ bubble = Synctest.bubble_for_operation(self)
499
+ return super unless bubble
500
+
501
+ bubble.sized_queue_push(self, object, non_block, timeout)
502
+ end
503
+
504
+ def <<(object)
505
+ bubble = Synctest.bubble_for_operation(self)
506
+ return super unless bubble
507
+
508
+ bubble.sized_queue_push(self, object, false, nil)
509
+ end
510
+
511
+ def close
512
+ bubble = Synctest.bubble_for_operation(self)
513
+ return super unless bubble
514
+
515
+ bubble.queue_close(self)
516
+ end
517
+
518
+ def clear
519
+ bubble = Synctest.bubble_for_operation(self)
520
+ return super unless bubble
521
+
522
+ bubble.queue_clear(self)
523
+ end
524
+
525
+ def max=(maximum)
526
+ bubble = Synctest.bubble_for_operation(self)
527
+ if bubble
528
+ bubble.sized_queue_set_max(self, maximum)
529
+ else
530
+ super
531
+ end
532
+ end
533
+
534
+ def num_waiting
535
+ bubble = Synctest.bubble_for_operation(self)
536
+ return super unless bubble
537
+
538
+ bubble.queue_num_waiting(self, super)
539
+ end
540
+ end
541
+
542
+ module FiberSingletonPatch
543
+ def set_scheduler(scheduler)
544
+ if Synctest.current_bubble
545
+ raise UnsupportedOperationError, "Fiber schedulers are not supported inside a Synctest bubble"
546
+ end
547
+
548
+ super
549
+ end
550
+ end
551
+
552
+ module TimeoutPatch
553
+ def timeout(seconds, exception_class = nil, message = nil, &block)
554
+ bubble = Synctest.current_bubble
555
+ return super unless bubble
556
+ return block.call(seconds) if seconds.nil? || seconds.zero?
557
+
558
+ raise ArgumentError, "Timeout sec must be a non-negative number" if seconds.negative?
559
+
560
+ message ||= "execution expired"
561
+ if exception_class
562
+ Patches.run_timeout_timer(seconds, exception_class, message, &block)
563
+ else
564
+ Timeout::Error.handle_timeout(message) do |exception|
565
+ Patches.run_timeout_timer(seconds, exception, message, &block)
566
+ end
567
+ end
568
+ end
569
+ end
570
+
571
+ class << self
572
+ def install!
573
+ return if @installed
574
+
575
+ Kernel.prepend(KernelPatch)
576
+ Kernel.singleton_class.prepend(KernelSingletonPatch)
577
+ Time.singleton_class.prepend(TimeSingletonPatch)
578
+ Process.singleton_class.prepend(ProcessSingletonPatch)
579
+ Thread.singleton_class.prepend(ThreadSingletonPatch)
580
+ Thread.prepend(ThreadPatch)
581
+ Mutex.prepend(MutexPatch)
582
+ ConditionVariable.prepend(ConditionVariablePatch)
583
+ Monitor.prepend(MonitorPatch)
584
+ Queue.prepend(QueuePatch)
585
+ SizedQueue.prepend(SizedQueuePatch)
586
+ Fiber.singleton_class.prepend(FiberSingletonPatch)
587
+ Timeout.singleton_class.prepend(TimeoutPatch)
588
+ Timeout.prepend(TimeoutPatch)
589
+
590
+ @installed = true
591
+ end
592
+
593
+ def run_timeout_timer(seconds, exception, message)
594
+ target = Thread.current
595
+ timer = Thread.new do
596
+ sleep(seconds)
597
+ target.raise(exception, message)
598
+ end
599
+ yield(seconds)
600
+ ensure
601
+ if timer
602
+ timer.kill if timer.alive?
603
+ timer.join
604
+ end
605
+ end
606
+ end
607
+ end
608
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Synctest
4
+ # Original methods captured before process-wide instrumentation is installed.
5
+ # Bubble internals must use these methods to avoid observing themselves.
6
+ module Raw
7
+ MUTEX_LOCK = Mutex.instance_method(:lock)
8
+ MUTEX_TRY_LOCK = Mutex.instance_method(:try_lock)
9
+ MUTEX_UNLOCK = Mutex.instance_method(:unlock)
10
+ MUTEX_LOCKED = Mutex.instance_method(:locked?)
11
+ MUTEX_OWNED = Mutex.instance_method(:owned?)
12
+
13
+ CONDITION_WAIT = ConditionVariable.instance_method(:wait)
14
+ CONDITION_SIGNAL = ConditionVariable.instance_method(:signal)
15
+ CONDITION_BROADCAST = ConditionVariable.instance_method(:broadcast)
16
+
17
+ MONITOR_ENTER = Monitor.instance_method(:enter)
18
+ MONITOR_TRY_ENTER = Monitor.instance_method(:try_enter)
19
+ MONITOR_EXIT = Monitor.instance_method(:exit)
20
+ MONITOR_OWNED = Monitor.instance_method(:mon_owned?)
21
+
22
+ QUEUE_POP = Queue.instance_method(:pop)
23
+ QUEUE_PUSH = Queue.instance_method(:push)
24
+ QUEUE_CLOSE = Queue.instance_method(:close)
25
+ QUEUE_CLOSED = Queue.instance_method(:closed?)
26
+ QUEUE_CLEAR = Queue.instance_method(:clear)
27
+ QUEUE_LENGTH = Queue.instance_method(:length)
28
+
29
+ SIZED_QUEUE_PUSH = SizedQueue.instance_method(:push)
30
+ SIZED_QUEUE_POP = SizedQueue.instance_method(:pop)
31
+ SIZED_QUEUE_CLOSE = SizedQueue.instance_method(:close)
32
+ SIZED_QUEUE_CLEAR = SizedQueue.instance_method(:clear)
33
+ SIZED_QUEUE_LENGTH = SizedQueue.instance_method(:length)
34
+ SIZED_QUEUE_MAX = SizedQueue.instance_method(:max)
35
+ SIZED_QUEUE_SET_MAX = SizedQueue.instance_method(:max=)
36
+
37
+ THREAD_JOIN = Thread.instance_method(:join)
38
+ THREAD_VALUE = Thread.instance_method(:value)
39
+ THREAD_KILL = Thread.instance_method(:kill)
40
+ THREAD_RAISE = Thread.instance_method(:raise)
41
+ THREAD_WAKEUP = Thread.instance_method(:wakeup)
42
+ THREAD_RUN = Thread.instance_method(:run)
43
+
44
+ CLOCK_GETTIME = Process.method(:clock_gettime)
45
+ end
46
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Synctest
4
+ VERSION = "0.0.1"
5
+ end