pbt 0.2.0 → 0.3.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d02ba2667633586ac6e6582c3f0f181dd089e63d56b160877a0b3e51ebb353e0
4
- data.tar.gz: 1e4dc60aa8ddbac429091bd86d3f8d601bb1a34034aa8e7c6c8c914d43e5e9b8
3
+ metadata.gz: 8415f37c2819e6370e7d418cb96d8ae2467fec24cfca1e64088c925874dc0c34
4
+ data.tar.gz: f6b38e54ec98fa669cc379c6c181e210d5fab02fc076a343e941aa17cb5be476
5
5
  SHA512:
6
- metadata.gz: 9ba49e79546492deec531f0ebef12e86e8458482740d20b3d0b3398a02aae220ea5fed476bee6489909bc2f62990fadcda2fff556fab2df69d30984a9d48435c
7
- data.tar.gz: df6c5128d3f2b839068bd736946114a81dabf590b49d1d93d7053cf7368fa65d299515323396864c9a942f2e004a3cab73f36dc215ba5660c3d5213e41ee7cfd
6
+ metadata.gz: cf614cf7100cbbf13f197dbc2a088e43c8b01fac31eaddc8d24ce5f1da7ed14140e3c59e3cafa6b0a22eee7014965c99df444d90dc70bcfbf07862f077ebb738
7
+ data.tar.gz: df70247adc89586f9ebb813427fe3efdcfd5ba93deea286dea8f5120dae92bc293f6a7f7ff2bcb2a5bdd263ed7e5108d7c358c4a96eb978e037143f02356a1db
data/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.3.0] - 2024-04-21
4
+
5
+ - Add experimental_ractor_rspec_integration mode. Be careful, it's quite experimental.
6
+ - Fix a bug: consider a case when a backtrace is nil.
7
+ - Allow to pass a predicate block keyword arguments with destruction.
8
+
3
9
  ## [0.2.0] - 2024-04-17
4
10
 
5
11
  - Add verbose mode. It's useful to debug the test case.
data/README.md CHANGED
@@ -32,12 +32,6 @@ Add this line to your application's Gemfile and run `bundle install`.
32
32
  gem 'pbt'
33
33
  ```
34
34
 
35
- If you want to use multi-processes or multi-threads (other than Ractor) as workers to run tests, install the [parallel](https://github.com/grosser/parallel) gem.
36
-
37
- ```ruby
38
- gem 'parallel'
39
- ```
40
-
41
35
  Off course you can install with `gem intstall pbt`.
42
36
 
43
37
  ## Basic Usage
@@ -226,6 +220,9 @@ Pbt.configure do |config|
226
220
  # Whether to report exceptions in threads.
227
221
  # It's useful to suppress error logs on Ractor that reports many errors. Default is `false`.
228
222
  config.thread_report_on_exception = false
223
+
224
+ # Whether to allow RSpec expectation and matchers in Ractor. It's quite experimental! Default is `false`.
225
+ config.experimental_ractor_rspec_integration = false
229
226
  end
230
227
  ```
231
228
 
@@ -289,6 +286,22 @@ it do
289
286
  end
290
287
  ```
291
288
 
289
+ 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.
290
+
291
+ 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.
292
+
293
+ ```ruby
294
+ it do
295
+ Pbt.assert(worker: :ractor, experimental_ractor_rspec_integration: true) do
296
+ Pbt.property(Pbt.integer) do |n|
297
+ # Some RSpec expectations and matchers are available in Ractor by hack.
298
+ # Other features like `let`, `subject`, `before`, `after` that access out of block are still not available.
299
+ expect(n).to be_an(Integer)
300
+ end
301
+ end
302
+ end
303
+ ```
304
+
292
305
  ### Process
293
306
 
294
307
  If you'd like to run test cases that are CPU-bound and `:ractor` is not available, `:process` becomes a good choice.
@@ -301,6 +314,8 @@ Pbt.assert(worker: :process) do
301
314
  end
302
315
  ```
303
316
 
317
+ If you want to use `:process`, you need to install the [parallel](https://github.com/grosser/parallel) gem.
318
+
304
319
  ### Thread
305
320
 
306
321
  You may not need to run test cases with multi-threads.
@@ -313,6 +328,8 @@ Pbt.assert(worker: :thread) do
313
328
  end
314
329
  ```
315
330
 
331
+ If you want to use `:thread`, you need to install the [parallel](https://github.com/grosser/parallel) gem.
332
+
316
333
  ### None
317
334
 
318
335
  For most cases, `:none` is the best choice. It runs tests sequentially (without parallelism) but most test cases finishes within a reasonable time.
@@ -343,9 +360,11 @@ Once this project finishes the following, we will release v1.0.0.
343
360
  - [x] Configuration
344
361
  - [x] Benchmark
345
362
  - [x] Rich report by verbose mode
346
- - [ ] Allow to use expectations and matchers provided by test framework in Ractor if possible.
363
+ - [x] (Partially) Allow to use expectations and matchers provided by test framework in Ractor if possible.
347
364
  - It'd be so hard to pass assertions like `expect`, `assert` to a Ractor.
348
- - [ ] More parallelism or faster execution if possible
365
+ - [ ] Implement frequency arbitrary
366
+ - [ ] Statistics feature to aggregate generated values
367
+ - [ ] Decide DSL
349
368
 
350
369
  ## Development
351
370
 
data/Rakefile CHANGED
@@ -19,7 +19,10 @@ namespace :benchmark do
19
19
  puts "This runs a script that does not do any IO or CPU bound work."
20
20
  puts
21
21
  ENV["RUBYOPT"] = "-W:no-experimental"
22
+ ENV["RUBY_MN_THREADS"] = "1"
23
+ puts "```"
22
24
  sh "ruby", "benchmark/success_simple.rb"
25
+ puts "```"
23
26
  puts
24
27
  end
25
28
 
@@ -29,7 +32,10 @@ namespace :benchmark do
29
32
  puts "This runs a script that does CPU bound work."
30
33
  puts
31
34
  ENV["RUBYOPT"] = "-W:no-experimental"
35
+ ENV["RUBY_MN_THREADS"] = "1"
36
+ puts "```"
32
37
  sh "ruby", "benchmark/success_cpu_bound.rb"
38
+ puts "```"
33
39
  puts
34
40
  end
35
41
 
@@ -39,7 +45,10 @@ namespace :benchmark do
39
45
  puts "This runs a script that does IO bound work."
40
46
  puts
41
47
  ENV["RUBYOPT"] = "-W:no-experimental"
48
+ ENV["RUBY_MN_THREADS"] = "1"
49
+ puts "```"
42
50
  sh "ruby", "benchmark/success_io_bound.rb"
51
+ puts "```"
43
52
  puts
44
53
  end
45
54
  end
@@ -51,7 +60,10 @@ namespace :benchmark do
51
60
  puts "This runs a script that fails and shrink happens."
52
61
  puts
53
62
  ENV["RUBYOPT"] = "-W:no-experimental"
63
+ ENV["RUBY_MN_THREADS"] = "1"
64
+ puts "```"
54
65
  sh "ruby", "benchmark/failure_simple.rb"
66
+ puts "```"
55
67
  puts
56
68
  end
57
69
  end
data/benchmark/README.md CHANGED
@@ -20,9 +20,9 @@ Interestingly, both multi-process (`worker: :process`) and multi-thread (`worker
20
20
 
21
21
  The following benchmarks are the results of running the benchmark suite.
22
22
 
23
- - macOS 13.3.1, Apple M1 Pro 10 cores (8 performance and 2 efficiency)
23
+ - macOS 14.4.1, Apple M1 Pro 10 cores (8 performance and 2 efficiency)
24
24
  - ruby 3.3.0 (2023-12-25 revision 5124f9ac75) [arm64-darwin22]
25
- - pbt commit hash 6582b27105ef5e92197b3f52f9c7cf78d731e1e2
25
+ - pbt commit hash 6f2c1cf1ab36d5d83a89fabb22da431e3600fe25
26
26
 
27
27
  ---
28
28
 
@@ -30,68 +30,74 @@ The following benchmarks are the results of running the benchmark suite.
30
30
 
31
31
  This runs a script that does not do any IO or CPU bound work.
32
32
 
33
+ ```
33
34
  ruby benchmark/success_simple.rb
34
- ruby 3.3.0 (2023-12-25 revision 5124f9ac75) [arm64-darwin22]
35
+ ruby 3.3.0 (2023-12-25 revision 5124f9ac75) +MN [arm64-darwin22]
35
36
  Warming up --------------------------------------
36
- ractor 20.000 i/100ms
37
- process 3.000 i/100ms
38
- thread 126.000 i/100ms
39
- none 668.000 i/100ms
37
+ ractor 19.000 i/100ms
38
+ process 2.000 i/100ms
39
+ thread 114.000 i/100ms
40
+ none 234.000 i/100ms
40
41
  Calculating -------------------------------------
41
- ractor 173.91811.5%) i/s - 880.000 in 5.129007s
42
- process 28.8613.5%) i/s - 147.000 in 5.100393s
43
- thread 1.130k 5.5%) i/s - 5.670k in 5.031552s
44
- none 6.534k 2.3%) i/s - 32.732k in 5.011885s
42
+ ractor 75.717 4.0%) i/s - 380.000 in 5.025954s
43
+ process 1.7120.0%) i/s - 10.000 in 5.840821s
44
+ thread 1.019k34.0%) i/s - 4.560k in 5.028774s
45
+ none 1.616k24.1%) i/s - 7.722k in 5.003744s
46
+ ```
45
47
 
46
48
  ### Benchmark success:cpu_bound
47
49
 
48
50
  This runs a script that does CPU bound work.
49
51
 
52
+ ```
50
53
  ruby benchmark/success_cpu_bound.rb
51
- Call tarai function with(9, 4, 0)
52
-
53
- ruby 3.3.0 (2023-12-25 revision 5124f9ac75) [arm64-darwin22]
54
+ ruby 3.3.0 (2023-12-25 revision 5124f9ac75) +MN [arm64-darwin22]
54
55
  Warming up --------------------------------------
55
- ractor 3.000 i/100ms
56
- process 2.000 i/100ms
57
- thread 1.000 i/100ms
58
- none 1.000 i/100ms
56
+ ractor 3.000 i/100ms
57
+ process 2.000 i/100ms
58
+ thread 1.000 i/100ms
59
+ none 1.000 i/100ms
59
60
  Calculating -------------------------------------
60
- ractor 32.7886.1%) i/s - 165.000 in 5.057492s
61
- process 22.0984.5%) i/s - 112.000 in 5.080410s
62
- thread 7.439 (± 0.0%) i/s - 38.000 in 5.108195s
63
- none 7.494 (± 0.0%) i/s - 38.000 in 5.070547s
61
+ ractor 35.6888.4%) i/s - 180.000 in 5.079922s
62
+ process 15.3296.5%) i/s - 78.000 in 5.097040s
63
+ thread 7.654 (± 0.0%) i/s - 39.000 in 5.095252s
64
+ none 7.707 (± 0.0%) i/s - 39.000 in 5.060309s
65
+ ```
64
66
 
65
67
  ### Benchmark success:io_bound
66
68
 
67
69
  This runs a script that does IO bound work.
68
70
 
71
+ ```
69
72
  ruby benchmark/success_io_bound.rb
70
- ruby 3.3.0 (2023-12-25 revision 5124f9ac75) [arm64-darwin22]
73
+ ruby 3.3.0 (2023-12-25 revision 5124f9ac75) +MN [arm64-darwin22]
71
74
  Warming up --------------------------------------
72
- ractor 11.000 i/100ms
73
- process 3.000 i/100ms
74
- thread 17.000 i/100ms
75
- none 22.000 i/100ms
75
+ ractor 11.000 i/100ms
76
+ process 2.000 i/100ms
77
+ thread 16.000 i/100ms
78
+ none 21.000 i/100ms
76
79
  Calculating -------------------------------------
77
- ractor 82.48814.5%) i/s - 407.000 in 5.054559s
78
- process 35.4035.6%) i/s - 177.000 in 5.013818s
79
- thread 143.022 7.7%) i/s - 714.000 in 5.021129s
80
- none 223.2529.0%) i/s - 1.122k in 5.071176s
80
+ ractor 55.907 3.6%) i/s - 286.000 in 5.122098s
81
+ process 9.9160.0%) i/s - 50.000 in 5.044945s
82
+ thread 131.28017.5%) i/s - 656.000 in 5.148757s
83
+ none 144.3644.8%) i/s - 735.000 in 5.102093s
84
+ ```
81
85
 
82
86
  ### Benchmark failure:simple
83
87
 
84
88
  This runs a script that fails and shrink happens.
85
89
 
90
+ ```
86
91
  ruby benchmark/failure_simple.rb
87
- ruby 3.3.0 (2023-12-25 revision 5124f9ac75) [arm64-darwin22]
92
+ ruby 3.3.0 (2023-12-25 revision 5124f9ac75) +MN [arm64-darwin22]
88
93
  Warming up --------------------------------------
89
- ractor 6.000 i/100ms
90
- process 1.000 i/100ms
91
- thread 9.000 i/100ms
92
- none 815.000 i/100ms
94
+ ractor 7.000 i/100ms
95
+ process 1.000 i/100ms
96
+ thread 14.000 i/100ms
97
+ none 288.000 i/100ms
93
98
  Calculating -------------------------------------
94
- ractor 62.77015.9%) i/s - 306.000 in 5.009858s
95
- process 1.783 (± 0.0%) i/s - 9.000 in 5.049606s
96
- thread 85.218 9.4%) i/s - 423.000 in 5.007178s
97
- none 5.387k 3.3%) i/s - 27.710k in 5.149867s
99
+ ractor 13.104 7.6%) i/s - 70.000 in 5.358898s
100
+ process 0.060 (± 0.0%) i/s - 1.000 in 16.545525s
101
+ thread 111.49320.6%) i/s - 532.000 in 5.012879s
102
+ none 2.445k19.0%) i/s - 12.096k in 5.086548s
103
+ ```
@@ -16,9 +16,6 @@ end
16
16
 
17
17
  a, b, c = [9, 4, 0]
18
18
 
19
- puts "Call tarai function with(#{a}, #{b}, #{c})"
20
- puts
21
-
22
19
  Benchmark.ips do |x|
23
20
  x.report("ractor") do
24
21
  Pbt.assert(worker: :ractor) do
@@ -9,6 +9,7 @@ module Pbt
9
9
  :num_runs,
10
10
  :seed,
11
11
  :thread_report_on_exception,
12
+ :experimental_ractor_rspec_integration,
12
13
  keyword_init: true
13
14
  ) do
14
15
  # @param verbose [Boolean] Whether to print verbose output. Default is `false`.
@@ -16,12 +17,14 @@ module Pbt
16
17
  # @param num_runs [Integer] The number of runs to perform. Default is `100`.
17
18
  # @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.
18
19
  # @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`.
19
21
  def initialize(
20
22
  verbose: false,
21
23
  worker: :none,
22
24
  num_runs: 100,
23
25
  seed: Random.new.seed,
24
- thread_report_on_exception: false
26
+ thread_report_on_exception: false,
27
+ experimental_ractor_rspec_integration: false
25
28
  )
26
29
  super
27
30
  end
@@ -34,7 +34,11 @@ module Pbt
34
34
  # @param val [Object]
35
35
  # @return [void]
36
36
  def run(val)
37
- @predicate.call(val)
37
+ if val.is_a?(Hash)
38
+ @predicate.call(**val)
39
+ else
40
+ @predicate.call(val)
41
+ end
38
42
  end
39
43
 
40
44
  # Run the predicate with the generated `val` in a Ractor.
@@ -0,0 +1,64 @@
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)
@@ -0,0 +1,47 @@
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/grosser/parallel) is required to use worker `:rator` and `:experimental_ractor_rspec_integration`. Please add `gem 'parallel'` 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
@@ -0,0 +1,72 @@
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 = 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, val) do |class_name, method_name, val|
33
+ klass = RSpecAdapter.const_get(class_name)
34
+ if val.is_a?(Hash)
35
+ klass.new.send(method_name, **val)
36
+ else
37
+ klass.new.send(method_name, *val)
38
+ end
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ # @return [void]
45
+ def define_ractor_callable_class
46
+ # The @method_name is invisible in the Class.new block, so we need to assign it to a local variable.
47
+ method_name = @method_name
48
+
49
+ inspector = extract_predicate_source_code
50
+
51
+ RSpecAdapter.const_set(@class_name, Class.new do
52
+ include ::RSpec::Matchers
53
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
54
+ def #{method_name}(#{inspector.method_params})
55
+ #{inspector.method_body}
56
+ end
57
+ RUBY
58
+ end)
59
+ end
60
+
61
+ # @return [PredicateBlockInspector]
62
+ def extract_predicate_source_code
63
+ filepath, line = @predicate.source_location
64
+ PredicateBlockInspector.new(line).tap do |inspector|
65
+ res = Prism.parse_file(filepath)
66
+ res.value.statements.accept(inspector)
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -43,31 +43,48 @@ module Pbt
43
43
  end
44
44
  end
45
45
 
46
- suppress_exception_report_for_ractor(config) do
46
+ setup_for_ractor(config, property) do
47
47
  run_it(property, source_values, config).to_run_details(config)
48
48
  end
49
49
  end
50
50
 
51
51
  private
52
52
 
53
- # If using Ractor, so many exception reports happen in Ractor and a console gets too messy. Suppress them to avoid that.
53
+ # If using Ractor, some extra configurations are available and they need to be set up.
54
+ #
55
+ # - `:thread_report_on_exception`
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.
54
59
  #
55
60
  # @param config [Hash] Configuration parameters.
61
+ # @param property [Property]
56
62
  # @param block [Proc]
57
- def suppress_exception_report_for_ractor(config, &block)
63
+ def setup_for_ractor(config, property, &block)
58
64
  if config[:worker] == :ractor
59
65
  original_report_on_exception = Thread.report_on_exception
60
66
  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
61
75
  end
62
76
 
63
77
  yield
64
78
  ensure
65
- Thread.report_on_exception = original_report_on_exception if config[:worker] == :ractor
79
+ if config[:worker] == :ractor
80
+ Thread.report_on_exception = original_report_on_exception
81
+ property.teardown_rspec_integration if config[:experimental_ractor_rspec_integration]
82
+ end
66
83
  end
67
84
 
68
85
  # Run the property test for each value.
69
86
  #
70
- # @param property [Proc] Property to test.
87
+ # @param property [Property] Property to test.
71
88
  # @param source_values [Enumerator] Enumerator of values to test.
72
89
  # @param config [Hash] Configuration parameters.
73
90
  # @return [RunExecution] Result of the test.
@@ -88,7 +105,7 @@ module Pbt
88
105
  runner.run_execution
89
106
  end
90
107
 
91
- # @param property [Proc]
108
+ # @param property [Property] Property to test.
92
109
  # @param runner [RunnerIterator]
93
110
  # @return [void]
94
111
  def run_it_in_sequential(property, runner)
@@ -105,7 +122,7 @@ module Pbt
105
122
  end
106
123
  end
107
124
 
108
- # @param property [Proc]
125
+ # @param property [Property] Property to test.
109
126
  # @param runner [RunnerIterator]
110
127
  # @return [void]
111
128
  def run_it_in_ractors(property, runner)
@@ -115,13 +132,28 @@ module Pbt
115
132
  c.ractor.take
116
133
  runner.handle_result(c)
117
134
  rescue => e
118
- c.exception = e.cause # Ractor error is wrapped in a Ractor::RemoteError. We need to get the cause.
135
+ handle_ractor_error(e.cause, c)
119
136
  runner.handle_result(c)
120
137
  break # Ignore the rest of the cases. Just pick up the first failure.
121
138
  end
122
139
  end
123
140
 
124
- # @param property [Proc]
141
+ def handle_ractor_error(cause, c)
142
+ # Ractor error is wrapped in a Ractor::RemoteError. We need to get the cause.
143
+ unless defined?(Pbt::Check::RSpecAdapter) && cause.is_a?(Pbt::Check::RSpecAdapter::ExpectationNotMet) # Unknown error.
144
+ c.exception = cause
145
+ return
146
+ end
147
+
148
+ # Convert Pbt's custom error to RSpec's error.
149
+ begin
150
+ RSpec::Expectations::ExpectationHelper.handle_failure(cause.matcher, cause.custom_message, cause.failure_message_method)
151
+ rescue RSpec::Expectations::ExpectationNotMetError => e # The class inherits Exception, not StandardError.
152
+ c.exception = e
153
+ end
154
+ end
155
+
156
+ # @param property [Property] Property to test.
125
157
  # @param runner [RunnerIterator]
126
158
  # @return [void]
127
159
  def run_it_in_threads(property, runner)
@@ -142,7 +174,7 @@ module Pbt
142
174
  end
143
175
  end
144
176
 
145
- # @param property [Proc]
177
+ # @param property [Property] Property to test.
146
178
  # @param runner [RunnerIterator]
147
179
  # @return [void]
148
180
  def run_it_in_processes(property, runner)
@@ -42,6 +42,8 @@ module Pbt
42
42
  end
43
43
 
44
44
  def error_backtrace
45
+ return "" if @run_details.error_instance.backtrace_locations.nil? # It can be nil.
46
+
45
47
  i = @run_details.verbose ? -1 : 10
46
48
  " #{@run_details.error_instance.backtrace_locations[..i].join("\n ")}"
47
49
  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.2.0"
4
+ VERSION = "0.3.0"
5
5
  end
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.2.0
4
+ version: 0.3.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-04-16 00:00:00.000000000 Z
11
+ date: 2024-04-21 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description:
14
14
  email:
@@ -46,6 +46,9 @@ 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
49
52
  - lib/pbt/check/runner_iterator.rb
50
53
  - lib/pbt/check/runner_methods.rb
51
54
  - lib/pbt/check/tosser.rb