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