Module: Synctest
- Defined in:
- lib/synctest.rb,
lib/synctest/raw.rb,
lib/synctest/bubble.rb,
lib/synctest/errors.rb,
lib/synctest/patches.rb,
lib/synctest/version.rb
Overview
Deterministic tests for concurrent Ruby code.
Defined Under Namespace
Modules: Patches, Raw Classes: Bubble, ConcurrentWaitError, DeadlockError, Error, IsolationError, NestedBubbleError, NotInBubbleError, StalledError, UnsupportedOperationError
Constant Summary collapse
- THREAD_BUBBLE_KEY =
:__synctest_bubble__- OBJECT_BUBBLE_IVAR =
:@__synctest_bubble__- DEFAULT_START_TIME =
Time.utc(2000, 1, 1).freeze
- DEFAULT_TIMEOUT =
5.0- VERSION =
"0.0.1"
Class Method Summary collapse
-
.active? ⇒ Boolean
Returns whether the calling thread currently belongs to a bubble.
-
.associate(object) ⇒ Object
:nodoc:.
-
.bubble_for(object) ⇒ Object
:nodoc:.
-
.bubble_for_operation(object) ⇒ Object
Returns the object's bubble when access is valid, nil for an ordinary object, and raises when a bubbled object is accessed from elsewhere.
-
.bubble_for_thread_operation(thread) ⇒ Object
Root threads predate their bubbles and are intentionally not permanently associated with them, so thread operations also inspect live thread-local membership.
-
.current_bubble ⇒ Object
:nodoc:.
-
.run(start_at: DEFAULT_START_TIME, timeout: DEFAULT_TIMEOUT, &block) ⇒ Object
Runs
blockin an isolated concurrency bubble. -
.set_current_bubble(bubble) ⇒ Object
:nodoc:.
-
.wait ⇒ Object
Waits until all other threads in the current bubble have either exited or become durably blocked.
Class Method Details
.active? ⇒ Boolean
Returns whether the calling thread currently belongs to a bubble.
47 48 49 |
# File 'lib/synctest.rb', line 47 def active? !current_bubble.nil? end |
.associate(object) ⇒ Object
:nodoc:
62 63 64 65 66 67 68 69 70 71 72 73 |
# File 'lib/synctest.rb', line 62 def associate(object) bubble = current_bubble return object unless bubble owner = bubble_for(object) if owner && !owner.equal?(bubble) raise IsolationError, "object already belongs to a different Synctest bubble" end object.instance_variable_set(OBJECT_BUBBLE_IVAR, bubble) object end |
.bubble_for(object) ⇒ Object
:nodoc:
57 58 59 |
# File 'lib/synctest.rb', line 57 def bubble_for(object) object.instance_variable_get(OBJECT_BUBBLE_IVAR) end |
.bubble_for_operation(object) ⇒ Object
Returns the object's bubble when access is valid, nil for an ordinary object, and raises when a bubbled object is accessed from elsewhere. :nodoc:
78 79 80 81 82 83 84 85 86 87 88 |
# File 'lib/synctest.rb', line 78 def bubble_for_operation(object) owner = bubble_for(object) return nil unless owner current = current_bubble unless owner.equal?(current) && owner.active? raise IsolationError, "#{object.class} belongs to a different or finished Synctest bubble" end owner end |
.bubble_for_thread_operation(thread) ⇒ Object
Root threads predate their bubbles and are intentionally not permanently associated with them, so thread operations also inspect live thread-local membership. :nodoc:
94 95 96 97 98 99 100 101 102 103 104 |
# File 'lib/synctest.rb', line 94 def bubble_for_thread_operation(thread) owner = thread.thread_variable_get(THREAD_BUBBLE_KEY) || bubble_for(thread) return nil unless owner current = current_bubble unless owner.equal?(current) && owner.active? raise IsolationError, "Thread belongs to a different or finished Synctest bubble" end owner end |
.current_bubble ⇒ Object
:nodoc:
52 53 54 |
# File 'lib/synctest.rb', line 52 def current_bubble Thread.current.thread_variable_get(THREAD_BUBBLE_KEY) end |
.run(start_at: DEFAULT_START_TIME, timeout: DEFAULT_TIMEOUT, &block) ⇒ Object
Runs block in an isolated concurrency bubble.
Threads and synchronization primitives created by the block belong to the bubble. Time is virtual and advances only when every bubble thread is durably blocked.
25 26 27 28 29 30 31 32 33 34 35 |
# File 'lib/synctest.rb', line 25 def run(start_at: DEFAULT_START_TIME, timeout: DEFAULT_TIMEOUT, &block) raise ArgumentError, "Synctest.run requires a block" unless block raise NestedBubbleError, "cannot start a Synctest bubble from inside another bubble" if current_bubble if Fiber.respond_to?(:scheduler) && Fiber.scheduler raise UnsupportedOperationError, "Fiber schedulers are not supported inside a Synctest bubble" end bubble = Bubble.new(start_at: start_at, timeout: timeout) bubble.run(&block) end |
.set_current_bubble(bubble) ⇒ Object
:nodoc:
107 108 109 |
# File 'lib/synctest.rb', line 107 def set_current_bubble(bubble) Thread.current.thread_variable_set(THREAD_BUBBLE_KEY, bubble) end |
.wait ⇒ Object
Waits until all other threads in the current bubble have either exited or become durably blocked. Unlike sleeping, waiting never advances time.
39 40 41 42 43 44 |
# File 'lib/synctest.rb', line 39 def wait bubble = current_bubble raise NotInBubbleError, "Synctest.wait must be called from inside Synctest.run" unless bubble bubble.wait end |