pbt 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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