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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 129484c73e5be7741c4bbf70c2ab374a0e48f28926c3cd4b6b3ca004c1037b98
4
- data.tar.gz: 83ec64585e4b33bfad802b262784b0507f341621764099279af3f6faeda273fa
3
+ metadata.gz: f0585c0152a9703256b3265830dd9105b33ebc65877555517af3ad643a6b8839
4
+ data.tar.gz: ac62f147919ac2e68642d70d9e0bb9d2c8010ef54b2aefc4d6c679fe1b9d7197
5
5
  SHA512:
6
- metadata.gz: 3c61fa019e0aff56404f4c3346b039615c368e175e98407f118be9ef55060e61f4ebacbd6cec79b5e0834cfec2550fdbdf6952be47eeacb1437455f5fd54dd5b
7
- data.tar.gz: 209b104767846e73d8db2b94d0b4c9e2e9528c695e9d4cb766d6845de7e8c1556e523b172461c693c77e6deea3f0a764dd51c8e2a17611a2914760146e399bcc
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
- - Ractor
38
- - Process
39
- - Thread
40
- - None (Run tests sequentially)
46
+ - Ractor
47
+ - Process
48
+ - Thread
49
+ - None (Run tests sequentially)
41
50
  - Documentation
42
- - Add better examples
43
- - Arbitrary usage
44
- - Configuration
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, the block runs in parallel by Ractor.
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`, `:thread`, `:process` and `:none` are supported. Default is `:none`.
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 any of the three workers—`:ractor`, `:process`, or `:thread`—using the `worker` option. Alternatively, choose `:none` for serial execution.
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 using `:process` as workers whose benchmark is the most similar to `:ractor`.
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 (without parallelism) but most test cases finishes within a reasonable time.
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 if possible.
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
@@ -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|
@@ -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|
@@ -92,7 +92,7 @@ module Pbt
92
92
  one_of(*HEXA_CHARS)
93
93
  end
94
94
 
95
- # For lowercase hexadecimal stings.
95
+ # For lowercase hexadecimal strings.
96
96
  #
97
97
  # @see Pbt.array
98
98
  # @param kwargs [Hash] Options for ArrayArbitrary. See `.array` for more information.
@@ -20,7 +20,13 @@ module Pbt
20
20
  end
21
21
 
22
22
  # @see Arbitrary#shrink
23
- def shrink(current, target: DEFAULT_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
- y.yield target # no diff here
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`, `:thread`, `:process` and `:none` are supported. Default is `:none`.
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.new.seed,
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
- handle_ractor_error(e.cause, c)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pbt
4
- VERSION = "0.4.2"
4
+ VERSION = "0.5.1"
5
5
  end
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.2
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: 2024-05-23 00:00:00.000000000 Z
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.5.9
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