pbt 0.4.2 → 0.5.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 +4 -4
- data/CHANGELOG.md +19 -10
- 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/arbitrary/integer_arbitrary.rb +13 -2
- 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 -10
- 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: f0585c0152a9703256b3265830dd9105b33ebc65877555517af3ad643a6b8839
|
4
|
+
data.tar.gz: ac62f147919ac2e68642d70d9e0bb9d2c8010ef54b2aefc4d6c679fe1b9d7197
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 54535a35fa3097b1fe3f4893f83ef9c646598b385c9d4c32f3eb2686aab3727ca22154a87edef748f94090682acee6f7090ecb4d51233e66e861b30bd20df0d3
|
7
|
+
data.tar.gz: e8126c1c5f1b044d83f66b6d0a734783d2843f16f0b24f9a1172797e5b144a630a26c642bfc606f57de9911935f1b25ff0c1d8af48ef92486680c80e264308b0
|
data/CHANGELOG.md
CHANGED
@@ -1,9 +1,18 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
## [0.5.1] - 2025-06-29
|
4
|
+
|
5
|
+
- Fix `IntegerArbitrary#shrink` to respect min/max bounds [#36](https://github.com/ohbarye/pbt/pull/36)
|
6
|
+
|
7
|
+
## [0.5.0] - 2024-12-30
|
8
|
+
|
9
|
+
- [Breaking change] Drop `:process` and `:thread` workers since there are no concrete use cases.
|
10
|
+
- [Breaking change] Drop `:experimental_ractor_rspec_integration` option since there are no concrete use cases.
|
11
|
+
|
3
12
|
## [0.4.2] - 2024-05-23
|
4
13
|
|
5
14
|
- Fix Prism `LoadError` message [#27](https://github.com/ohbarye/pbt/pull/27) by @sambostock
|
6
|
-
|
15
|
+
|
7
16
|
## [0.4.1] - 2024-05-10
|
8
17
|
|
9
18
|
- 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.
|
@@ -23,7 +32,7 @@
|
|
23
32
|
## [0.2.0] - 2024-04-17
|
24
33
|
|
25
34
|
- Add verbose mode. It's useful to debug the test case.
|
26
|
-
|
35
|
+
|
27
36
|
## [0.1.1] - 2024-04-14
|
28
37
|
|
29
38
|
- Change default worker from `:ractor` to `:none`
|
@@ -34,15 +43,15 @@
|
|
34
43
|
- Implement composite arbitraries
|
35
44
|
- Support shrinking
|
36
45
|
- Support multiple concurrency methods
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
46
|
+
- Ractor
|
47
|
+
- Process
|
48
|
+
- Thread
|
49
|
+
- None (Run tests sequentially)
|
41
50
|
- Documentation
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
51
|
+
- Add better examples
|
52
|
+
- Arbitrary usage
|
53
|
+
- Configuration
|
54
|
+
|
46
55
|
## [0.0.1] - 2024-01-27
|
47
56
|
|
48
57
|
- Initial release (Proof of concept)
|
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|
|
@@ -20,7 +20,13 @@ module Pbt
|
|
20
20
|
end
|
21
21
|
|
22
22
|
# @see Arbitrary#shrink
|
23
|
-
def shrink(current, target:
|
23
|
+
def shrink(current, target: nil)
|
24
|
+
# If no target is specified, use the appropriate bound as target
|
25
|
+
target ||= DEFAULT_TARGET.clamp(@min, @max)
|
26
|
+
|
27
|
+
# Ensure target is within bounds
|
28
|
+
target = target.clamp(@min, @max)
|
29
|
+
|
24
30
|
gap = current - target
|
25
31
|
return Enumerator.new { |_| } if gap == 0
|
26
32
|
|
@@ -30,9 +36,14 @@ module Pbt
|
|
30
36
|
while (diff = (current - target).abs) > 1
|
31
37
|
halved = diff / 2
|
32
38
|
current -= is_positive_gap ? halved : -halved
|
39
|
+
# Ensure current stays within bounds
|
40
|
+
current = current.clamp(@min, @max)
|
33
41
|
y.yield current
|
34
42
|
end
|
35
|
-
|
43
|
+
# Only yield target if it's different from current
|
44
|
+
if current != target
|
45
|
+
y.yield target
|
46
|
+
end
|
36
47
|
end
|
37
48
|
end
|
38
49
|
end
|
@@ -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,16 +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.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- ohbarye
|
8
|
-
autorequire:
|
9
8
|
bindir: exe
|
10
9
|
cert_chain: []
|
11
|
-
date:
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
12
11
|
dependencies: []
|
13
|
-
description:
|
14
12
|
email:
|
15
13
|
- over.rye@gmail.com
|
16
14
|
executables: []
|
@@ -46,9 +44,6 @@ files:
|
|
46
44
|
- lib/pbt/check/case.rb
|
47
45
|
- lib/pbt/check/configuration.rb
|
48
46
|
- 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
47
|
- lib/pbt/check/runner_iterator.rb
|
53
48
|
- lib/pbt/check/runner_methods.rb
|
54
49
|
- lib/pbt/check/tosser.rb
|
@@ -64,7 +59,6 @@ metadata:
|
|
64
59
|
homepage_uri: https://github.com/ohbarye/pbt
|
65
60
|
source_code_uri: https://github.com/ohbarye/pbt
|
66
61
|
changelog_uri: https://github.com/ohbarye/pbt/releases
|
67
|
-
post_install_message:
|
68
62
|
rdoc_options: []
|
69
63
|
require_paths:
|
70
64
|
- lib
|
@@ -79,8 +73,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
79
73
|
- !ruby/object:Gem::Version
|
80
74
|
version: '0'
|
81
75
|
requirements: []
|
82
|
-
rubygems_version: 3.
|
83
|
-
signing_key:
|
76
|
+
rubygems_version: 3.6.7
|
84
77
|
specification_version: 4
|
85
78
|
summary: Property-Based Testing tool for Ruby, utilizing Ractor for parallelizing
|
86
79
|
test cases.
|
@@ -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
|