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 +4 -4
- data/CHANGELOG.md +6 -0
- data/README.md +27 -8
- data/Rakefile +12 -0
- data/benchmark/README.md +46 -40
- data/benchmark/success_cpu_bound.rb +0 -3
- data/lib/pbt/check/configuration.rb +4 -1
- data/lib/pbt/check/property.rb +5 -1
- data/lib/pbt/check/rspec_adapter/integration.rb +64 -0
- data/lib/pbt/check/rspec_adapter/predicate_block_inspector.rb +47 -0
- data/lib/pbt/check/rspec_adapter/property_extension.rb +72 -0
- data/lib/pbt/check/runner_methods.rb +42 -10
- data/lib/pbt/reporter/run_details_reporter.rb +2 -0
- data/lib/pbt/version.rb +1 -1
- metadata +5 -2
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA256:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: 8415f37c2819e6370e7d418cb96d8ae2467fec24cfca1e64088c925874dc0c34
         | 
| 4 | 
            +
              data.tar.gz: f6b38e54ec98fa669cc379c6c181e210d5fab02fc076a343e941aa17cb5be476
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 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 | 
            -
            - [ | 
| 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 | 
            -
            - [ ]  | 
| 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  | 
| 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  | 
| 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     | 
| 37 | 
            -
            process      | 
| 38 | 
            -
            thread    | 
| 39 | 
            -
            none    | 
| 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 | 
| 42 | 
            -
            process | 
| 43 | 
            -
            thread      1. | 
| 44 | 
            -
            none       | 
| 42 | 
            +
                          ractor     75.717 (± 4.0%) i/s -    380.000 in   5.025954s
         | 
| 43 | 
            +
                         process      1.712 (± 0.0%) i/s -     10.000 in   5.840821s
         | 
| 44 | 
            +
                          thread      1.019k (±34.0%) i/s -      4.560k in   5.028774s
         | 
| 45 | 
            +
                            none      1.616k (±24.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 | 
            -
             | 
| 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      | 
| 61 | 
            -
            process      | 
| 62 | 
            -
            thread      7. | 
| 63 | 
            -
            none      7. | 
| 61 | 
            +
                          ractor     35.688 (± 8.4%) i/s -    180.000 in   5.079922s
         | 
| 62 | 
            +
                         process     15.329 (± 6.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      | 
| 74 | 
            -
            thread     | 
| 75 | 
            -
            none     | 
| 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      | 
| 78 | 
            -
            process | 
| 79 | 
            -
            thread     | 
| 80 | 
            -
            none     | 
| 80 | 
            +
                          ractor     55.907 (± 3.6%) i/s -    286.000 in   5.122098s
         | 
| 81 | 
            +
                         process      9.916 (± 0.0%) i/s -     50.000 in   5.044945s
         | 
| 82 | 
            +
                          thread    131.280 (±17.5%) i/s -    656.000 in   5.148757s
         | 
| 83 | 
            +
                            none    144.364 (± 4.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      | 
| 90 | 
            -
            process     1.000 i/100ms
         | 
| 91 | 
            -
            thread | 
| 92 | 
            -
            none    | 
| 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      | 
| 95 | 
            -
            process       | 
| 96 | 
            -
            thread | 
| 97 | 
            -
            none       | 
| 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.493 (±20.6%) i/s -    532.000 in   5.012879s
         | 
| 102 | 
            +
                            none      2.445k (±19.0%) i/s -     12.096k in   5.086548s
         | 
| 103 | 
            +
            ```
         | 
| @@ -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
         | 
    
        data/lib/pbt/check/property.rb
    CHANGED
    
    | @@ -34,7 +34,11 @@ module Pbt | |
| 34 34 | 
             
                  # @param val [Object]
         | 
| 35 35 | 
             
                  # @return [void]
         | 
| 36 36 | 
             
                  def run(val)
         | 
| 37 | 
            -
                     | 
| 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 | 
            -
                     | 
| 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,  | 
| 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  | 
| 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 | 
            -
                     | 
| 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 [ | 
| 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 [ | 
| 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 [ | 
| 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 | 
            -
                       | 
| 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 | 
            -
                   | 
| 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 [ | 
| 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)
         | 
    
        data/lib/pbt/version.rb
    CHANGED
    
    
    
        metadata
    CHANGED
    
    | @@ -1,14 +1,14 @@ | |
| 1 1 | 
             
            --- !ruby/object:Gem::Specification
         | 
| 2 2 | 
             
            name: pbt
         | 
| 3 3 | 
             
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            -
              version: 0. | 
| 4 | 
            +
              version: 0.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- | 
| 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
         |