pbt 0.4.2 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +6 -1
- data/README.md +8 -57
- data/benchmark/failure_simple.rb +0 -20
- data/benchmark/success_cpu_bound.rb +0 -16
- data/benchmark/success_io_bound.rb +0 -16
- data/benchmark/success_simple.rb +0 -16
- data/lib/pbt/arbitrary/arbitrary_methods.rb +1 -1
- data/lib/pbt/check/configuration.rb +3 -6
- data/lib/pbt/check/runner_methods.rb +1 -85
- data/lib/pbt/version.rb +1 -1
- metadata +3 -6
- data/lib/pbt/check/rspec_adapter/integration.rb +0 -64
- data/lib/pbt/check/rspec_adapter/predicate_block_inspector.rb +0 -47
- data/lib/pbt/check/rspec_adapter/property_extension.rb +0 -74
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2b93d8a06cbff5946bab644527868f42601ccd7521d4d11e3746fa00c279cd62
|
4
|
+
data.tar.gz: ab19c9a123d4408cb49e7a24225bca930d5bd4134000e98a3efc9c233ce97f9c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cd5fd29fb5e5d3afb5817fd24b0c8967037e2158dd3cc57e4d4ab83eb12c7a31c1dfff376a88b96173ca14853cd3d56b89de212c3630bb6cffe4bb4f04b106d6
|
7
|
+
data.tar.gz: b0d37a7198cd0d1c504170f00041b5decab223e73c8896ebd1d357f4482503665ec0ca200cf5557bbed6c04f775cb3c7264eb61b1a42e6b6982f30e90941155e
|
data/CHANGELOG.md
CHANGED
@@ -1,9 +1,14 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
## [0.5.0] - 2024-12-30
|
4
|
+
|
5
|
+
- [Breaking change] Drop `:process` and `:thread` workers since there are no concrete use cases.
|
6
|
+
- [Breaking change] Drop `:experimental_ractor_rspec_integration` option since there are no concrete use cases.
|
7
|
+
|
3
8
|
## [0.4.2] - 2024-05-23
|
4
9
|
|
5
10
|
- Fix Prism `LoadError` message [#27](https://github.com/ohbarye/pbt/pull/27) by @sambostock
|
6
|
-
|
11
|
+
|
7
12
|
## [0.4.1] - 2024-05-10
|
8
13
|
|
9
14
|
- Fix a bug for experimental_ractor_rspec_integration mode. When a test file name starts with a number, it can't be a constant name.
|
data/README.md
CHANGED
@@ -51,7 +51,7 @@ end
|
|
51
51
|
|
52
52
|
Pbt.assert do
|
53
53
|
# The given block is executed 100 times with different arrays with random numbers.
|
54
|
-
# Besides,
|
54
|
+
# Besides, if you set `worker: :ractor` option to `assert` method, it runs in parallel using Ractor.
|
55
55
|
Pbt.property(Pbt.array(Pbt.integer)) do |numbers|
|
56
56
|
result = sort(numbers)
|
57
57
|
result.each_cons(2) do |x, y|
|
@@ -218,7 +218,7 @@ Pbt.configure do |config|
|
|
218
218
|
# Whether to print verbose output. Default is `false`.
|
219
219
|
config.verbose = false
|
220
220
|
|
221
|
-
# The concurrency method to use. `:ractor
|
221
|
+
# The concurrency method to use. `:ractor` and `:none` are supported. Default is `:none`.
|
222
222
|
config.worker = :none
|
223
223
|
|
224
224
|
# The number of runs to perform. Default is `100`.
|
@@ -231,9 +231,6 @@ Pbt.configure do |config|
|
|
231
231
|
# Whether to report exceptions in threads.
|
232
232
|
# It's useful to suppress error logs on Ractor that reports many errors. Default is `false`.
|
233
233
|
config.thread_report_on_exception = false
|
234
|
-
|
235
|
-
# Whether to allow RSpec expectation and matchers in Ractor. It's quite experimental! Default is `false`.
|
236
|
-
config.experimental_ractor_rspec_integration = false
|
237
234
|
end
|
238
235
|
```
|
239
236
|
|
@@ -249,15 +246,13 @@ end
|
|
249
246
|
|
250
247
|
One of the key features of `Pbt` is its ability to rapidly execute test cases in parallel or concurrently, using a large number of values (by default, `100`) generated by `Arbitrary`.
|
251
248
|
|
252
|
-
For concurrent processing, you can specify
|
253
|
-
|
254
|
-
`Pbt` supports 3 concurrency methods and 1 sequential one. You can choose one of them by setting the `worker` option.
|
249
|
+
For concurrent processing, you can specify `:ractor` using the `worker` option. Alternatively, choose `:none` for serial execution.
|
255
250
|
|
256
251
|
Be aware that the performance of each method depends on the test subject. For example, if the test subject is CPU-bound, `:ractor` may be the best choice. Otherwise, `:none` shall be the best choice for most cases. See [benchmarks](benchmark/README.md).
|
257
252
|
|
258
253
|
### Ractor
|
259
254
|
|
260
|
-
`:ractor` worker is useful for test cases that are CPU-bound. But it's experimental and has some limitations as described below. If you encounter any issues due to those limitations, consider
|
255
|
+
`:ractor` worker is useful for test cases that are CPU-bound. But it's experimental and has some limitations as described below. If you encounter any issues due to those limitations, consider falling back to `:none`.
|
261
256
|
|
262
257
|
```ruby
|
263
258
|
Pbt.assert(worker: :ractor) do
|
@@ -297,53 +292,9 @@ it do
|
|
297
292
|
end
|
298
293
|
```
|
299
294
|
|
300
|
-
If you're a challenger, you can enable the experimental feature to allow using RSpec expectations and matchers in Ractor. It works but it's quite experimental and could cause unexpected behaviors.
|
301
|
-
|
302
|
-
Please note that this feature depends on [prism](https://ruby.github.io/prism/) gem. If you use Ruby 3.2 or prior, you need to install the gem by yourself.
|
303
|
-
|
304
|
-
```ruby
|
305
|
-
it do
|
306
|
-
Pbt.assert(worker: :ractor, experimental_ractor_rspec_integration: true) do
|
307
|
-
Pbt.property(Pbt.integer) do |n|
|
308
|
-
# Some RSpec expectations and matchers are available in Ractor by hack.
|
309
|
-
# Other features like `let`, `subject`, `before`, `after` that access out of block are still not available.
|
310
|
-
expect(n).to be_an(Integer)
|
311
|
-
end
|
312
|
-
end
|
313
|
-
end
|
314
|
-
```
|
315
|
-
|
316
|
-
### Process
|
317
|
-
|
318
|
-
If you'd like to run test cases that are CPU-bound and `:ractor` is not available, `:process` becomes a good choice.
|
319
|
-
|
320
|
-
```ruby
|
321
|
-
Pbt.assert(worker: :process) do
|
322
|
-
Pbt.property(Pbt.integer) do |n|
|
323
|
-
# ...
|
324
|
-
end
|
325
|
-
end
|
326
|
-
```
|
327
|
-
|
328
|
-
If you want to use `:process`, you need to install the [parallel](https://github.com/grosser/parallel) gem.
|
329
|
-
|
330
|
-
### Thread
|
331
|
-
|
332
|
-
You may not need to run test cases with multi-threads.
|
333
|
-
|
334
|
-
```ruby
|
335
|
-
Pbt.assert(worker: :thread) do
|
336
|
-
Pbt.property(Pbt.integer) do |n|
|
337
|
-
# ...
|
338
|
-
end
|
339
|
-
end
|
340
|
-
```
|
341
|
-
|
342
|
-
If you want to use `:thread`, you need to install the [parallel](https://github.com/grosser/parallel) gem.
|
343
|
-
|
344
295
|
### None
|
345
296
|
|
346
|
-
For most cases, `:none` is the best choice. It runs tests sequentially
|
297
|
+
For most cases, `:none` is the best choice. It runs tests sequentially but most test cases finishes within a reasonable time.
|
347
298
|
|
348
299
|
```ruby
|
349
300
|
Pbt.assert(worker: :none) do
|
@@ -362,8 +313,8 @@ Once this project finishes the following, we will release v1.0.0.
|
|
362
313
|
- [x] Support shrinking
|
363
314
|
- [x] Support multiple concurrency methods
|
364
315
|
- [x] Ractor
|
365
|
-
- [x] Process
|
366
|
-
- [x] Thread
|
316
|
+
- [x] Process (dropped)
|
317
|
+
- [x] Thread (dropped)
|
367
318
|
- [x] None (Run tests sequentially)
|
368
319
|
- [x] Documentation
|
369
320
|
- [x] Add better examples
|
@@ -371,7 +322,7 @@ Once this project finishes the following, we will release v1.0.0.
|
|
371
322
|
- [x] Configuration
|
372
323
|
- [x] Benchmark
|
373
324
|
- [x] Rich report by verbose mode
|
374
|
-
- [x] (Partially) Allow to use expectations and matchers provided by test framework in Ractor
|
325
|
+
- [x] (Partially) Allow to use expectations and matchers provided by test framework in Ractor. (dropped)
|
375
326
|
- It'd be so hard to pass assertions like `expect`, `assert` to a Ractor.
|
376
327
|
- [ ] Implement frequency arbitrary
|
377
328
|
- [ ] Statistics feature to aggregate generated values
|
data/benchmark/failure_simple.rb
CHANGED
@@ -17,26 +17,6 @@ Benchmark.ips do |x|
|
|
17
17
|
# noop
|
18
18
|
end
|
19
19
|
|
20
|
-
x.report("process") do
|
21
|
-
Pbt.assert(worker: :process, seed:, num_runs: 100) do
|
22
|
-
Pbt.property(Pbt.integer) do |x|
|
23
|
-
task(x)
|
24
|
-
end
|
25
|
-
end
|
26
|
-
rescue Pbt::PropertyFailure
|
27
|
-
# noop
|
28
|
-
end
|
29
|
-
|
30
|
-
x.report("thread") do
|
31
|
-
Pbt.assert(worker: :thread, seed:, num_runs: 100) do
|
32
|
-
Pbt.property(Pbt.integer) do |x|
|
33
|
-
task(x)
|
34
|
-
end
|
35
|
-
end
|
36
|
-
rescue Pbt::PropertyFailure
|
37
|
-
# noop
|
38
|
-
end
|
39
|
-
|
40
20
|
x.report("none") do
|
41
21
|
Pbt.assert(worker: :none, seed:, num_runs: 100) do
|
42
22
|
Pbt.property(Pbt.integer) do |x|
|
@@ -25,22 +25,6 @@ Benchmark.ips do |x|
|
|
25
25
|
end
|
26
26
|
end
|
27
27
|
|
28
|
-
x.report("process") do
|
29
|
-
Pbt.assert(worker: :process, num_runs: 100) do
|
30
|
-
Pbt.property(Pbt.constant([a, b, c])) do |x, y, z|
|
31
|
-
task(x, y, z)
|
32
|
-
end
|
33
|
-
end
|
34
|
-
end
|
35
|
-
|
36
|
-
x.report("thread") do
|
37
|
-
Pbt.assert(worker: :thread, num_runs: 100) do
|
38
|
-
Pbt.property(Pbt.constant([a, b, c])) do |x, y, z|
|
39
|
-
task(x, y, z)
|
40
|
-
end
|
41
|
-
end
|
42
|
-
end
|
43
|
-
|
44
28
|
x.report("none") do
|
45
29
|
Pbt.assert(worker: :none, num_runs: 100) do
|
46
30
|
Pbt.property(Pbt.constant([a, b, c])) do |x, y, z|
|
@@ -18,22 +18,6 @@ Benchmark.ips do |x|
|
|
18
18
|
end
|
19
19
|
end
|
20
20
|
|
21
|
-
x.report("process") do
|
22
|
-
Pbt.assert(worker: :process, seed:, num_runs: 100) do
|
23
|
-
Pbt.property(Pbt.ascii_string) do |str|
|
24
|
-
task(str)
|
25
|
-
end
|
26
|
-
end
|
27
|
-
end
|
28
|
-
|
29
|
-
x.report("thread") do
|
30
|
-
Pbt.assert(worker: :thread, seed:, num_runs: 100) do
|
31
|
-
Pbt.property(Pbt.ascii_string) do |str|
|
32
|
-
task(str)
|
33
|
-
end
|
34
|
-
end
|
35
|
-
end
|
36
|
-
|
37
21
|
x.report("none") do
|
38
22
|
Pbt.assert(worker: :none, seed:, num_runs: 100) do
|
39
23
|
Pbt.property(Pbt.ascii_string) do |str|
|
data/benchmark/success_simple.rb
CHANGED
@@ -14,22 +14,6 @@ Benchmark.ips do |x|
|
|
14
14
|
end
|
15
15
|
end
|
16
16
|
|
17
|
-
x.report("process") do
|
18
|
-
Pbt.assert(worker: :process, seed:, num_runs: 100) do
|
19
|
-
Pbt.property(Pbt.integer) do |x|
|
20
|
-
task(x)
|
21
|
-
end
|
22
|
-
end
|
23
|
-
end
|
24
|
-
|
25
|
-
x.report("thread") do
|
26
|
-
Pbt.assert(worker: :thread, seed:, num_runs: 100) do
|
27
|
-
Pbt.property(Pbt.integer) do |x|
|
28
|
-
task(x)
|
29
|
-
end
|
30
|
-
end
|
31
|
-
end
|
32
|
-
|
33
17
|
x.report("none") do
|
34
18
|
Pbt.assert(worker: :none, seed:, num_runs: 100) do
|
35
19
|
Pbt.property(Pbt.integer) do |x|
|
@@ -9,22 +9,19 @@ module Pbt
|
|
9
9
|
:num_runs,
|
10
10
|
:seed,
|
11
11
|
:thread_report_on_exception,
|
12
|
-
:experimental_ractor_rspec_integration,
|
13
12
|
keyword_init: true
|
14
13
|
) do
|
15
14
|
# @param verbose [Boolean] Whether to print verbose output. Default is `false`.
|
16
|
-
# @param worker [Symbol] The concurrency method to use. :ractor
|
15
|
+
# @param worker [Symbol] The concurrency method to use. :ractor` and `:none` are supported. Default is `:none`.
|
17
16
|
# @param num_runs [Integer] The number of runs to perform. Default is `100`.
|
18
17
|
# @param seed [Integer] The seed to use for random number generation. It's useful to reproduce failed test with the seed you'd pick up from failure messages. Default is a random seed.
|
19
18
|
# @param thread_report_on_exception [Boolean] Whether to report exceptions in threads. It's useful to suppress error logs on Ractor that reports many errors. Default is `false`.
|
20
|
-
# @param experimental_ractor_rspec_integration [Boolean] Whether to allow RSpec expectation and matchers in Ractor. It's quite experimental! Default is `false`.
|
21
19
|
def initialize(
|
22
20
|
verbose: false,
|
23
21
|
worker: :none,
|
24
22
|
num_runs: 100,
|
25
|
-
seed: Random.
|
26
|
-
thread_report_on_exception: false
|
27
|
-
experimental_ractor_rspec_integration: false
|
23
|
+
seed: Random.new_seed,
|
24
|
+
thread_report_on_exception: false
|
28
25
|
)
|
29
26
|
super
|
30
27
|
end
|
@@ -54,8 +54,6 @@ module Pbt
|
|
54
54
|
#
|
55
55
|
# - `:thread_report_on_exception`
|
56
56
|
# So many exception reports happen in Ractor and a console gets too messy. Suppress them to avoid that.
|
57
|
-
# - `:experimental_ractor_rspec_integration`
|
58
|
-
# Allow to use Ractor with RSpec. This is an experimental feature and it's not stable.
|
59
57
|
#
|
60
58
|
# @param config [Hash] Configuration parameters.
|
61
59
|
# @param property [Property]
|
@@ -64,21 +62,12 @@ module Pbt
|
|
64
62
|
if config[:worker] == :ractor
|
65
63
|
original_report_on_exception = Thread.report_on_exception
|
66
64
|
Thread.report_on_exception = config[:thread_report_on_exception]
|
67
|
-
|
68
|
-
if config[:experimental_ractor_rspec_integration]
|
69
|
-
require "pbt/check/rspec_adapter/integration"
|
70
|
-
class << property
|
71
|
-
include Pbt::Check::RSpecAdapter::PropertyExtension
|
72
|
-
end
|
73
|
-
property.setup_rspec_integration
|
74
|
-
end
|
75
65
|
end
|
76
66
|
|
77
67
|
yield
|
78
68
|
ensure
|
79
69
|
if config[:worker] == :ractor
|
80
70
|
Thread.report_on_exception = original_report_on_exception
|
81
|
-
property.teardown_rspec_integration if config[:experimental_ractor_rspec_integration]
|
82
71
|
end
|
83
72
|
end
|
84
73
|
|
@@ -96,10 +85,6 @@ module Pbt
|
|
96
85
|
run_it_in_sequential(property, runner)
|
97
86
|
in :ractor
|
98
87
|
run_it_in_ractors(property, runner)
|
99
|
-
in :process
|
100
|
-
run_it_in_processes(property, runner)
|
101
|
-
in :thread
|
102
|
-
run_it_in_threads(property, runner)
|
103
88
|
end
|
104
89
|
end
|
105
90
|
runner.run_execution
|
@@ -133,80 +118,11 @@ module Pbt
|
|
133
118
|
c.ractor.take
|
134
119
|
runner.handle_result(c)
|
135
120
|
rescue => e
|
136
|
-
|
121
|
+
c.exception = e.cause # Ractor error is wrapped in a Ractor::RemoteError. We need to get the cause.
|
137
122
|
runner.handle_result(c)
|
138
123
|
break # Ignore the rest of the cases. Just pick up the first failure.
|
139
124
|
end
|
140
125
|
end
|
141
|
-
|
142
|
-
def handle_ractor_error(cause, c)
|
143
|
-
# Ractor error is wrapped in a Ractor::RemoteError. We need to get the cause.
|
144
|
-
unless defined?(Pbt::Check::RSpecAdapter) && cause.is_a?(Pbt::Check::RSpecAdapter::ExpectationNotMet) # Unknown error.
|
145
|
-
c.exception = cause
|
146
|
-
return
|
147
|
-
end
|
148
|
-
|
149
|
-
# Convert Pbt's custom error to RSpec's error.
|
150
|
-
begin
|
151
|
-
RSpec::Expectations::ExpectationHelper.handle_failure(cause.matcher, cause.custom_message, cause.failure_message_method)
|
152
|
-
rescue RSpec::Expectations::ExpectationNotMetError => e # The class inherits Exception, not StandardError.
|
153
|
-
c.exception = e
|
154
|
-
end
|
155
|
-
end
|
156
|
-
|
157
|
-
# @param property [Property] Property to test.
|
158
|
-
# @param runner [RunnerIterator]
|
159
|
-
# @return [void]
|
160
|
-
def run_it_in_threads(property, runner)
|
161
|
-
require_parallel
|
162
|
-
|
163
|
-
Parallel.map_with_index(runner, in_threads: Parallel.processor_count) do |val, index|
|
164
|
-
Case.new(val:, index:).tap do |c|
|
165
|
-
property.run(val)
|
166
|
-
# Catch all exceptions including RSpec's ExpectationNotMet (It inherits Exception).
|
167
|
-
rescue Exception => e # standard:disable Lint/RescueException:
|
168
|
-
c.exception = e
|
169
|
-
# It's possible to break this loop here by raising `Parallel::Break`.
|
170
|
-
# But if it raises, we cannot fetch all cases' result. So this loop continues until the end.
|
171
|
-
end
|
172
|
-
end.each do |c|
|
173
|
-
runner.handle_result(c)
|
174
|
-
break if c.exception
|
175
|
-
# Ignore the rest of the cases. Just pick up the first failure.
|
176
|
-
end
|
177
|
-
end
|
178
|
-
|
179
|
-
# @param property [Property] Property to test.
|
180
|
-
# @param runner [RunnerIterator]
|
181
|
-
# @return [void]
|
182
|
-
def run_it_in_processes(property, runner)
|
183
|
-
require_parallel
|
184
|
-
|
185
|
-
Parallel.map_with_index(runner, in_processes: Parallel.processor_count) do |val, index|
|
186
|
-
Case.new(val:, index:).tap do |c|
|
187
|
-
property.run(val)
|
188
|
-
# Catch all exceptions including RSpec's ExpectationNotMet (It inherits Exception).
|
189
|
-
rescue Exception => e # standard:disable Lint/RescueException:
|
190
|
-
c.exception = e
|
191
|
-
# It's possible to break this loop here by raising `Parallel::Break`.
|
192
|
-
# But if it raises, we cannot fetch all cases' result. So this loop continues until the end.
|
193
|
-
end
|
194
|
-
end.each do |c|
|
195
|
-
runner.handle_result(c)
|
196
|
-
break if c.exception
|
197
|
-
# Ignore the rest of the cases. Just pick up the first failure.
|
198
|
-
end
|
199
|
-
end
|
200
|
-
|
201
|
-
# Load Parallel gem. If it's not installed, raise an error.
|
202
|
-
# @see https://github.com/grosser/parallel
|
203
|
-
# @raise [InvalidConfiguration]
|
204
|
-
def require_parallel
|
205
|
-
require "parallel"
|
206
|
-
rescue LoadError
|
207
|
-
raise InvalidConfiguration,
|
208
|
-
"Parallel gem (https://github.com/grosser/parallel) is required to use worker `:process` or `:thread`. Please add `gem 'parallel'` to your Gemfile."
|
209
|
-
end
|
210
126
|
end
|
211
127
|
end
|
212
128
|
end
|
data/lib/pbt/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: pbt
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- ohbarye
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
11
|
+
date: 2024-12-30 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description:
|
14
14
|
email:
|
@@ -46,9 +46,6 @@ files:
|
|
46
46
|
- lib/pbt/check/case.rb
|
47
47
|
- lib/pbt/check/configuration.rb
|
48
48
|
- lib/pbt/check/property.rb
|
49
|
-
- lib/pbt/check/rspec_adapter/integration.rb
|
50
|
-
- lib/pbt/check/rspec_adapter/predicate_block_inspector.rb
|
51
|
-
- lib/pbt/check/rspec_adapter/property_extension.rb
|
52
49
|
- lib/pbt/check/runner_iterator.rb
|
53
50
|
- lib/pbt/check/runner_methods.rb
|
54
51
|
- lib/pbt/check/tosser.rb
|
@@ -79,7 +76,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
79
76
|
- !ruby/object:Gem::Version
|
80
77
|
version: '0'
|
81
78
|
requirements: []
|
82
|
-
rubygems_version: 3.5.
|
79
|
+
rubygems_version: 3.5.21
|
83
80
|
signing_key:
|
84
81
|
specification_version: 4
|
85
82
|
summary: Property-Based Testing tool for Ruby, utilizing Ractor for parallelizing
|
@@ -1,64 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
unless defined?(RSpec)
|
4
|
-
raise InvalidConfigurationError, "You configured `experimental_ractor_rspec_integration: true` but RSpec is not loaded. Please use RSpec or set the config `false`."
|
5
|
-
end
|
6
|
-
|
7
|
-
require "pbt/check/rspec_adapter/predicate_block_inspector"
|
8
|
-
require "pbt/check/rspec_adapter/property_extension"
|
9
|
-
|
10
|
-
module Pbt
|
11
|
-
module Check
|
12
|
-
# @private
|
13
|
-
module RSpecAdapter
|
14
|
-
# This custom error contains RSpec matcher and message to handle Pbt's runner.
|
15
|
-
# @private
|
16
|
-
class ExpectationNotMet < StandardError
|
17
|
-
attr_reader :matcher, :custom_message, :failure_message_method
|
18
|
-
|
19
|
-
def initialize(msg, matcher, custom_message, failure_message_method)
|
20
|
-
super(msg)
|
21
|
-
@matcher = matcher
|
22
|
-
@custom_message = custom_message
|
23
|
-
@failure_message_method = failure_message_method
|
24
|
-
end
|
25
|
-
end
|
26
|
-
end
|
27
|
-
end
|
28
|
-
end
|
29
|
-
|
30
|
-
# `autoload` is not allowed in Ractor but RSpec uses autoload for matchers.
|
31
|
-
# We need to load them in advance in order to be able to use them in Ractor.
|
32
|
-
#
|
33
|
-
# e.g. Ractor raises... `be_a_kind_of': require by autoload on non-main Ractor is not supported (BeAKindOf) (Ractor::UnsafeError)
|
34
|
-
RSpec::Matchers::BuiltIn.constants.each { |c| Object.const_get("RSpec::Matchers::BuiltIn::#{c}") }
|
35
|
-
|
36
|
-
# TODO: preload more helpers like aggregate_failures.
|
37
|
-
# RSpec::Expectations.constants.each { |c| Object.const_get("RSpec::Expectations::#{c}") }
|
38
|
-
# The code above is not enough. Even if we run this code in advance, Ractor raises...
|
39
|
-
# in `failure_notifier': can not access non-shareable objects in constant RSpec::Support::DEFAULT_FAILURE_NOTIFIER by non-main ractor. (Ractor::IsolationError)
|
40
|
-
|
41
|
-
# CAUTION: This is a dirty hack! We need to override the original method to make it Ractor-safe.
|
42
|
-
RSpec::Expectations::ExpectationHelper.singleton_class.prepend(Module.new do
|
43
|
-
def with_matcher(handler, matcher, message)
|
44
|
-
check_message(message)
|
45
|
-
matcher = modern_matcher_from(matcher)
|
46
|
-
yield matcher
|
47
|
-
ensure
|
48
|
-
# The original method is not Ractor-safe unless stopping assigning these class variables.
|
49
|
-
if Ractor.current == Ractor.main
|
50
|
-
::RSpec::Matchers.last_expectation_handler = handler
|
51
|
-
::RSpec::Matchers.last_matcher = matcher
|
52
|
-
end
|
53
|
-
end
|
54
|
-
|
55
|
-
def handle_failure(matcher, message, failure_message_method)
|
56
|
-
# This method is not Ractor-safe. RSpec::Support::ObjectFormatter.default_instance assigns class variables.
|
57
|
-
# If this method is called in non-main-Ractor, it raises a custom error and let it handle in the main Ractor.
|
58
|
-
if Ractor.current != Ractor.main
|
59
|
-
raise Pbt::Check::RSpecAdapter::ExpectationNotMet.new(nil, matcher, message, failure_message_method)
|
60
|
-
end
|
61
|
-
|
62
|
-
super
|
63
|
-
end
|
64
|
-
end)
|
@@ -1,47 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
begin
|
4
|
-
# Use prism to get user-defined block code.
|
5
|
-
require "prism"
|
6
|
-
rescue LoadError
|
7
|
-
raise InvalidConfiguration,
|
8
|
-
"Prism gem (https://github.com/ruby/prism) is required to use worker `:ractor` and `:experimental_ractor_rspec_integration`. Please add `gem 'prism'` to your Gemfile."
|
9
|
-
end
|
10
|
-
|
11
|
-
module Pbt
|
12
|
-
module Check
|
13
|
-
module RSpecAdapter
|
14
|
-
# This class is used to get user-defined block code.
|
15
|
-
# If a user defines code like below:
|
16
|
-
#
|
17
|
-
# Pbt.property(Pbt.integer, Pbt.integer) do |x, y|
|
18
|
-
# x > 0 && y > 0
|
19
|
-
# end
|
20
|
-
#
|
21
|
-
# inspector.method_params #=> "x, y"
|
22
|
-
# inspector.method_body #=> "x > 0 && y > 0"
|
23
|
-
#
|
24
|
-
# @private
|
25
|
-
# @!attribute [r] method_body
|
26
|
-
# @!attribute [r] method_params
|
27
|
-
class PredicateBlockInspector < Prism::Visitor
|
28
|
-
attr_reader :method_body, :method_params
|
29
|
-
|
30
|
-
def initialize(line)
|
31
|
-
@line = line
|
32
|
-
@method_body = nil
|
33
|
-
super()
|
34
|
-
end
|
35
|
-
|
36
|
-
def visit_call_node(node)
|
37
|
-
if node.name == :property && node.block.opening_loc.start_line == @line
|
38
|
-
@method_params = node.block.parameters.parameters.slice
|
39
|
-
@method_body = node.block.body.slice
|
40
|
-
end
|
41
|
-
|
42
|
-
super
|
43
|
-
end
|
44
|
-
end
|
45
|
-
end
|
46
|
-
end
|
47
|
-
end
|
@@ -1,74 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Pbt
|
4
|
-
module Check
|
5
|
-
module RSpecAdapter
|
6
|
-
# @private
|
7
|
-
module PropertyExtension
|
8
|
-
# Define an original class to be called in Ractor.
|
9
|
-
#
|
10
|
-
# @return [void]
|
11
|
-
def setup_rspec_integration
|
12
|
-
filepath, line = @predicate.source_location
|
13
|
-
basename = File.basename(filepath, ".rb")
|
14
|
-
@class_name = "Test" + basename.split("_").map(&:capitalize).join + line.to_s
|
15
|
-
@method_name = "predicate_#{basename}_#{line}"
|
16
|
-
define_ractor_callable_class
|
17
|
-
end
|
18
|
-
|
19
|
-
# Clean up an original class to be called in Ractor to avoid any persisted namespace pollution.
|
20
|
-
#
|
21
|
-
# @return [void]
|
22
|
-
def teardown_rspec_integration
|
23
|
-
RSpecAdapter.__send__(:remove_const, @class_name) if RSpecAdapter.const_defined?(@class_name)
|
24
|
-
end
|
25
|
-
|
26
|
-
# Run the predicate with the generated `val`.
|
27
|
-
# This overrides the original `Property#run_in_ractor`.
|
28
|
-
#
|
29
|
-
# @param val [Object]
|
30
|
-
# @return [Ractor]
|
31
|
-
def run_in_ractor(val)
|
32
|
-
Ractor.new(@class_name, @method_name, @predicate.parameters.size, val) do |class_name, method_name, param_size, val|
|
33
|
-
klass = RSpecAdapter.const_get(class_name)
|
34
|
-
if val.is_a?(Hash)
|
35
|
-
klass.new.send(method_name, **val)
|
36
|
-
elsif param_size >= 2
|
37
|
-
klass.new.send(method_name, *val)
|
38
|
-
else
|
39
|
-
klass.new.send(method_name, val)
|
40
|
-
end
|
41
|
-
end
|
42
|
-
end
|
43
|
-
|
44
|
-
private
|
45
|
-
|
46
|
-
# @return [void]
|
47
|
-
def define_ractor_callable_class
|
48
|
-
# The @method_name is invisible in the Class.new block, so we need to assign it to a local variable.
|
49
|
-
method_name = @method_name
|
50
|
-
|
51
|
-
inspector = extract_predicate_source_code
|
52
|
-
|
53
|
-
RSpecAdapter.const_set(@class_name, Class.new do
|
54
|
-
include ::RSpec::Matchers
|
55
|
-
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
56
|
-
def #{method_name}(#{inspector.method_params})
|
57
|
-
#{inspector.method_body}
|
58
|
-
end
|
59
|
-
RUBY
|
60
|
-
end)
|
61
|
-
end
|
62
|
-
|
63
|
-
# @return [PredicateBlockInspector]
|
64
|
-
def extract_predicate_source_code
|
65
|
-
filepath, line = @predicate.source_location
|
66
|
-
PredicateBlockInspector.new(line).tap do |inspector|
|
67
|
-
res = Prism.parse_file(filepath)
|
68
|
-
res.value.statements.accept(inspector)
|
69
|
-
end
|
70
|
-
end
|
71
|
-
end
|
72
|
-
end
|
73
|
-
end
|
74
|
-
end
|