semian 0.0.8 → 0.1.0

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,114 @@
1
+ require 'minitest/autorun'
2
+ require 'semian'
3
+ require 'timecop'
4
+
5
+ class TestCircuitBreaker < MiniTest::Unit::TestCase
6
+ SomeError = Class.new(StandardError)
7
+
8
+ def setup
9
+ Semian.destroy(:testing) rescue nil
10
+ Semian.register(:testing, tickets: 1, exceptions: [SomeError], error_threshold: 2, error_timeout: 5, success_threshold: 1)
11
+ @resource = Semian[:testing]
12
+ end
13
+
14
+ def test_with_fallback_value_returns_the_value
15
+ result = @resource.with_fallback(42) do
16
+ raise SomeError
17
+ end
18
+ assert_equal 42, result
19
+ end
20
+
21
+ def test_with_fallback_block_call_the_block
22
+ result = @resource.with_fallback(-> { 42 }) do
23
+ raise SomeError
24
+ end
25
+ assert_equal 42, result
26
+ end
27
+
28
+ def test_unknown_exceptions_are_not_rescued
29
+ assert_raises RuntimeError do
30
+ @resource.with_fallback(42) do
31
+ raise RuntimeError
32
+ end
33
+ end
34
+ end
35
+
36
+ def test_all_semian_exceptions_are_rescued
37
+ result = @resource.with_fallback(42) do
38
+ raise Semian::BaseError
39
+ end
40
+ assert_equal 42, result
41
+ end
42
+
43
+ def test_acquire_yield_when_the_circuit_is_closed
44
+ block_called = false
45
+ @resource.acquire { block_called = true }
46
+ assert_equal true, block_called
47
+ end
48
+
49
+ def test_acquire_raises_circuit_open_error_when_the_circuit_is_open
50
+ open_circuit!
51
+ assert_raises Semian::OpenCircuitError do
52
+ @resource.acquire { 1 + 1 }
53
+ end
54
+ end
55
+
56
+ def test_after_error_threshold_the_circuit_is_open
57
+ open_circuit!
58
+ assert_circuit_opened
59
+ end
60
+
61
+ def test_after_error_timeout_is_elapsed_requests_are_attempted_again
62
+ half_open_cicuit!
63
+ assert_circuit_closed
64
+ end
65
+
66
+ def test_until_success_threshold_is_reached_a_single_error_will_reopen_the_circuit
67
+ half_open_cicuit!
68
+ trigger_error!
69
+ assert_circuit_opened
70
+ end
71
+
72
+ def test_once_success_threshold_is_reached_only_error_threshold_will_open_the_circuit_again
73
+ half_open_cicuit!
74
+ assert_circuit_closed
75
+ trigger_error!
76
+ assert_circuit_closed
77
+ trigger_error!
78
+ assert_circuit_opened
79
+ end
80
+
81
+ def test_reset_allow_to_close_the_circuit_and_forget_errors
82
+ open_circuit!
83
+ @resource.reset
84
+ assert_circuit_closed
85
+ end
86
+
87
+ private
88
+
89
+ def open_circuit!
90
+ 2.times { trigger_error! }
91
+ end
92
+
93
+ def half_open_cicuit!
94
+ Timecop.travel(Time.now - 10) do
95
+ open_circuit!
96
+ end
97
+ end
98
+
99
+ def trigger_error!
100
+ @resource.with_fallback(42) { raise SomeError }
101
+ end
102
+
103
+ def assert_circuit_closed
104
+ block_called = false
105
+ @resource.with_fallback(42) { block_called = true }
106
+ assert block_called, 'Expected the circuit to be closed, but it was open'
107
+ end
108
+
109
+ def assert_circuit_opened
110
+ block_called = false
111
+ @resource.with_fallback(42) { block_called = true }
112
+ refute block_called, 'Expected the circuit to be open, but it was closed'
113
+ end
114
+ end
@@ -0,0 +1,62 @@
1
+ require 'minitest/autorun'
2
+ require 'semian'
3
+
4
+ class TestInstrumentation < MiniTest::Unit::TestCase
5
+ def setup
6
+ Semian.destroy(:testing) if Semian[:testing]
7
+ Semian.register(:testing, tickets: 1, error_threshold: 1, error_timeout: 5, success_threshold: 1)
8
+ end
9
+
10
+ def test_occupied_instrumentation
11
+ assert_notify(:success, :occupied) do
12
+ Semian[:testing].acquire do
13
+ assert_raises Semian::TimeoutError do
14
+ Semian[:testing].acquire {}
15
+ end
16
+ end
17
+ end
18
+ end
19
+
20
+ def test_circuit_open_instrumentation
21
+ assert_notify(:success, :occupied) do
22
+ Semian[:testing].acquire do
23
+ assert_raises Semian::TimeoutError do
24
+ Semian[:testing].acquire {}
25
+ end
26
+ end
27
+ end
28
+
29
+ assert_notify(:circuit_open) do
30
+ assert_raises Semian::OpenCircuitError do
31
+ Semian[:testing].acquire {}
32
+ end
33
+ end
34
+ end
35
+
36
+ def test_success_instrumentation
37
+ assert_notify(:success) do
38
+ Semian[:testing].acquire {}
39
+ end
40
+ end
41
+
42
+ def test_success_instrumentation_when_unknown_exceptions_occur
43
+ assert_notify(:success) do
44
+ assert_raises RuntimeError do
45
+ Semian[:testing].acquire { raise "Some error" }
46
+ end
47
+ end
48
+ end
49
+
50
+ private
51
+
52
+ def assert_notify(*expected_events)
53
+ events = []
54
+ subscription = Semian.subscribe do |event, resource|
55
+ events << event
56
+ end
57
+ yield
58
+ assert_equal expected_events, events, "The timeline of events was not as expected"
59
+ ensure
60
+ Semian.unsubscribe(subscription)
61
+ end
62
+ end
@@ -0,0 +1,186 @@
1
+ require 'minitest/autorun'
2
+ require 'semian/mysql2'
3
+ require 'toxiproxy'
4
+ require 'timecop'
5
+
6
+ class TestMysql2 < MiniTest::Unit::TestCase
7
+ ERROR_TIMEOUT = 5
8
+ ERROR_THRESHOLD = 1
9
+ SEMIAN_OPTIONS = {
10
+ name: :testing,
11
+ tickets: 1,
12
+ timeout: 0,
13
+ error_threshold: ERROR_THRESHOLD,
14
+ success_threshold: 2,
15
+ error_timeout: ERROR_TIMEOUT,
16
+ }
17
+
18
+ attr_writer :threads
19
+ def setup
20
+ @proxy = Toxiproxy[:semian_test_mysql]
21
+ Semian.destroy(:mysql_testing)
22
+ end
23
+
24
+ def teardown
25
+ threads.each { |t| t.kill }
26
+ self.threads = []
27
+ end
28
+
29
+ def test_semian_identifier
30
+ assert_equal :mysql_foo, FakeMysql.new(semian: {name: 'foo'}).semian_identifier
31
+ assert_equal :'mysql_localhost:3306', FakeMysql.new.semian_identifier
32
+ assert_equal :'mysql_127.0.0.1:3306', FakeMysql.new(host: '127.0.0.1').semian_identifier
33
+ assert_equal :'mysql_example.com:42', FakeMysql.new(host: 'example.com', port: 42).semian_identifier
34
+ end
35
+
36
+ def test_connect_instrumentation
37
+ notified = false
38
+ subscriber = Semian.subscribe do |event, resource, scope|
39
+ notified = true
40
+ assert_equal :success, event
41
+ assert_equal Semian[:mysql_testing], resource
42
+ assert_equal :connect, scope
43
+ end
44
+
45
+ connect_to_mysql!
46
+
47
+ assert notified, 'No notification have been emitted'
48
+ ensure
49
+ Semian.unsubscribe(subscriber)
50
+ end
51
+
52
+ def test_resource_acquisition_for_connect
53
+ client = connect_to_mysql!
54
+
55
+ Semian[:mysql_testing].acquire do
56
+ assert_raises Mysql2::ResourceOccupiedError do
57
+ connect_to_mysql!
58
+ end
59
+ end
60
+ end
61
+
62
+ def test_resource_timeout_on_connect
63
+ @proxy.downstream(:latency, latency: 500).apply do
64
+ background { connect_to_mysql! }
65
+
66
+ assert_raises Mysql2::ResourceOccupiedError do
67
+ connect_to_mysql!
68
+ end
69
+ end
70
+ end
71
+
72
+ def test_circuit_breaker_on_connect
73
+ @proxy.downstream(:latency, latency: 500).apply do
74
+ background { connect_to_mysql! }
75
+
76
+ ERROR_THRESHOLD.times do
77
+ assert_raises Mysql2::ResourceOccupiedError do
78
+ connect_to_mysql!
79
+ end
80
+ end
81
+ end
82
+
83
+ yield_to_background
84
+
85
+ assert_raises Mysql2::CircuitOpenError do
86
+ connect_to_mysql!
87
+ end
88
+
89
+ Timecop.travel(ERROR_TIMEOUT + 1) do
90
+ connect_to_mysql!
91
+ end
92
+ end
93
+
94
+ def test_query_instrumentation
95
+ client = connect_to_mysql!
96
+
97
+ notified = false
98
+ subscriber = Semian.subscribe do |event, resource, scope|
99
+ notified = true
100
+ assert_equal :success, event
101
+ assert_equal Semian[:mysql_testing], resource
102
+ assert_equal :query, scope
103
+ end
104
+
105
+ client.query('SELECT 1 + 1;')
106
+
107
+ assert notified, 'No notification have been emitted'
108
+ ensure
109
+ Semian.unsubscribe(subscriber)
110
+ end
111
+
112
+ def test_resource_acquisition_for_query
113
+ client = connect_to_mysql!
114
+
115
+ Semian[:mysql_testing].acquire do
116
+ assert_raises Mysql2::ResourceOccupiedError do
117
+ client.query('SELECT 1 + 1;')
118
+ end
119
+ end
120
+ end
121
+
122
+ def test_resource_timeout_on_query
123
+ client = connect_to_mysql!
124
+ client2 = connect_to_mysql!
125
+
126
+ @proxy.downstream(:latency, latency: 500).apply do
127
+ background { client2.query('SELECT 1 + 1;') }
128
+
129
+ assert_raises Mysql2::ResourceOccupiedError do
130
+ client.query('SELECT 1 + 1;')
131
+ end
132
+ end
133
+ end
134
+
135
+ def test_circuit_breaker_on_query
136
+ client = connect_to_mysql!
137
+ client2 = connect_to_mysql!
138
+
139
+ @proxy.downstream(:latency, latency: 1000).apply do
140
+ background { client2.query('SELECT 1 + 1;') }
141
+
142
+ ERROR_THRESHOLD.times do
143
+ assert_raises Mysql2::ResourceOccupiedError do
144
+ client.query('SELECT 1 + 1;')
145
+ end
146
+ end
147
+ end
148
+
149
+ yield_to_background
150
+
151
+ assert_raises Mysql2::CircuitOpenError do
152
+ client.query('SELECT 1 + 1;')
153
+ end
154
+
155
+ Timecop.travel(ERROR_TIMEOUT + 1) do
156
+ assert_equal 2, client.query('SELECT 1 + 1 as sum;').to_a.first['sum']
157
+ end
158
+ end
159
+
160
+ private
161
+
162
+ def background(&block)
163
+ thread = Thread.new(&block)
164
+ threads << thread
165
+ thread.join(0.1)
166
+ thread
167
+ end
168
+
169
+ def threads
170
+ @threads ||= []
171
+ end
172
+
173
+ def yield_to_background
174
+ threads.each(&:join)
175
+ end
176
+
177
+ def connect_to_mysql!(semian_options = {})
178
+ Mysql2::Client.new(host: '127.0.0.1', port: '13306', semian: SEMIAN_OPTIONS.merge(semian_options))
179
+ end
180
+
181
+ class FakeMysql < Mysql2::Client
182
+ private
183
+ def connect(*)
184
+ end
185
+ end
186
+ end
@@ -1,25 +1,29 @@
1
- require 'test/unit'
1
+ require 'minitest/autorun'
2
2
  require 'semian'
3
3
  require 'tempfile'
4
4
  require 'fileutils'
5
5
 
6
- class TestSemian < Test::Unit::TestCase
6
+ class TestResource < MiniTest::Unit::TestCase
7
7
  def setup
8
- Semian[:testing].destroy rescue Semian::BaseError
8
+ Semian.destroy(:testing) rescue nil
9
9
  end
10
10
 
11
- def test_register_invalid_args
11
+ def teardown
12
+ destroy_resources
13
+ end
14
+
15
+ def test_initialize_invalid_args
12
16
  assert_raises TypeError do
13
- Semian.register 123
17
+ create_resource 123, tickets: 2
14
18
  end
15
19
  assert_raises ArgumentError do
16
- Semian.register :testing, tickets: -1
20
+ create_resource :testing, tickets: -1
17
21
  end
18
22
  assert_raises ArgumentError do
19
- Semian.register :testing, tickets: 1_000_000
23
+ create_resource :testing, tickets: 1_000_000
20
24
  end
21
25
  assert_raises TypeError do
22
- Semian.register :testing, permissions: "test"
26
+ create_resource :testing, tickets: 2, permissions: 'test'
23
27
  end
24
28
  end
25
29
 
@@ -28,30 +32,30 @@ class TestSemian < Test::Unit::TestCase
28
32
  end
29
33
 
30
34
  def test_register
31
- Semian.register :testing, tickets: 2
35
+ create_resource :testing, tickets: 2
32
36
  end
33
37
 
34
38
  def test_register_with_no_tickets_raises
35
39
  assert_raises Semian::SyscallError do
36
- Semian.register :testing
40
+ create_resource :testing, tickets: 0
37
41
  end
38
42
  end
39
43
 
40
44
  def test_acquire
41
45
  acquired = false
42
- Semian.register :testing, tickets: 1
43
- Semian[:testing].acquire { acquired = true }
46
+ resource = create_resource :testing, tickets: 1
47
+ resource.acquire { acquired = true }
44
48
  assert acquired
45
49
  end
46
50
 
47
51
  def test_acquire_return_val
48
- Semian.register :testing, tickets: 1
49
- val = Semian[:testing].acquire { 1234 }
52
+ resource = create_resource :testing, tickets: 1
53
+ val = resource.acquire { 1234 }
50
54
  assert_equal 1234, val
51
55
  end
52
56
 
53
57
  def test_acquire_timeout
54
- Semian.register :testing, tickets: 1, timeout: 0.05
58
+ resource = create_resource :testing, tickets: 1, timeout: 0.05
55
59
 
56
60
  acquired = false
57
61
  m = Monitor.new
@@ -61,12 +65,12 @@ class TestSemian < Test::Unit::TestCase
61
65
  m.synchronize do
62
66
  cond.wait_until { acquired }
63
67
  assert_raises Semian::TimeoutError do
64
- Semian[:testing].acquire { refute true }
68
+ resource.acquire { refute true }
65
69
  end
66
70
  end
67
71
  end
68
72
 
69
- Semian[:testing].acquire do
73
+ resource.acquire do
70
74
  acquired = true
71
75
  m.synchronize { cond.signal }
72
76
  sleep 0.2
@@ -78,7 +82,7 @@ class TestSemian < Test::Unit::TestCase
78
82
  end
79
83
 
80
84
  def test_acquire_timeout_override
81
- Semian.register :testing, tickets: 1, timeout: 0.01
85
+ resource = create_resource :testing, tickets: 1, timeout: 0.01
82
86
 
83
87
  acquired = false
84
88
  thread_acquired = false
@@ -88,11 +92,11 @@ class TestSemian < Test::Unit::TestCase
88
92
  t = Thread.start do
89
93
  m.synchronize do
90
94
  cond.wait_until { acquired }
91
- Semian[:testing].acquire(timeout: 1) { thread_acquired = true }
95
+ resource.acquire(timeout: 1) { thread_acquired = true }
92
96
  end
93
97
  end
94
98
 
95
- Semian[:testing].acquire do
99
+ resource.acquire do
96
100
  acquired = true
97
101
  m.synchronize { cond.signal }
98
102
  sleep 0.2
@@ -105,14 +109,13 @@ class TestSemian < Test::Unit::TestCase
105
109
  end
106
110
 
107
111
  def test_acquire_with_fork
108
- Semian.register :testing, tickets: 2, timeout: 0.5
112
+ resource = create_resource :testing, tickets: 2, timeout: 0.5
109
113
 
110
- Semian[:testing].acquire do
114
+ resource.acquire do
111
115
  pid = fork do
112
- Semian.register :testing, timeout: 0.5
113
- Semian[:testing].acquire do
116
+ resource.acquire do
114
117
  assert_raises Semian::TimeoutError do
115
- Semian[:testing].acquire { }
118
+ resource.acquire { }
116
119
  end
117
120
  end
118
121
  end
@@ -123,7 +126,7 @@ class TestSemian < Test::Unit::TestCase
123
126
 
124
127
  def test_acquire_releases_on_kill
125
128
  begin
126
- Semian.register :testing, tickets: 1, timeout: 0.1
129
+ resource = create_resource :testing, tickets: 1, timeout: 0.1
127
130
  acquired = false
128
131
 
129
132
  # Ghetto process synchronization
@@ -132,7 +135,7 @@ class TestSemian < Test::Unit::TestCase
132
135
  file.close!
133
136
 
134
137
  pid = fork do
135
- Semian[:testing].acquire do
138
+ resource.acquire do
136
139
  FileUtils.touch(path)
137
140
  sleep 1000
138
141
  end
@@ -140,11 +143,11 @@ class TestSemian < Test::Unit::TestCase
140
143
 
141
144
  sleep 0.1 until File.exists?(path)
142
145
  assert_raises Semian::TimeoutError do
143
- Semian[:testing].acquire {}
146
+ resource.acquire {}
144
147
  end
145
148
 
146
149
  Process.kill("KILL", pid)
147
- Semian[:testing].acquire { acquired = true }
150
+ resource.acquire { acquired = true }
148
151
  assert acquired
149
152
 
150
153
  Process.wait
@@ -154,50 +157,51 @@ class TestSemian < Test::Unit::TestCase
154
157
  end
155
158
 
156
159
  def test_count
157
- Semian.register :testing, tickets: 2
160
+ resource = create_resource :testing, tickets: 2
158
161
  acquired = false
159
162
 
160
- Semian[:testing].acquire do
163
+ resource.acquire do
161
164
  acquired = true
162
- assert_equal 1, Semian[:testing].count
165
+ assert_equal 1, resource.count
166
+ assert_equal 2, resource.tickets
163
167
  end
164
168
 
165
169
  assert acquired
166
170
  end
167
171
 
168
172
  def test_sem_undo
169
- Semian.register :testing, tickets: 1
173
+ resource = create_resource :testing, tickets: 1
170
174
 
171
175
  # Ensure we don't hit ERANGE errors caused by lack of SEM_UNDO on semop* calls
172
176
  # by doing an acquire > SEMVMX (32767) times:
173
177
  #
174
178
  # See: http://lxr.free-electrons.com/source/ipc/sem.c?v=3.8#L419
175
179
  (1 << 16).times do # do an acquire 64k times
176
- Semian[:testing].acquire do
180
+ resource.acquire do
177
181
  1
178
182
  end
179
183
  end
180
184
  end
181
185
 
182
186
  def test_destroy
183
- Semian.register :testing, tickets: 1
184
- Semian[:testing].destroy
187
+ resource = create_resource :testing, tickets: 1
188
+ resource.destroy
185
189
  assert_raises Semian::SyscallError do
186
- Semian[:testing].acquire { }
190
+ resource.acquire { }
187
191
  end
188
192
  end
189
193
 
190
194
  def test_permissions
191
- Semian.register :testing, permissions: 0600, tickets: 1
192
- semid = Semian[:testing].semid
195
+ resource = create_resource :testing, permissions: 0600, tickets: 1
196
+ semid = resource.semid
193
197
  `ipcs -s `.lines.each do |line|
194
198
  if /\s#{semid}\s/.match(line)
195
199
  assert_equal '600', line.split[3]
196
200
  end
197
201
  end
198
202
 
199
- Semian.register :testing, permissions: 0660, tickets: 1
200
- semid = Semian[:testing].semid
203
+ resource = create_resource :testing, permissions: 0660, tickets: 1
204
+ semid = resource.semid
201
205
  `ipcs -s `.lines.each do |line|
202
206
  if /\s#{semid}\s/.match(line)
203
207
  assert_equal '660', line.split[3]
@@ -206,7 +210,7 @@ class TestSemian < Test::Unit::TestCase
206
210
  end
207
211
 
208
212
  def test_resize_tickets_increase
209
- Semian.register :testing, tickets: 1
213
+ resource = create_resource :testing, tickets: 1
210
214
 
211
215
  acquired = false
212
216
  m = Monitor.new
@@ -216,14 +220,14 @@ class TestSemian < Test::Unit::TestCase
216
220
  m.synchronize do
217
221
  cond.wait_until { acquired }
218
222
 
219
- Semian.register :testing, tickets: 5
220
- assert_equal 4, Semian[:testing].count
223
+ resource = create_resource :testing, tickets: 5
224
+ assert_equal 4, resource.count
221
225
  end
222
226
  end
223
227
 
224
- assert_equal 1, Semian[:testing].count
228
+ assert_equal 1, resource.count
225
229
 
226
- Semian[:testing].acquire do
230
+ resource.acquire do
227
231
  acquired = true
228
232
  m.synchronize { cond.signal }
229
233
  sleep 0.2
@@ -231,11 +235,11 @@ class TestSemian < Test::Unit::TestCase
231
235
 
232
236
  t.join
233
237
 
234
- assert_equal 5, Semian[:testing].count
238
+ assert_equal 5, resource.count
235
239
  end
236
240
 
237
241
  def test_resize_tickets_decrease
238
- Semian.register :testing, tickets: 5
242
+ resource = create_resource :testing, tickets: 5
239
243
 
240
244
  acquired = false
241
245
  m = Monitor.new
@@ -245,14 +249,14 @@ class TestSemian < Test::Unit::TestCase
245
249
  m.synchronize do
246
250
  cond.wait_until { acquired }
247
251
 
248
- Semian.register :testing, tickets: 1
249
- assert_equal 0, Semian[:testing].count
252
+ resource = create_resource :testing, tickets: 1
253
+ assert_equal 0, resource.count
250
254
  end
251
255
  end
252
256
 
253
- assert_equal 5, Semian[:testing].count
257
+ assert_equal 5, resource.count
254
258
 
255
- Semian[:testing].acquire do
259
+ resource.acquire do
256
260
  acquired = true
257
261
  m.synchronize { cond.signal }
258
262
  sleep 0.2
@@ -260,7 +264,7 @@ class TestSemian < Test::Unit::TestCase
260
264
 
261
265
  t.join
262
266
 
263
- assert_equal 1, Semian[:testing].count
267
+ assert_equal 1, resource.count
264
268
  end
265
269
 
266
270
  def test_multiple_register_with_fork
@@ -275,7 +279,7 @@ class TestSemian < Test::Unit::TestCase
275
279
  acquired = false
276
280
 
277
281
  f.flock(File::LOCK_SH)
278
- Semian.register(:testing, tickets: 5).acquire do |resource|
282
+ create_resource(:testing, tickets: 5).acquire do |resource|
279
283
  assert resource.count < 5
280
284
  acquired = true
281
285
  end
@@ -290,10 +294,24 @@ class TestSemian < Test::Unit::TestCase
290
294
  children.delete(Process.wait)
291
295
  end
292
296
 
293
- assert_equal 5, Semian.register(:testing).count
297
+ assert_equal 5, create_resource(:testing, tickets: 0).count
294
298
  ensure
295
299
  f.close!
296
300
  end
297
301
  end
298
302
 
303
+ def create_resource(*args)
304
+ @resources ||= []
305
+ resource = Semian::Resource.new(*args)
306
+ @resources << resource
307
+ resource
308
+ end
309
+
310
+ def destroy_resources
311
+ return unless @resources
312
+ @resources.each do |resource|
313
+ resource.destroy rescue nil
314
+ end
315
+ @resources = []
316
+ end
299
317
  end
@@ -1,10 +1,14 @@
1
- require 'test/unit'
1
+ require 'minitest/autorun'
2
2
  require 'semian'
3
3
 
4
- class TestSemian < Test::Unit::TestCase
4
+ class TestSemian < MiniTest::Unit::TestCase
5
+ def setup
6
+ Semian.destroy(:testing) rescue nil
7
+ end
8
+
5
9
  def test_unsupported_acquire_yields
6
10
  acquired = false
7
- Semian.register :testing, tickets: 1
11
+ Semian.register :testing, tickets: 1, error_threshold: 1, error_timeout: 2, success_threshold: 1
8
12
  Semian[:testing].acquire { acquired = true }
9
13
  assert acquired
10
14
  end
data.tar.gz.sig CHANGED
Binary file