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

Class Method Details

.active?Boolean

Returns whether the calling thread currently belongs to a bubble.

Returns:

  • (Boolean)


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_bubbleObject

: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.

Raises:

  • (ArgumentError)


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

.waitObject

Waits until all other threads in the current bubble have either exited or become durably blocked. Unlike sleeping, waiting never advances time.

Raises:



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