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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +235 -0
- data/lib/synctest/bubble.rb +1225 -0
- data/lib/synctest/errors.rb +27 -0
- data/lib/synctest/patches.rb +608 -0
- data/lib/synctest/raw.rb +46 -0
- data/lib/synctest/version.rb +5 -0
- data/lib/synctest.rb +113 -0
- metadata +135 -0
data/lib/synctest.rb
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "monitor"
|
|
4
|
+
require "timeout"
|
|
5
|
+
|
|
6
|
+
require_relative "synctest/version"
|
|
7
|
+
require_relative "synctest/errors"
|
|
8
|
+
require_relative "synctest/raw"
|
|
9
|
+
require_relative "synctest/bubble"
|
|
10
|
+
require_relative "synctest/patches"
|
|
11
|
+
|
|
12
|
+
# Deterministic tests for concurrent Ruby code.
|
|
13
|
+
module Synctest
|
|
14
|
+
THREAD_BUBBLE_KEY = :__synctest_bubble__
|
|
15
|
+
OBJECT_BUBBLE_IVAR = :@__synctest_bubble__
|
|
16
|
+
DEFAULT_START_TIME = Time.utc(2000, 1, 1).freeze
|
|
17
|
+
DEFAULT_TIMEOUT = 5.0
|
|
18
|
+
|
|
19
|
+
class << self
|
|
20
|
+
# Runs +block+ in an isolated concurrency bubble.
|
|
21
|
+
#
|
|
22
|
+
# Threads and synchronization primitives created by the block belong to
|
|
23
|
+
# the bubble. Time is virtual and advances only when every bubble thread is
|
|
24
|
+
# durably blocked.
|
|
25
|
+
def run(start_at: DEFAULT_START_TIME, timeout: DEFAULT_TIMEOUT, &block)
|
|
26
|
+
raise ArgumentError, "Synctest.run requires a block" unless block
|
|
27
|
+
raise NestedBubbleError, "cannot start a Synctest bubble from inside another bubble" if current_bubble
|
|
28
|
+
|
|
29
|
+
if Fiber.respond_to?(:scheduler) && Fiber.scheduler
|
|
30
|
+
raise UnsupportedOperationError, "Fiber schedulers are not supported inside a Synctest bubble"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
bubble = Bubble.new(start_at: start_at, timeout: timeout)
|
|
34
|
+
bubble.run(&block)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Waits until all other threads in the current bubble have either exited
|
|
38
|
+
# or become durably blocked. Unlike sleeping, waiting never advances time.
|
|
39
|
+
def wait
|
|
40
|
+
bubble = current_bubble
|
|
41
|
+
raise NotInBubbleError, "Synctest.wait must be called from inside Synctest.run" unless bubble
|
|
42
|
+
|
|
43
|
+
bubble.wait
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Returns whether the calling thread currently belongs to a bubble.
|
|
47
|
+
def active?
|
|
48
|
+
!current_bubble.nil?
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# :nodoc:
|
|
52
|
+
def current_bubble
|
|
53
|
+
Thread.current.thread_variable_get(THREAD_BUBBLE_KEY)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# :nodoc:
|
|
57
|
+
def bubble_for(object)
|
|
58
|
+
object.instance_variable_get(OBJECT_BUBBLE_IVAR)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# :nodoc:
|
|
62
|
+
def associate(object)
|
|
63
|
+
bubble = current_bubble
|
|
64
|
+
return object unless bubble
|
|
65
|
+
|
|
66
|
+
owner = bubble_for(object)
|
|
67
|
+
if owner && !owner.equal?(bubble)
|
|
68
|
+
raise IsolationError, "object already belongs to a different Synctest bubble"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
object.instance_variable_set(OBJECT_BUBBLE_IVAR, bubble)
|
|
72
|
+
object
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Returns the object's bubble when access is valid, nil for an ordinary
|
|
76
|
+
# object, and raises when a bubbled object is accessed from elsewhere.
|
|
77
|
+
# :nodoc:
|
|
78
|
+
def bubble_for_operation(object)
|
|
79
|
+
owner = bubble_for(object)
|
|
80
|
+
return nil unless owner
|
|
81
|
+
|
|
82
|
+
current = current_bubble
|
|
83
|
+
unless owner.equal?(current) && owner.active?
|
|
84
|
+
raise IsolationError, "#{object.class} belongs to a different or finished Synctest bubble"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
owner
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Root threads predate their bubbles and are intentionally not permanently
|
|
91
|
+
# associated with them, so thread operations also inspect live thread-local
|
|
92
|
+
# membership.
|
|
93
|
+
# :nodoc:
|
|
94
|
+
def bubble_for_thread_operation(thread)
|
|
95
|
+
owner = thread.thread_variable_get(THREAD_BUBBLE_KEY) || bubble_for(thread)
|
|
96
|
+
return nil unless owner
|
|
97
|
+
|
|
98
|
+
current = current_bubble
|
|
99
|
+
unless owner.equal?(current) && owner.active?
|
|
100
|
+
raise IsolationError, "Thread belongs to a different or finished Synctest bubble"
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
owner
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# :nodoc:
|
|
107
|
+
def set_current_bubble(bubble)
|
|
108
|
+
Thread.current.thread_variable_set(THREAD_BUBBLE_KEY, bubble)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
Patches.install!
|
|
113
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: synctest
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.0.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Alexander Baygeldin
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: bundler
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '2.0'
|
|
19
|
+
type: :development
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '2.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: nats-pure
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: 2.5.0
|
|
33
|
+
type: :development
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: 2.5.0
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: pry
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '0.15'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '0.15'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: rake
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '13.0'
|
|
61
|
+
type: :development
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '13.0'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: rspec
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - "~>"
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '3.0'
|
|
75
|
+
type: :development
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - "~>"
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '3.0'
|
|
82
|
+
- !ruby/object:Gem::Dependency
|
|
83
|
+
name: standard
|
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - "~>"
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '1.0'
|
|
89
|
+
type: :development
|
|
90
|
+
prerelease: false
|
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - "~>"
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: '1.0'
|
|
96
|
+
description: Runs concurrent Ruby tests in isolated bubbles with tracked threads,
|
|
97
|
+
synchronization primitives, quiescence, and virtual time.
|
|
98
|
+
email:
|
|
99
|
+
- a.baygeldin@gmail.com
|
|
100
|
+
executables: []
|
|
101
|
+
extensions: []
|
|
102
|
+
extra_rdoc_files: []
|
|
103
|
+
files:
|
|
104
|
+
- LICENSE
|
|
105
|
+
- README.md
|
|
106
|
+
- lib/synctest.rb
|
|
107
|
+
- lib/synctest/bubble.rb
|
|
108
|
+
- lib/synctest/errors.rb
|
|
109
|
+
- lib/synctest/patches.rb
|
|
110
|
+
- lib/synctest/raw.rb
|
|
111
|
+
- lib/synctest/version.rb
|
|
112
|
+
homepage: https://github.com/baygeldin/synctest-rb
|
|
113
|
+
licenses:
|
|
114
|
+
- MIT
|
|
115
|
+
metadata:
|
|
116
|
+
rubygems_mfa_required: 'true'
|
|
117
|
+
source_code_uri: https://github.com/baygeldin/synctest-rb
|
|
118
|
+
rdoc_options: []
|
|
119
|
+
require_paths:
|
|
120
|
+
- lib
|
|
121
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
122
|
+
requirements:
|
|
123
|
+
- - ">="
|
|
124
|
+
- !ruby/object:Gem::Version
|
|
125
|
+
version: 3.4.0
|
|
126
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
127
|
+
requirements:
|
|
128
|
+
- - ">="
|
|
129
|
+
- !ruby/object:Gem::Version
|
|
130
|
+
version: '0'
|
|
131
|
+
requirements: []
|
|
132
|
+
rubygems_version: 3.6.9
|
|
133
|
+
specification_version: 4
|
|
134
|
+
summary: Deterministic concurrency testing with virtual time for Ruby.
|
|
135
|
+
test_files: []
|