├── .github └── workflows │ └── main.yml ├── .gitignore ├── .standard.yml ├── CHANGELOG.md ├── Gemfile ├── Gemfile.lock ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin ├── console ├── m ├── rake ├── setup └── tapioca ├── docs ├── code_of_conduct.md ├── configuring_minitest.md ├── configuring_rspec.md ├── example_test.md ├── faq │ ├── existing_tests.md │ ├── mocking_http.md │ ├── mocking_the_subject.md │ ├── mocking_time.md │ ├── partial_mocks.md │ └── verifying_real_interactions.md ├── img │ ├── delegator_tree.png │ ├── example_test.png │ ├── extract_transform_load.png │ ├── mocktail_sorbet.jpg │ ├── mocktail_untyped.jpg │ └── spacer.png ├── installation_sorbet.md ├── installation_untyped.md ├── other_uses.md ├── stubbing_and_verifying.md ├── support │ ├── api.md │ ├── example_test.rb │ └── glossary.md ├── tdd.md └── tdd │ ├── class_methods.md │ ├── poro.md │ ├── poro │ ├── dependency_inception.md │ └── dependency_injection.md │ └── third_party.md ├── lib ├── mocktail.rb └── mocktail │ ├── collects_calls.rb │ ├── debug.rb │ ├── dsl.rb │ ├── errors.rb │ ├── explains_nils.rb │ ├── explains_thing.rb │ ├── grabs_original_method_parameters.rb │ ├── handles_dry_call.rb │ ├── handles_dry_call │ ├── fulfills_stubbing.rb │ ├── fulfills_stubbing │ │ ├── describes_unsatisfied_stubbing.rb │ │ └── finds_satisfaction.rb │ ├── logs_call.rb │ └── validates_arguments.rb │ ├── handles_dry_new_call.rb │ ├── imitates_type.rb │ ├── imitates_type │ ├── ensures_imitation_support.rb │ ├── makes_double.rb │ └── makes_double │ │ ├── declares_dry_class.rb │ │ ├── declares_dry_class │ │ └── reconstructs_call.rb │ │ └── gathers_fakeable_instance_methods.rb │ ├── initialize_based_on_type_system_mode_switching.rb │ ├── initializes_mocktail.rb │ ├── matcher_presentation.rb │ ├── matchers.rb │ ├── matchers │ ├── any.rb │ ├── base.rb │ ├── captor.rb │ ├── includes.rb │ ├── includes_hash.rb │ ├── includes_key.rb │ ├── includes_string.rb │ ├── is_a.rb │ ├── matches.rb │ ├── not.rb │ ├── numeric.rb │ └── that.rb │ ├── raises_neato_no_method_error.rb │ ├── records_demonstration.rb │ ├── registers_matcher.rb │ ├── registers_stubbing.rb │ ├── replaces_next.rb │ ├── replaces_type.rb │ ├── replaces_type │ ├── redefines_new.rb │ ├── redefines_singleton_methods.rb │ └── runs_sorbet_sig_blocks_before_replacement.rb │ ├── resets_state.rb │ ├── share │ ├── bind.rb │ ├── cleans_backtrace.rb │ ├── creates_identifier.rb │ ├── determines_matching_calls.rb │ ├── stringifies_call.rb │ └── stringifies_method_name.rb │ ├── simulates_argument_error.rb │ ├── simulates_argument_error │ ├── reconciles_args_with_params.rb │ ├── recreates_message.rb │ └── transforms_params.rb │ ├── sorbet.rb │ ├── sorbet │ ├── mocktail.rb │ └── mocktail │ │ ├── collects_calls.rb │ │ ├── debug.rb │ │ ├── dsl.rb │ │ ├── errors.rb │ │ ├── explains_nils.rb │ │ ├── explains_thing.rb │ │ ├── grabs_original_method_parameters.rb │ │ ├── handles_dry_call.rb │ │ ├── handles_dry_call │ │ ├── fulfills_stubbing.rb │ │ ├── fulfills_stubbing │ │ │ ├── describes_unsatisfied_stubbing.rb │ │ │ └── finds_satisfaction.rb │ │ ├── logs_call.rb │ │ └── validates_arguments.rb │ │ ├── handles_dry_new_call.rb │ │ ├── imitates_type.rb │ │ ├── imitates_type │ │ ├── ensures_imitation_support.rb │ │ ├── makes_double.rb │ │ └── makes_double │ │ │ ├── declares_dry_class.rb │ │ │ ├── declares_dry_class │ │ │ └── reconstructs_call.rb │ │ │ └── gathers_fakeable_instance_methods.rb │ │ ├── initialize_based_on_type_system_mode_switching.rb │ │ ├── initializes_mocktail.rb │ │ ├── matcher_presentation.rb │ │ ├── matchers.rb │ │ ├── matchers │ │ ├── any.rb │ │ ├── base.rb │ │ ├── captor.rb │ │ ├── includes.rb │ │ ├── includes_hash.rb │ │ ├── includes_key.rb │ │ ├── includes_string.rb │ │ ├── is_a.rb │ │ ├── matches.rb │ │ ├── not.rb │ │ ├── numeric.rb │ │ └── that.rb │ │ ├── raises_neato_no_method_error.rb │ │ ├── records_demonstration.rb │ │ ├── registers_matcher.rb │ │ ├── registers_stubbing.rb │ │ ├── replaces_next.rb │ │ ├── replaces_type.rb │ │ ├── replaces_type │ │ ├── redefines_new.rb │ │ ├── redefines_singleton_methods.rb │ │ └── runs_sorbet_sig_blocks_before_replacement.rb │ │ ├── resets_state.rb │ │ ├── share │ │ ├── bind.rb │ │ ├── cleans_backtrace.rb │ │ ├── creates_identifier.rb │ │ ├── determines_matching_calls.rb │ │ ├── stringifies_call.rb │ │ └── stringifies_method_name.rb │ │ ├── simulates_argument_error.rb │ │ ├── simulates_argument_error │ │ ├── reconciles_args_with_params.rb │ │ ├── recreates_message.rb │ │ └── transforms_params.rb │ │ ├── sorbet.rb │ │ ├── stringifies_method_signature.rb │ │ ├── typed.rb │ │ ├── value.rb │ │ ├── value │ │ ├── cabinet.rb │ │ ├── call.rb │ │ ├── demo_config.rb │ │ ├── double.rb │ │ ├── double_data.rb │ │ ├── explanation.rb │ │ ├── explanation_data.rb │ │ ├── fake_method_data.rb │ │ ├── matcher_registry.rb │ │ ├── no_explanation_data.rb │ │ ├── signature.rb │ │ ├── stubbing.rb │ │ ├── top_shelf.rb │ │ ├── type_replacement.rb │ │ ├── type_replacement_data.rb │ │ ├── unsatisfying_call.rb │ │ └── unsatisfying_call_explanation.rb │ │ ├── verifies_call.rb │ │ ├── verifies_call │ │ ├── finds_verifiable_calls.rb │ │ ├── raises_verification_error.rb │ │ └── raises_verification_error │ │ │ └── gathers_calls_of_method.rb │ │ └── version.rb │ ├── stringifies_method_signature.rb │ ├── typed.rb │ ├── value.rb │ ├── value │ ├── cabinet.rb │ ├── call.rb │ ├── demo_config.rb │ ├── double.rb │ ├── double_data.rb │ ├── explanation.rb │ ├── explanation_data.rb │ ├── fake_method_data.rb │ ├── matcher_registry.rb │ ├── no_explanation_data.rb │ ├── signature.rb │ ├── stubbing.rb │ ├── top_shelf.rb │ ├── type_replacement.rb │ ├── type_replacement_data.rb │ ├── unsatisfying_call.rb │ └── unsatisfying_call_explanation.rb │ ├── verifies_call.rb │ ├── verifies_call │ ├── finds_verifiable_calls.rb │ ├── raises_verification_error.rb │ └── raises_verification_error │ │ └── gathers_calls_of_method.rb │ └── version.rb ├── mocktail.gemspec ├── rbi ├── mocktail-pregenerated.rbi ├── mocktail.rbi └── sorbet-runtime.rbi ├── script ├── build ├── setup ├── spoom_me ├── strip_sigils ├── test ├── test_double_require_warnings └── update ├── sorbet ├── config ├── rbi │ ├── annotations │ │ └── rainbow.rbi │ └── gems │ │ ├── ast@2.4.2.rbi │ │ ├── diff-lcs@1.5.0.rbi │ │ ├── docile@1.4.0.rbi │ │ ├── json@2.6.3.rbi │ │ ├── language_server-protocol@3.17.0.3.rbi │ │ ├── lint_roller@1.0.0.rbi │ │ ├── m@1.6.1.rbi │ │ ├── method_source@1.0.0.rbi │ │ ├── minitest@5.18.0.rbi │ │ ├── netrc@0.11.0.rbi │ │ ├── parallel@1.23.0.rbi │ │ ├── parser@3.2.2.1.rbi │ │ ├── rainbow@3.1.1.rbi │ │ ├── rake@13.0.6.rbi │ │ ├── rbi@0.0.16.rbi │ │ ├── regexp_parser@2.8.0.rbi │ │ ├── rexml@3.2.5.rbi │ │ ├── rubocop-ast@1.29.0.rbi │ │ ├── rubocop-performance@1.18.0.rbi │ │ ├── rubocop-sorbet@0.7.0.rbi │ │ ├── rubocop@1.52.0.rbi │ │ ├── ruby-progressbar@1.13.0.rbi │ │ ├── simplecov-html@0.12.3.rbi │ │ ├── simplecov@0.22.0.rbi │ │ ├── simplecov_json_formatter@0.1.4.rbi │ │ ├── spoom@1.2.1.rbi │ │ ├── standard-custom@1.0.1.rbi │ │ ├── standard-performance@1.1.0.rbi │ │ ├── standard@1.29.0.rbi │ │ ├── tapioca@0.11.6.rbi │ │ ├── thor@1.2.2.rbi │ │ ├── unicode-display_width@2.4.2.rbi │ │ ├── unparser@0.6.7.rbi │ │ ├── yard-sorbet@0.8.1.rbi │ │ └── yard@0.9.34.rbi └── tapioca │ ├── config.yml │ └── require.rb ├── spoom_data ├── 05f0c49.json ├── 1657012.json ├── 167434b.json ├── 1d0ba5b.json ├── 1ffa724.json ├── 2129b3d.json ├── 24b1c92.json ├── 2c46aee.json ├── 305ec0b.json ├── 30e9528.json ├── 4638cd5.json ├── 47c7dad.json ├── 4b1edef.json ├── 4de157f.json ├── 526e7db.json ├── 5d093b9.json ├── 5db3b43.json ├── 5fe2a65.json ├── 6891312.json ├── 6b0fef4.json ├── 6b83d12.json ├── 74c83c2.json ├── 7644ff4.json ├── 79054db.json ├── 814e515.json ├── 88c3b60.json ├── 8bd4b6e.json ├── 93f8153.json ├── 95242fe.json ├── 97f4c09.json ├── a13d150.json ├── a17f215.json ├── b705a9d.json ├── ba19195.json ├── bef51ca.json ├── dbb595a.json ├── e8fab92.json ├── f166c87.json ├── f57992d.json ├── f5a1e40.json ├── f6d6431.json ├── f72b67c.json └── fc2f231.json ├── spoom_report.html ├── src ├── mocktail.rb └── mocktail │ ├── collects_calls.rb │ ├── debug.rb │ ├── dsl.rb │ ├── errors.rb │ ├── explains_nils.rb │ ├── explains_thing.rb │ ├── grabs_original_method_parameters.rb │ ├── handles_dry_call.rb │ ├── handles_dry_call │ ├── fulfills_stubbing.rb │ ├── fulfills_stubbing │ │ ├── describes_unsatisfied_stubbing.rb │ │ └── finds_satisfaction.rb │ ├── logs_call.rb │ └── validates_arguments.rb │ ├── handles_dry_new_call.rb │ ├── imitates_type.rb │ ├── imitates_type │ ├── ensures_imitation_support.rb │ ├── makes_double.rb │ └── makes_double │ │ ├── declares_dry_class.rb │ │ ├── declares_dry_class │ │ └── reconstructs_call.rb │ │ └── gathers_fakeable_instance_methods.rb │ ├── initialize_based_on_type_system_mode_switching.rb │ ├── initializes_mocktail.rb │ ├── matcher_presentation.rb │ ├── matchers.rb │ ├── matchers │ ├── any.rb │ ├── base.rb │ ├── captor.rb │ ├── includes.rb │ ├── includes_hash.rb │ ├── includes_key.rb │ ├── includes_string.rb │ ├── is_a.rb │ ├── matches.rb │ ├── not.rb │ ├── numeric.rb │ └── that.rb │ ├── raises_neato_no_method_error.rb │ ├── records_demonstration.rb │ ├── registers_matcher.rb │ ├── registers_stubbing.rb │ ├── replaces_next.rb │ ├── replaces_type.rb │ ├── replaces_type │ ├── redefines_new.rb │ ├── redefines_singleton_methods.rb │ └── runs_sorbet_sig_blocks_before_replacement.rb │ ├── resets_state.rb │ ├── share │ ├── bind.rb │ ├── cleans_backtrace.rb │ ├── creates_identifier.rb │ ├── determines_matching_calls.rb │ ├── stringifies_call.rb │ └── stringifies_method_name.rb │ ├── simulates_argument_error.rb │ ├── simulates_argument_error │ ├── reconciles_args_with_params.rb │ ├── recreates_message.rb │ └── transforms_params.rb │ ├── sorbet.rb │ ├── stringifies_method_signature.rb │ ├── typed.rb │ ├── value.rb │ ├── value │ ├── cabinet.rb │ ├── call.rb │ ├── demo_config.rb │ ├── double.rb │ ├── double_data.rb │ ├── explanation.rb │ ├── explanation_data.rb │ ├── fake_method_data.rb │ ├── matcher_registry.rb │ ├── no_explanation_data.rb │ ├── signature.rb │ ├── stubbing.rb │ ├── top_shelf.rb │ ├── type_replacement.rb │ ├── type_replacement_data.rb │ ├── unsatisfying_call.rb │ └── unsatisfying_call_explanation.rb │ ├── verifies_call.rb │ ├── verifies_call │ ├── finds_verifiable_calls.rb │ ├── raises_verification_error.rb │ └── raises_verification_error │ │ └── gathers_calls_of_method.rb │ └── version.rb ├── sub_projects ├── rbi_generator │ ├── Gemfile │ ├── Gemfile.lock │ └── sorbet │ │ └── rbi │ │ └── gems │ │ └── .gitattributes ├── sorbet_user │ ├── Gemfile │ ├── Gemfile.lock │ ├── Rakefile │ ├── bin │ │ └── tapioca │ ├── rbi │ │ └── mocktail.rbi │ ├── script │ │ └── test │ ├── sorbet │ │ ├── config │ │ ├── rbi │ │ │ └── gems │ │ │ │ ├── .gitattributes │ │ │ │ ├── ast@2.4.3.rbi │ │ │ │ ├── benchmark@0.4.0.rbi │ │ │ │ ├── erubi@1.13.1.rbi │ │ │ │ ├── json@2.10.2.rbi │ │ │ │ ├── language_server-protocol@3.17.0.4.rbi │ │ │ │ ├── lint_roller@1.1.0.rbi │ │ │ │ ├── logger@1.7.0.rbi │ │ │ │ ├── m@1.6.2.rbi │ │ │ │ ├── method_source@1.1.0.rbi │ │ │ │ ├── minitest@5.25.5.rbi │ │ │ │ ├── mocktail@2.0.0.rbi │ │ │ │ ├── netrc@0.11.0.rbi │ │ │ │ ├── parallel@1.26.3.rbi │ │ │ │ ├── parser@3.3.7.4.rbi │ │ │ │ ├── prism@1.4.0.rbi │ │ │ │ ├── racc@1.8.1.rbi │ │ │ │ ├── rainbow@3.1.1.rbi │ │ │ │ ├── rake@13.2.1.rbi │ │ │ │ ├── rbi@0.3.1.rbi │ │ │ │ ├── rbs@3.9.2.rbi │ │ │ │ ├── regexp_parser@2.10.0.rbi │ │ │ │ ├── rubocop-ast@1.43.0.rbi │ │ │ │ ├── rubocop-performance@1.24.0.rbi │ │ │ │ ├── rubocop-sorbet@0.9.0.rbi │ │ │ │ ├── rubocop@1.73.2.rbi │ │ │ │ ├── ruby-progressbar@1.13.0.rbi │ │ │ │ ├── sorbet-eraser@0.3.1.rbi │ │ │ │ ├── spoom@1.6.1.rbi │ │ │ │ ├── standard-custom@1.0.2.rbi │ │ │ │ ├── standard-performance@1.7.0.rbi │ │ │ │ ├── standard-sorbet@0.0.3.rbi │ │ │ │ ├── standard@1.47.0.rbi │ │ │ │ ├── tapioca@0.16.11.rbi │ │ │ │ ├── thor@1.3.2.rbi │ │ │ │ ├── unicode-display_width@3.1.4.rbi │ │ │ │ ├── unicode-emoji@4.0.4.rbi │ │ │ │ ├── yard-sorbet@0.9.0.rbi │ │ │ │ └── yard@0.9.37.rbi │ │ └── tapioca │ │ │ ├── config.yml │ │ │ └── require.rb │ └── test │ │ ├── ensure_type_safety_test.rb │ │ ├── paint_by_number_test.rb │ │ ├── sorbet_test.rb │ │ └── test_helper.rb └── untyped_user │ ├── Gemfile │ ├── Gemfile.lock │ ├── antitype_test.rb │ └── script │ └── test └── test ├── mocktail_test.rb ├── safe ├── call_count_test.rb ├── dsl_test.rb ├── explain_test.rb ├── kwargs_vs_options_hash_test.rb ├── mocking_methodful_classes_test.rb ├── of_next_test.rb ├── of_test.rb ├── replace_test.rb ├── reset_test.rb ├── stub_test.rb └── verify_test.rb ├── support ├── let.rb ├── sorbet_override.rb └── sorbet_stubs.rb ├── test_helper.rb └── unit ├── imitates_type └── makes_double │ └── declares_dry_class_test.rb ├── matcher_presentation_test.rb ├── matchers ├── base_test.rb ├── captor_test.rb ├── includes_test.rb ├── matches_test.rb ├── numeric_test.rb └── that_test.rb ├── raises_neato_no_method_error_test.rb ├── registers_matcher_test.rb ├── share ├── bind_test.rb └── creates_identifier_test.rb ├── simulates_argument_error ├── cleans_backtrace_test.rb ├── reconciles_args_with_params_test.rb ├── recreates_message_test.rb └── transforms_params_test.rb ├── stringifies_method_signature_test.rb └── verifies_call └── raises_verification_error └── stringifies_call_test.rb /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Ruby 2 | 3 | on: [push,pull_request] 4 | 5 | jobs: 6 | build: 7 | strategy: 8 | matrix: 9 | os: [ ubuntu-latest ] 10 | ruby-version: ['3.0', '3.1', '3.2'] 11 | 12 | runs-on: ${{ matrix.os }} 13 | 14 | steps: 15 | - uses: actions/checkout@v3 16 | - name: Set up Ruby 17 | uses: ruby/setup-ruby@v1 18 | with: 19 | ruby-version: ${{ matrix.ruby-version }} 20 | - name: Install gem dependencies 21 | run: | 22 | gem install bundler 23 | ./script/setup 24 | - name: Run the test script 25 | run: ./script/test 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated RBI that will be moved into rbi/ 2 | /sub_projects/rbi_generator/sorbet/rbi/gems/*.rbi 3 | 4 | /.bundle/ 5 | /.yardoc 6 | /_yardoc/ 7 | /coverage/ 8 | /doc/ 9 | /pkg/ 10 | /spec/reports/ 11 | /tmp/ 12 | -------------------------------------------------------------------------------- /.standard.yml: -------------------------------------------------------------------------------- 1 | ruby_version: 3.0 2 | 3 | plugins: 4 | - standard-sorbet 5 | 6 | ignore: 7 | - "{docs,lib,sub_projects/untyped_user}/**/*.rb": 8 | - Sorbet/FalseSigil 9 | 10 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | # Specify your gem's dependencies in mocktail.gemspec 4 | gemspec 5 | 6 | gem "rake" 7 | gem "minitest" 8 | gem "standard" 9 | gem "standard-sorbet" 10 | gem "simplecov" 11 | gem "m" 12 | 13 | gem "sorbet-static" 14 | gem "tapioca" 15 | gem "spoom" 16 | 17 | gem "bigdecimal" 18 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 Test Double, Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mocktail 2 | 3 | Mocktail is a mocking library for Ruby built with modern Ruby 3 APIs and the 4 | _only one_ with first-class support for type checking with 5 | [Sorbet](https://sorbet.org). Mocktail was created to accelerate test-driven 6 | development in Ruby and is designed to prevent common problems that lead to 7 | brittle and confusing tests. 8 | 9 | Your first choice is a consequential one: **how do you want your Mocktail?** 10 | 11 |
19 | 20 | You can also skip all the fun stuff and dive straight into the [full API documentation](/docs/support/api.md). 21 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rake/testtask" 3 | require "standard/rake" 4 | 5 | Rake::TestTask.new(:test) do |t| 6 | t.libs << "test" 7 | t.test_files = FileList["test/**/*_test.rb"] 8 | end 9 | 10 | task default: [:test, "standard:fix"] 11 | -------------------------------------------------------------------------------- /bin/m: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'm' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | require "pathname" 12 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 13 | Pathname.new(__FILE__).realpath) 14 | 15 | bundle_binstub = File.expand_path("../bundle", __FILE__) 16 | 17 | if File.file?(bundle_binstub) 18 | if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/ 19 | load(bundle_binstub) 20 | else 21 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. 22 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") 23 | end 24 | end 25 | 26 | require "rubygems" 27 | require "bundler/setup" 28 | 29 | load Gem.bin_path("m", "m") 30 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'rake' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | require "pathname" 12 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 13 | Pathname.new(__FILE__).realpath) 14 | 15 | bundle_binstub = File.expand_path("../bundle", __FILE__) 16 | 17 | if File.file?(bundle_binstub) 18 | if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/ 19 | load(bundle_binstub) 20 | else 21 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. 22 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") 23 | end 24 | end 25 | 26 | require "rubygems" 27 | require "bundler/setup" 28 | 29 | load Gem.bin_path("rake", "rake") 30 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /bin/tapioca: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'tapioca' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | require "pathname" 12 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 13 | Pathname.new(__FILE__).realpath) 14 | 15 | bundle_binstub = File.expand_path("../bundle", __FILE__) 16 | 17 | if File.file?(bundle_binstub) 18 | if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/ 19 | load(bundle_binstub) 20 | else 21 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. 22 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") 23 | end 24 | end 25 | 26 | require "rubygems" 27 | require "bundler/setup" 28 | 29 | load Gem.bin_path("tapioca", "tapioca") 30 | -------------------------------------------------------------------------------- /docs/code_of_conduct.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | 3 | This project follows Test Double's [code of 4 | conduct](https://testdouble.com/code-of-conduct) for all community interactions, 5 | including (but not limited to) one-on-one communications, public posts/comments, 6 | code reviews, pull requests, and GitHub issues. If violations occur, Test Double 7 | will take any action they deem appropriate for the infraction, up to and 8 | including blocking a user from the organization's repositories. 9 | -------------------------------------------------------------------------------- /docs/configuring_minitest.md: -------------------------------------------------------------------------------- 1 | ## Configuring Minitest 2 | 3 | If you're using Minitest, you'll want to plunk this into a test helper: 4 | 5 | ```ruby 6 | class Minitest::Test 7 | include Mocktail::DSL 8 | 9 | def teardown 10 | super 11 | Mocktail.reset 12 | end 13 | end 14 | ``` 15 | 16 | Finally, the real work of faking things can begin. 17 | 18 | **Incorporate Mocktail into your [test-driven development workflow](tdd.md).** 19 | 20 | **Leverage Mocktail's metaprogramming essence for [other testing utilities](other_uses.md).** 21 | -------------------------------------------------------------------------------- /docs/configuring_rspec.md: -------------------------------------------------------------------------------- 1 | ## Configuring RSpec 2 | 3 | If you find yourself in an RSpec establishment, your spec helper just needs 4 | this: 5 | 6 | ```ruby 7 | RSpec.configure do |config| 8 | config.include Mocktail::DSL 9 | 10 | config.after(:example) { Mocktail.reset } 11 | end 12 | ``` 13 | 14 | But configuration will only get you so far. Once your prep is complete, it's 15 | time to roll up your sleeves and get to work. 16 | 17 | **Incorporate Mocktail into your [test-driven development workflow](tdd.md).** 18 | 19 | **Leverage Mocktail's metaprogramming essence for [other testing utilities](other_uses.md).** 20 | -------------------------------------------------------------------------------- /docs/img/delegator_tree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/testdouble/mocktail/b2e1933fe109fe51ad03639a7f676009491aa0e3/docs/img/delegator_tree.png -------------------------------------------------------------------------------- /docs/img/example_test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/testdouble/mocktail/b2e1933fe109fe51ad03639a7f676009491aa0e3/docs/img/example_test.png -------------------------------------------------------------------------------- /docs/img/extract_transform_load.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/testdouble/mocktail/b2e1933fe109fe51ad03639a7f676009491aa0e3/docs/img/extract_transform_load.png -------------------------------------------------------------------------------- /docs/img/mocktail_sorbet.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/testdouble/mocktail/b2e1933fe109fe51ad03639a7f676009491aa0e3/docs/img/mocktail_sorbet.jpg -------------------------------------------------------------------------------- /docs/img/mocktail_untyped.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/testdouble/mocktail/b2e1933fe109fe51ad03639a7f676009491aa0e3/docs/img/mocktail_untyped.jpg -------------------------------------------------------------------------------- /docs/img/spacer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/testdouble/mocktail/b2e1933fe109fe51ad03639a7f676009491aa0e3/docs/img/spacer.png -------------------------------------------------------------------------------- /docs/installation_untyped.md: -------------------------------------------------------------------------------- 1 | # Installing Mocktail 2 | 3 | So, you want to get started with Mocktail Classic™, do you? Here's how to get it 4 | installed! (And if you're having second thoughts on passing on [the Sorbet 5 | edition](installation_sorbet.md), there's still time!) 6 | 7 | ## Installation 8 | 9 | First thing's first, add this to your Gemfile: 10 | 11 | ```ruby 12 | gem "mocktail", group: :test, require: "mocktail" 13 | ``` 14 | 15 | (That redundant `require` option is just a garnish and won't cause any harm; 16 | it's there to signal to future readers that you're _not_ using 17 | `mocktail/sorbet`.) 18 | 19 | Once installed, you can require Mocktail like you might expect: 20 | 21 | ```ruby 22 | require "mocktail" 23 | ``` 24 | 25 | Next step: it's time to configure Mocktail with your test runner! 26 | 27 | **If you like to keep things classy, maybe [you use Minitest](configuring_minitest.md).** 28 | 29 | **Or maybe you'd rather express your intent [to use RSpec](configuring_rspec.md).** 30 | -------------------------------------------------------------------------------- /docs/other_uses.md: -------------------------------------------------------------------------------- 1 | # Other uses for Mocktail 2 | 3 | If you're looking to accomplish something with Mocktail that doesn't involve 4 | test-driven development, let's get real. There's a very high likelihood that 5 | what you're looking to do is either: 6 | 7 | 1. Not the best tool for the job 8 | 2. Not a job worth doing 9 | 10 | But hey, could be wrong. The present author once did a [talk about all the ways 11 | people abuse mocking 12 | libraries](https://blog.testdouble.com/talks/2018-03-06-please-dont-mock-me/) 13 | and undermine the value of their tests, so there is a track record of bias here. 14 | 15 | By popular demand, here are some ways you might be thinking about using 16 | Mocktail: 17 | 18 | **Mocking out the system clock in a vain attempt to [master space and time](faq/mocking_time.md).** 19 | 20 | **Mocking an HTTP API by faking out [Ruby's built-in networking](faq/mocking_http.md).** 21 | 22 | **Mocking out _just one_ method on an [otherwise real object](faq/partial_mocks.md).** 23 | 24 | **Recording method invocations while [calling through to their real implementation](faq/verifying_real_interactions.md).** 25 | 26 | **Using Mocktail to fix an existing test that's [failing in a gnarly way you don't understand](faq/existing_tests.md).** 27 | 28 | **Mocking out a method on the subject under test, AKA [the thing you're testing itself](faq/mocking_the_subject.md).** 29 | 30 | --- 31 | 32 | **Once you've seen enough, you can take a second look at [Mocktail as a TDD tool](tdd.md).** 33 | -------------------------------------------------------------------------------- /docs/tdd.md: -------------------------------------------------------------------------------- 1 | # Using Mocktail for test-driven development 2 | 3 | If you plan to use this mocking library as a tool in your test-driven 4 | development workflow, you've come to the right place—it's what Mocktail was 5 | designed for! 6 | 7 | There are several flavors of test-driven development, but the relevant 8 | distinction for figuring out which direction you want to take Mocktail lies 9 | before you: 10 | 11 | **Use Mocktail to create [fake instances of Ruby classes](tdd/poro.md) you own.** 12 | 13 | **Use Mocktail to [fake out class and module methods](tdd/class_methods.md) of types you own.** 14 | 15 | **Use Mocktail to [fake out third-party code](tdd/third_party.md) you can't readily change yourself.** 16 | -------------------------------------------------------------------------------- /docs/tdd/poro.md: -------------------------------------------------------------------------------- 1 | # Creating mocked instances of classes you own 2 | 3 | Good news! You find yourself on the golden path of Mocktail usage. Creating 4 | mocks of instances of classes that you or your team have authored and can 5 | readily change is right in the crosshairs of what the library was made to do! 6 | (If you're just starting out you should probably target >90% of your Mocktail 7 | usage to be of this variety.) 8 | 9 | Exactly _how_ you create these mocked instances depends on how you prefer to 10 | get [dependencies](/docs/support/glossary.md#dependency) into the hands of your 11 | [subject under test](/docs/support/glossary.md#subject-under-test). 12 | 13 | **Manually pass instances of dependencies to your test subject, AKA [dependency injection](poro/dependency_injection.md).** 14 | 15 | **Allow your subject to instantiate its dependencies by wielding ✨mocking magic✨, AKA [dependency inception](poro/dependency_inception.md).** 16 | -------------------------------------------------------------------------------- /lib/mocktail/collects_calls.rb: -------------------------------------------------------------------------------- 1 | module Mocktail 2 | class CollectsCalls 3 | extend T::Sig 4 | 5 | def collect(double, method_name) 6 | calls = ExplainsThing.new.explain(double).reference.calls 7 | 8 | if method_name.nil? 9 | calls 10 | else 11 | calls.select { |call| call.method.to_s == method_name.to_s } 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/mocktail/dsl.rb: -------------------------------------------------------------------------------- 1 | module Mocktail 2 | module DSL 3 | extend T::Sig 4 | 5 | def stubs(ignore_block: false, ignore_extra_args: false, ignore_arity: false, times: nil, &demo) 6 | RegistersStubbing.new.register(demo, DemoConfig.new( 7 | ignore_block: ignore_block, 8 | ignore_extra_args: ignore_extra_args, 9 | ignore_arity: ignore_arity, 10 | times: times 11 | )) 12 | end 13 | 14 | def verify(ignore_block: false, ignore_extra_args: false, ignore_arity: false, times: nil, &demo) 15 | VerifiesCall.new.verify(demo, DemoConfig.new( 16 | ignore_block: ignore_block, 17 | ignore_extra_args: ignore_extra_args, 18 | ignore_arity: ignore_arity, 19 | times: times 20 | )) 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/mocktail/errors.rb: -------------------------------------------------------------------------------- 1 | module Mocktail 2 | class Error < StandardError; end 3 | 4 | class UnexpectedError < Error; end 5 | 6 | class UnsupportedMocktail < Error; end 7 | 8 | class MissingDemonstrationError < Error; end 9 | 10 | class AmbiguousDemonstrationError < Error; end 11 | 12 | class InvalidMatcherError < Error; end 13 | 14 | class VerificationError < Error; end 15 | 16 | class TypeCheckingError < Error; end 17 | end 18 | -------------------------------------------------------------------------------- /lib/mocktail/explains_nils.rb: -------------------------------------------------------------------------------- 1 | require_relative "share/stringifies_method_name" 2 | require_relative "share/stringifies_call" 3 | 4 | module Mocktail 5 | class ExplainsNils 6 | extend T::Sig 7 | 8 | def initialize 9 | @stringifies_method_name = StringifiesMethodName.new 10 | @stringifies_call = StringifiesCall.new 11 | end 12 | 13 | def explain 14 | Mocktail.cabinet.unsatisfying_calls.map { |unsatisfying_call| 15 | dry_call = unsatisfying_call.call 16 | other_stubbings = unsatisfying_call.other_stubbings 17 | 18 | UnsatisfyingCallExplanation.new(unsatisfying_call, <<~MSG) 19 | `nil' was returned by a mocked `#{@stringifies_method_name.stringify(dry_call)}' method 20 | because none of its configured stubbings were satisfied. 21 | 22 | The actual call: 23 | 24 | #{@stringifies_call.stringify(dry_call, always_parens: true)} 25 | 26 | The call site: 27 | 28 | #{unsatisfying_call.backtrace.first} 29 | 30 | #{@stringifies_call.stringify_multiple(other_stubbings.map(&:recording), 31 | nonzero_message: "Stubbings configured prior to this call but not satisfied by it", 32 | zero_message: "No stubbings were configured on this method")} 33 | MSG 34 | } 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/mocktail/grabs_original_method_parameters.rb: -------------------------------------------------------------------------------- 1 | module Mocktail 2 | class GrabsOriginalMethodParameters 3 | extend T::Sig 4 | 5 | # Sorbet wraps the original method in a sig wrapper, so we need to unwrap it. 6 | # The value returned from `owner.instance_method(method_name)` does not have 7 | # the real parameters values available, as they'll have been erased 8 | # 9 | # If the method isn't wrapped by Sorbet, this will return the #instance_method, 10 | # per usual 11 | 12 | def grab(method) 13 | return [] unless method 14 | 15 | if (wrapped_method = sorbet_wrapped_method(method)) 16 | wrapped_method.parameters 17 | else 18 | method.parameters 19 | end 20 | end 21 | 22 | private 23 | 24 | def sorbet_wrapped_method(method) 25 | return unless defined?(::T::Private::Methods) 26 | 27 | T::Private::Methods.signature_for_method(method) 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/mocktail/handles_dry_call.rb: -------------------------------------------------------------------------------- 1 | require_relative "handles_dry_call/fulfills_stubbing" 2 | require_relative "handles_dry_call/logs_call" 3 | require_relative "handles_dry_call/validates_arguments" 4 | 5 | module Mocktail 6 | class HandlesDryCall 7 | extend T::Sig 8 | 9 | def initialize 10 | @validates_arguments = ValidatesArguments.new 11 | @logs_call = LogsCall.new 12 | @fulfills_stubbing = FulfillsStubbing.new 13 | end 14 | 15 | def handle(dry_call) 16 | @validates_arguments.validate(dry_call) 17 | @logs_call.log(dry_call) 18 | @fulfills_stubbing.fulfill(dry_call) 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/mocktail/handles_dry_call/fulfills_stubbing.rb: -------------------------------------------------------------------------------- 1 | require_relative "fulfills_stubbing/finds_satisfaction" 2 | require_relative "fulfills_stubbing/describes_unsatisfied_stubbing" 3 | 4 | module Mocktail 5 | class FulfillsStubbing 6 | extend T::Sig 7 | 8 | def initialize 9 | @finds_satisfaction = FindsSatisfaction.new 10 | @describes_unsatisfied_stubbing = DescribesUnsatisfiedStubbing.new 11 | end 12 | 13 | def fulfill(dry_call) 14 | if (stubbing = satisfaction(dry_call)) 15 | stubbing.satisfied! 16 | stubbing.effect&.call(dry_call) 17 | else 18 | store_unsatisfying_call!(dry_call) 19 | nil 20 | end 21 | end 22 | 23 | def satisfaction(dry_call) 24 | return if Mocktail.cabinet.demonstration_in_progress? 25 | 26 | @finds_satisfaction.find(dry_call) 27 | end 28 | 29 | private 30 | 31 | def store_unsatisfying_call!(dry_call) 32 | return if Mocktail.cabinet.demonstration_in_progress? 33 | 34 | Mocktail.cabinet.store_unsatisfying_call( 35 | @describes_unsatisfied_stubbing.describe(dry_call) 36 | ) 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/mocktail/handles_dry_call/fulfills_stubbing/describes_unsatisfied_stubbing.rb: -------------------------------------------------------------------------------- 1 | require_relative "../../share/cleans_backtrace" 2 | require_relative "../../share/bind" 3 | 4 | module Mocktail 5 | class DescribesUnsatisfiedStubbing 6 | extend T::Sig 7 | 8 | def initialize 9 | @cleans_backtrace = CleansBacktrace.new 10 | end 11 | 12 | def describe(dry_call) 13 | UnsatisfyingCall.new( 14 | call: dry_call, 15 | other_stubbings: Mocktail.cabinet.stubbings.select { |stubbing| 16 | Bind.call(dry_call.double, :==, stubbing.recording.double) && 17 | dry_call.method == stubbing.recording.method 18 | }, 19 | backtrace: @cleans_backtrace.clean(Error.new).backtrace || [] 20 | ) 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/mocktail/handles_dry_call/fulfills_stubbing/finds_satisfaction.rb: -------------------------------------------------------------------------------- 1 | require_relative "../../share/determines_matching_calls" 2 | 3 | module Mocktail 4 | class FindsSatisfaction 5 | extend T::Sig 6 | 7 | def initialize 8 | @determines_matching_calls = DeterminesMatchingCalls.new 9 | end 10 | 11 | def find(dry_call) 12 | Mocktail.cabinet.stubbings.reverse.find { |stubbing| 13 | demo_config_times = stubbing.demo_config.times 14 | 15 | @determines_matching_calls.determine(dry_call, stubbing.recording, stubbing.demo_config) && 16 | (demo_config_times.nil? || demo_config_times > stubbing.satisfaction_count) 17 | } 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/mocktail/handles_dry_call/logs_call.rb: -------------------------------------------------------------------------------- 1 | module Mocktail 2 | class LogsCall 3 | extend T::Sig 4 | 5 | def log(dry_call) 6 | Mocktail.cabinet.store_call(dry_call) 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/mocktail/handles_dry_call/validates_arguments.rb: -------------------------------------------------------------------------------- 1 | module Mocktail 2 | class ValidatesArguments 3 | extend T::Sig 4 | 5 | def self.disable! 6 | Thread.current[:mocktail_arity_validation_disabled] = true 7 | end 8 | 9 | def self.enable! 10 | Thread.current[:mocktail_arity_validation_disabled] = false 11 | end 12 | 13 | def self.disabled? 14 | !!Thread.current[:mocktail_arity_validation_disabled] 15 | end 16 | 17 | def self.optional(disable, &blk) 18 | return blk.call unless disable 19 | 20 | disable! 21 | ret = blk.call 22 | enable! 23 | ret 24 | end 25 | 26 | def initialize 27 | @simulates_argument_error = SimulatesArgumentError.new 28 | end 29 | 30 | def validate(dry_call) 31 | return if self.class.disabled? 32 | 33 | if (error = @simulates_argument_error.simulate(dry_call)) 34 | raise error 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/mocktail/handles_dry_new_call.rb: -------------------------------------------------------------------------------- 1 | module Mocktail 2 | class HandlesDryNewCall 3 | extend T::Sig 4 | 5 | def initialize 6 | @validates_arguments = ValidatesArguments.new 7 | @logs_call = LogsCall.new 8 | @fulfills_stubbing = FulfillsStubbing.new 9 | @imitates_type = ImitatesType.new 10 | end 11 | 12 | def handle(type, args, kwargs, block) 13 | @validates_arguments.validate(Call.new( 14 | original_method: type.instance_method(:initialize), 15 | args: args, 16 | kwargs: kwargs, 17 | block: block 18 | )) 19 | 20 | new_call = Call.new( 21 | singleton: true, 22 | double: type, 23 | original_type: type, 24 | dry_type: type, 25 | method: :new, 26 | args: args, 27 | kwargs: kwargs, 28 | block: block 29 | ) 30 | @logs_call.log(new_call) 31 | if @fulfills_stubbing.satisfaction(new_call) 32 | @fulfills_stubbing.fulfill(new_call) 33 | else 34 | @imitates_type.imitate(type) 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/mocktail/imitates_type.rb: -------------------------------------------------------------------------------- 1 | require_relative "imitates_type/ensures_imitation_support" 2 | require_relative "imitates_type/makes_double" 3 | 4 | module Mocktail 5 | class ImitatesType 6 | extend T::Sig 7 | extend T::Generic 8 | 9 | def initialize 10 | @ensures_imitation_support = EnsuresImitationSupport.new 11 | @makes_double = MakesDouble.new 12 | end 13 | 14 | def imitate(type) 15 | @ensures_imitation_support.ensure(type) 16 | @makes_double.make(type).tap do |double| 17 | Mocktail.cabinet.store_double(double) 18 | end.dry_instance 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/mocktail/imitates_type/ensures_imitation_support.rb: -------------------------------------------------------------------------------- 1 | module Mocktail 2 | class EnsuresImitationSupport 3 | extend T::Sig 4 | 5 | def ensure(type) 6 | unless type.is_a?(Class) || type.is_a?(Module) 7 | raise UnsupportedMocktail.new <<~MSG.tr("\n", " ") 8 | Mocktail.of() can only mix mocktail instances of modules and classes. 9 | MSG 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/mocktail/imitates_type/makes_double.rb: -------------------------------------------------------------------------------- 1 | require_relative "makes_double/declares_dry_class" 2 | require_relative "makes_double/gathers_fakeable_instance_methods" 3 | 4 | module Mocktail 5 | class MakesDouble 6 | extend T::Sig 7 | 8 | def initialize 9 | @declares_dry_class = DeclaresDryClass.new 10 | @gathers_fakeable_instance_methods = GathersFakeableInstanceMethods.new 11 | end 12 | 13 | def make(type) 14 | dry_methods = @gathers_fakeable_instance_methods.gather(type) 15 | dry_type = @declares_dry_class.declare(type, dry_methods) 16 | 17 | Double.new( 18 | original_type: type, 19 | dry_type: dry_type, 20 | dry_instance: dry_type.new, 21 | dry_methods: dry_methods 22 | ) 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/mocktail/imitates_type/makes_double/gathers_fakeable_instance_methods.rb: -------------------------------------------------------------------------------- 1 | module Mocktail 2 | class GathersFakeableInstanceMethods 3 | extend T::Sig 4 | 5 | def gather(type) 6 | methods = type.instance_methods + [ 7 | (:respond_to_missing? if type.private_method_defined?(:respond_to_missing?)) 8 | ].compact 9 | 10 | methods.reject { |m| 11 | ignore?(type, m) 12 | } 13 | end 14 | 15 | def ignore?(type, method_name) 16 | ignored_ancestors.include?(type.instance_method(method_name).owner) 17 | end 18 | 19 | def ignored_ancestors 20 | Object.ancestors 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/mocktail/initialize_based_on_type_system_mode_switching.rb: -------------------------------------------------------------------------------- 1 | require_relative "typed" 2 | 3 | # Constant boolean, so won't statically type-check, but `T.unsafe` can't be used 4 | # because we haven't required sorbet-runtime yet 5 | if eval("Mocktail::TYPED", binding, __FILE__, __LINE__) 6 | require "sorbet-runtime" 7 | else 8 | require "#{Gem.loaded_specs["sorbet-eraser"].gem_dir}/lib/t" 9 | end 10 | -------------------------------------------------------------------------------- /lib/mocktail/initializes_mocktail.rb: -------------------------------------------------------------------------------- 1 | module Mocktail 2 | class InitializesMocktail 3 | extend T::Sig 4 | 5 | def init 6 | [ 7 | Mocktail::Matchers::Any, 8 | Mocktail::Matchers::Includes, 9 | Mocktail::Matchers::IncludesString, 10 | Mocktail::Matchers::IncludesKey, 11 | Mocktail::Matchers::IncludesHash, 12 | Mocktail::Matchers::IsA, 13 | Mocktail::Matchers::Matches, 14 | Mocktail::Matchers::Not, 15 | Mocktail::Matchers::Numeric, 16 | Mocktail::Matchers::That 17 | ].each do |matcher_type| 18 | Mocktail.register_matcher(matcher_type) 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/mocktail/matcher_presentation.rb: -------------------------------------------------------------------------------- 1 | module Mocktail 2 | class MatcherPresentation 3 | extend T::Sig 4 | 5 | def respond_to_missing?(name, include_private = false) 6 | !!MatcherRegistry.instance.get(name) || super 7 | end 8 | 9 | def method_missing(name, *args, **kwargs, &blk) # standard:disable Style/ArgumentsForwarding 10 | if (matcher = MatcherRegistry.instance.get(name)) 11 | matcher.new(*args, **kwargs, &blk) # standard:disable Style/ArgumentsForwarding 12 | else 13 | super 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/mocktail/matchers.rb: -------------------------------------------------------------------------------- 1 | module Mocktail 2 | module Matchers 3 | end 4 | end 5 | 6 | require_relative "matchers/base" 7 | require_relative "matchers/any" 8 | require_relative "matchers/captor" 9 | require_relative "matchers/includes" 10 | require_relative "matchers/includes_string" 11 | require_relative "matchers/includes_hash" 12 | require_relative "matchers/includes_key" 13 | require_relative "matchers/is_a" 14 | require_relative "matchers/matches" 15 | require_relative "matchers/not" 16 | require_relative "matchers/numeric" 17 | require_relative "matchers/that" 18 | -------------------------------------------------------------------------------- /lib/mocktail/matchers/any.rb: -------------------------------------------------------------------------------- 1 | module Mocktail::Matchers 2 | class Any < Base 3 | extend T::Sig 4 | 5 | def self.matcher_name 6 | :any 7 | end 8 | 9 | def initialize 10 | # Empty initialize is necessary b/c Base default expects an argument 11 | end 12 | 13 | def match?(actual) 14 | true 15 | end 16 | 17 | def inspect 18 | "any" 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/mocktail/matchers/base.rb: -------------------------------------------------------------------------------- 1 | module Mocktail::Matchers 2 | class Base 3 | extend T::Sig 4 | extend T::Helpers 5 | 6 | if Mocktail::TYPED && T::Private::RuntimeLevels.default_checked_level != :never 7 | 8 | end 9 | 10 | # Custom matchers can receive any args, kwargs, or block they want. Usually 11 | # single-argument, though, so that's defaulted here and in #insepct 12 | 13 | def initialize(expected) 14 | @expected = expected 15 | end 16 | 17 | def self.matcher_name 18 | raise Mocktail::InvalidMatcherError.new("The `matcher_name` class method must return a valid method name") 19 | end 20 | 21 | def match?(actual) 22 | raise Mocktail::InvalidMatcherError.new("Matchers must implement `match?(argument)`") 23 | end 24 | 25 | def inspect 26 | "#{self.class.matcher_name}(#{@expected.inspect})" 27 | end 28 | 29 | def is_mocktail_matcher? 30 | true 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/mocktail/matchers/includes.rb: -------------------------------------------------------------------------------- 1 | module Mocktail::Matchers 2 | class Includes < Base 3 | extend T::Sig 4 | 5 | def self.matcher_name 6 | :includes 7 | end 8 | 9 | def initialize(*expecteds) 10 | @expecteds = expecteds 11 | end 12 | 13 | def match?(actual) 14 | @expecteds.all? { |expected| 15 | (actual.respond_to?(:include?) && actual.include?(expected)) || 16 | (actual.is_a?(Hash) && expected.is_a?(Hash) && expected.all? { |k, v| actual[k] == v }) 17 | } 18 | rescue 19 | false 20 | end 21 | 22 | def inspect 23 | "#{self.class.matcher_name}(#{@expecteds.map(&:inspect).join(", ")})" 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/mocktail/matchers/includes_hash.rb: -------------------------------------------------------------------------------- 1 | module Mocktail::Matchers 2 | class IncludesHash < Includes 3 | extend T::Sig 4 | 5 | def self.matcher_name 6 | :includes_hash 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/mocktail/matchers/includes_key.rb: -------------------------------------------------------------------------------- 1 | module Mocktail::Matchers 2 | class IncludesKey < Includes 3 | extend T::Sig 4 | 5 | def self.matcher_name 6 | :includes_key 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/mocktail/matchers/includes_string.rb: -------------------------------------------------------------------------------- 1 | module Mocktail::Matchers 2 | class IncludesString < Includes 3 | extend T::Sig 4 | 5 | def self.matcher_name 6 | :includes_string 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/mocktail/matchers/is_a.rb: -------------------------------------------------------------------------------- 1 | module Mocktail::Matchers 2 | class IsA < Base 3 | extend T::Sig 4 | 5 | def self.matcher_name 6 | :is_a 7 | end 8 | 9 | def match?(actual) 10 | actual.is_a?(@expected) 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/mocktail/matchers/matches.rb: -------------------------------------------------------------------------------- 1 | module Mocktail::Matchers 2 | class Matches < Base 3 | extend T::Sig 4 | 5 | def self.matcher_name 6 | :matches 7 | end 8 | 9 | def match?(actual) 10 | actual.respond_to?(:match?) && actual.match?(@expected) 11 | rescue 12 | false 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/mocktail/matchers/not.rb: -------------------------------------------------------------------------------- 1 | module Mocktail::Matchers 2 | class Not < Base 3 | extend T::Sig 4 | 5 | def self.matcher_name 6 | :not 7 | end 8 | 9 | def match?(actual) 10 | @expected != actual 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/mocktail/matchers/numeric.rb: -------------------------------------------------------------------------------- 1 | module Mocktail::Matchers 2 | class Numeric < Base 3 | extend T::Sig 4 | 5 | def self.matcher_name 6 | :numeric 7 | end 8 | 9 | def initialize 10 | # Empty initialize is necessary b/c Base default expects an argument 11 | end 12 | 13 | def match?(actual) 14 | actual.is_a?(::Numeric) 15 | end 16 | 17 | def inspect 18 | "numeric" 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/mocktail/matchers/that.rb: -------------------------------------------------------------------------------- 1 | module Mocktail::Matchers 2 | class That < Base 3 | extend T::Sig 4 | 5 | def self.matcher_name 6 | :that 7 | end 8 | 9 | def initialize(&blk) 10 | if blk.nil? 11 | raise ArgumentError.new("The `that` matcher must be passed a block (e.g. `that { |arg| … }`)") 12 | end 13 | @blk = blk 14 | end 15 | 16 | def match?(actual) 17 | @blk.call(actual) 18 | rescue 19 | false 20 | end 21 | 22 | def inspect 23 | "that {…}" 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/mocktail/records_demonstration.rb: -------------------------------------------------------------------------------- 1 | module Mocktail 2 | class RecordsDemonstration 3 | extend T::Sig 4 | 5 | def record(demonstration, demo_config) 6 | cabinet = Mocktail.cabinet 7 | prior_call_count = Mocktail.cabinet.calls.dup.size 8 | 9 | begin 10 | cabinet.demonstration_in_progress = true 11 | ValidatesArguments.optional(demo_config.ignore_arity) do 12 | demonstration.call(Mocktail.matchers) 13 | end 14 | ensure 15 | cabinet.demonstration_in_progress = false 16 | end 17 | 18 | if prior_call_count + 1 == cabinet.calls.size 19 | cabinet.calls.pop 20 | elsif prior_call_count == cabinet.calls.size 21 | raise MissingDemonstrationError.new <<~MSG.tr("\n", " ") 22 | `stubs` & `verify` expect an invocation of a mocked method by a passed 23 | block, but no invocation occurred. 24 | MSG 25 | else 26 | raise AmbiguousDemonstrationError.new <<~MSG.tr("\n", " ") 27 | `stubs` & `verify` expect exactly one invocation of a mocked method, 28 | but #{cabinet.calls.size - prior_call_count} were detected. As a 29 | result, Mocktail doesn't know which invocation to stub or verify. 30 | MSG 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/mocktail/registers_stubbing.rb: -------------------------------------------------------------------------------- 1 | require_relative "records_demonstration" 2 | 3 | module Mocktail 4 | class RegistersStubbing 5 | extend T::Sig 6 | 7 | def initialize 8 | @records_demonstration = RecordsDemonstration.new 9 | end 10 | 11 | def register(demonstration, demo_config) 12 | Stubbing.new( 13 | demonstration: demonstration, 14 | demo_config: demo_config, 15 | recording: @records_demonstration.record(demonstration, demo_config) 16 | ).tap do |stubbing| 17 | Mocktail.cabinet.store_stubbing(stubbing) 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/mocktail/replaces_next.rb: -------------------------------------------------------------------------------- 1 | module Mocktail 2 | class ReplacesNext 3 | extend T::Sig 4 | 5 | def initialize 6 | @top_shelf = TopShelf.instance 7 | @redefines_new = RedefinesNew.new 8 | @imitates_type = ImitatesType.new 9 | end 10 | 11 | def replace_once(type) 12 | replace(type, 1).fetch(0) 13 | end 14 | 15 | def replace(type, count) 16 | raise UnsupportedMocktail.new("Mocktail.of_next() only supports classes") unless type.is_a?(Class) 17 | 18 | mocktails = count.times.map { @imitates_type.imitate(type) } 19 | 20 | @top_shelf.register_of_next_replacement!(type) 21 | @redefines_new.redefine(type) 22 | mocktails.reverse_each do |mocktail| 23 | Mocktail.stubs( 24 | ignore_extra_args: true, 25 | ignore_block: true, 26 | ignore_arity: true, 27 | times: 1 28 | ) { 29 | type.new 30 | }.with { 31 | if mocktail == mocktails.last 32 | @top_shelf.unregister_of_next_replacement!(type) 33 | end 34 | 35 | mocktail 36 | } 37 | end 38 | 39 | mocktails 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/mocktail/replaces_type.rb: -------------------------------------------------------------------------------- 1 | require_relative "replaces_type/redefines_new" 2 | require_relative "replaces_type/redefines_singleton_methods" 3 | require_relative "replaces_type/runs_sorbet_sig_blocks_before_replacement" 4 | 5 | module Mocktail 6 | class ReplacesType 7 | extend T::Sig 8 | 9 | def initialize 10 | @top_shelf = TopShelf.instance 11 | @runs_sorbet_sig_blocks_before_replacement = RunsSorbetSigBlocksBeforeReplacement.new 12 | @redefines_new = RedefinesNew.new 13 | @redefines_singleton_methods = RedefinesSingletonMethods.new 14 | end 15 | 16 | def replace(type) 17 | unless type.is_a?(Class) || type.is_a?(Module) 18 | raise UnsupportedMocktail.new("Mocktail.replace() only supports classes and modules") 19 | end 20 | 21 | @runs_sorbet_sig_blocks_before_replacement.run(type) 22 | 23 | if type.is_a?(Class) 24 | @top_shelf.register_new_replacement!(type) 25 | @redefines_new.redefine(type) 26 | end 27 | 28 | @top_shelf.register_singleton_method_replacement!(type) 29 | @redefines_singleton_methods.redefine(type) 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/mocktail/replaces_type/redefines_new.rb: -------------------------------------------------------------------------------- 1 | module Mocktail 2 | class RedefinesNew 3 | extend T::Sig 4 | 5 | def initialize 6 | @handles_dry_new_call = HandlesDryNewCall.new 7 | end 8 | 9 | def redefine(type) 10 | type_replacement = TopShelf.instance.type_replacement_for(type) 11 | 12 | if type_replacement.replacement_new.nil? 13 | type_replacement.original_new = type.method(:new) 14 | type.singleton_class.send(:undef_method, :new) 15 | handles_dry_new_call = @handles_dry_new_call 16 | type.define_singleton_method :new, ->(*args, **kwargs, &block) { 17 | if TopShelf.instance.new_replaced?(type) || 18 | (type.is_a?(Class) && TopShelf.instance.of_next_registered?(type)) 19 | handles_dry_new_call.handle(type, args, kwargs, block) 20 | else 21 | type_replacement.original_new.call(*args, **kwargs, &block) 22 | end 23 | } 24 | type_replacement.replacement_new = type.singleton_method(:new) 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/mocktail/resets_state.rb: -------------------------------------------------------------------------------- 1 | module Mocktail 2 | class ResetsState 3 | extend T::Sig 4 | 5 | def reset 6 | TopShelf.instance.reset_current_thread! 7 | Mocktail.cabinet.reset! 8 | ValidatesArguments.enable! 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/mocktail/share/bind.rb: -------------------------------------------------------------------------------- 1 | module Mocktail 2 | module Bind 3 | # sig intentionally omitted, because the wrapper will cause infinite recursion if certain methods are mocked 4 | def self.call(mock, method_name, *args, **kwargs, &blk) # standard:disable Style/ArgumentsForwarding 5 | if Mocktail.cabinet.double_for_instance(mock) 6 | Object.instance_method(method_name).bind_call(mock, *args, **kwargs, &blk) # standard:disable Style/ArgumentsForwarding 7 | elsif (mock.is_a?(Module) || mock.is_a?(Class)) && 8 | (type_replacement = TopShelf.instance.type_replacement_if_exists_for(mock)) && 9 | (og_method = type_replacement.original_methods&.find { |m| m.name == method_name }) 10 | og_method.call(*args, **kwargs, &blk) # standard:disable Style/ArgumentsForwarding 11 | else 12 | mock.__send__(method_name, *args, **kwargs, &blk) # standard:disable Style/ArgumentsForwarding 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/mocktail/share/cleans_backtrace.rb: -------------------------------------------------------------------------------- 1 | module Mocktail 2 | class CleansBacktrace 3 | extend T::Sig 4 | 5 | def clean(error) 6 | raise error 7 | rescue error.class => e 8 | e.tap do |e| 9 | e.set_backtrace(e.backtrace.drop_while { |frame| 10 | frame.start_with?(BASE_PATH, BASE_PATH) || frame.match?(/[\\|\/]sorbet-runtime.*[\\|\/]lib[\\|\/]types[\\|\/]private/) 11 | }) 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/mocktail/share/creates_identifier.rb: -------------------------------------------------------------------------------- 1 | module Mocktail 2 | class CreatesIdentifier 3 | extend T::Sig 4 | 5 | KEYWORDS = %w[__FILE__ __LINE__ alias and begin BEGIN break case class def defined? do else elsif end END ensure false for if in module next nil not or redo rescue retry return self super then true undef unless until when while yield] 6 | 7 | def create(s, default: "identifier", max_length: 24) 8 | case s 9 | when Kernel 10 | id = (s.to_s.downcase 11 | .gsub(/:0x[0-9a-f]+/, "") # Lazy attempt to wipe any Object:0x802beef identifiers 12 | .gsub(/[^\w\s]/, "") 13 | .gsub(/^\d+/, "")[0...max_length] || "") 14 | .strip 15 | .gsub(/\s+/, "_") # snake_case 16 | 17 | if id.empty? 18 | default 19 | else 20 | unreserved(id, default) 21 | end 22 | else 23 | default 24 | end 25 | end 26 | 27 | private 28 | 29 | def unreserved(id, default) 30 | return id unless KEYWORDS.include?(id) 31 | 32 | "#{id}_#{default}" 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/mocktail/share/stringifies_method_name.rb: -------------------------------------------------------------------------------- 1 | module Mocktail 2 | class StringifiesMethodName 3 | extend T::Sig 4 | 5 | def stringify(call) 6 | [ 7 | call.original_type&.name, 8 | call.singleton ? "." : "#", 9 | call.method 10 | ].join 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/mocktail/simulates_argument_error.rb: -------------------------------------------------------------------------------- 1 | require_relative "simulates_argument_error/transforms_params" 2 | require_relative "simulates_argument_error/reconciles_args_with_params" 3 | require_relative "simulates_argument_error/recreates_message" 4 | require_relative "share/cleans_backtrace" 5 | require_relative "share/stringifies_call" 6 | 7 | module Mocktail 8 | class SimulatesArgumentError 9 | extend T::Sig 10 | 11 | def initialize 12 | @transforms_params = TransformsParams.new 13 | @reconciles_args_with_params = ReconcilesArgsWithParams.new 14 | @recreates_message = RecreatesMessage.new 15 | @cleans_backtrace = CleansBacktrace.new 16 | @stringifies_call = StringifiesCall.new 17 | end 18 | 19 | def simulate(dry_call) 20 | signature = @transforms_params.transform(dry_call) 21 | 22 | unless @reconciles_args_with_params.reconcile(signature) 23 | @cleans_backtrace.clean( 24 | ArgumentError.new([ 25 | @recreates_message.recreate(signature), 26 | "[Mocktail call: `#{@stringifies_call.stringify(dry_call)}']" 27 | ].join(" ")) 28 | ) 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/mocktail/simulates_argument_error/reconciles_args_with_params.rb: -------------------------------------------------------------------------------- 1 | module Mocktail 2 | class ReconcilesArgsWithParams 3 | extend T::Sig 4 | 5 | def reconcile(signature) 6 | args_match?(signature.positional_params, signature.positional_args) && 7 | kwargs_match?(signature.keyword_params, signature.keyword_args) 8 | end 9 | 10 | private 11 | 12 | def args_match?(arg_params, args) 13 | args.size >= arg_params.required.size && 14 | (arg_params.rest? || args.size <= arg_params.allowed.size) 15 | end 16 | 17 | def kwargs_match?(kwarg_params, kwargs) 18 | kwarg_params.required.all? { |name| kwargs.key?(name) } && 19 | (kwarg_params.rest? || kwargs.keys.all? { |name| kwarg_params.allowed.include?(name) }) 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/mocktail/sorbet.rb: -------------------------------------------------------------------------------- 1 | require_relative "sorbet/mocktail" 2 | -------------------------------------------------------------------------------- /lib/mocktail/sorbet/mocktail/collects_calls.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | module Mocktail 4 | class CollectsCalls 5 | extend T::Sig 6 | 7 | sig { params(double: Object, method_name: T.nilable(Symbol)).returns(T::Array[Call]) } 8 | def collect(double, method_name) 9 | calls = ExplainsThing.new.explain(double).reference.calls 10 | 11 | if method_name.nil? 12 | calls 13 | else 14 | calls.select { |call| call.method.to_s == method_name.to_s } 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/mocktail/sorbet/mocktail/errors.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | module Mocktail 4 | class Error < StandardError; end 5 | 6 | class UnexpectedError < Error; end 7 | 8 | class UnsupportedMocktail < Error; end 9 | 10 | class MissingDemonstrationError < Error; end 11 | 12 | class AmbiguousDemonstrationError < Error; end 13 | 14 | class InvalidMatcherError < Error; end 15 | 16 | class VerificationError < Error; end 17 | 18 | class TypeCheckingError < Error; end 19 | end 20 | -------------------------------------------------------------------------------- /lib/mocktail/sorbet/mocktail/grabs_original_method_parameters.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | module Mocktail 4 | class GrabsOriginalMethodParameters 5 | extend T::Sig 6 | 7 | # Sorbet wraps the original method in a sig wrapper, so we need to unwrap it. 8 | # The value returned from `owner.instance_method(method_name)` does not have 9 | # the real parameters values available, as they'll have been erased 10 | # 11 | # If the method isn't wrapped by Sorbet, this will return the #instance_method, 12 | # per usual 13 | sig { params(method: T.nilable(T.any(UnboundMethod, Method))).returns(T::Array[T::Array[Symbol]]) } 14 | def grab(method) 15 | return [] unless method 16 | 17 | if (wrapped_method = sorbet_wrapped_method(method)) 18 | wrapped_method.parameters 19 | else 20 | method.parameters 21 | end 22 | end 23 | 24 | private 25 | 26 | sig { params(method: T.any(UnboundMethod, Method)).returns(T.nilable(T::Private::Methods::Signature)) } 27 | def sorbet_wrapped_method(method) 28 | return unless defined?(::T::Private::Methods) 29 | 30 | T::Private::Methods.signature_for_method(method) 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/mocktail/sorbet/mocktail/handles_dry_call.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | require_relative "handles_dry_call/fulfills_stubbing" 4 | require_relative "handles_dry_call/logs_call" 5 | require_relative "handles_dry_call/validates_arguments" 6 | 7 | module Mocktail 8 | class HandlesDryCall 9 | extend T::Sig 10 | 11 | sig { void } 12 | def initialize 13 | @validates_arguments = T.let(ValidatesArguments.new, ValidatesArguments) 14 | @logs_call = T.let(LogsCall.new, LogsCall) 15 | @fulfills_stubbing = T.let(FulfillsStubbing.new, FulfillsStubbing) 16 | end 17 | 18 | sig { params(dry_call: Call).returns(T.anything) } 19 | def handle(dry_call) 20 | @validates_arguments.validate(dry_call) 21 | @logs_call.log(dry_call) 22 | @fulfills_stubbing.fulfill(dry_call) 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/mocktail/sorbet/mocktail/handles_dry_call/fulfills_stubbing.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | require_relative "fulfills_stubbing/finds_satisfaction" 4 | require_relative "fulfills_stubbing/describes_unsatisfied_stubbing" 5 | 6 | module Mocktail 7 | class FulfillsStubbing 8 | extend T::Sig 9 | 10 | sig { void } 11 | def initialize 12 | @finds_satisfaction = T.let(FindsSatisfaction.new, Mocktail::FindsSatisfaction) 13 | @describes_unsatisfied_stubbing = T.let(DescribesUnsatisfiedStubbing.new, Mocktail::DescribesUnsatisfiedStubbing) 14 | end 15 | 16 | sig { params(dry_call: Call).returns(T.anything) } 17 | def fulfill(dry_call) 18 | if (stubbing = satisfaction(dry_call)) 19 | stubbing.satisfied! 20 | stubbing.effect&.call(dry_call) 21 | else 22 | store_unsatisfying_call!(dry_call) 23 | nil 24 | end 25 | end 26 | 27 | sig { params(dry_call: Call).returns(T.nilable(Stubbing[T.anything])) } 28 | def satisfaction(dry_call) 29 | return if Mocktail.cabinet.demonstration_in_progress? 30 | 31 | @finds_satisfaction.find(dry_call) 32 | end 33 | 34 | private 35 | 36 | sig { params(dry_call: Call).void } 37 | def store_unsatisfying_call!(dry_call) 38 | return if Mocktail.cabinet.demonstration_in_progress? 39 | 40 | Mocktail.cabinet.store_unsatisfying_call( 41 | @describes_unsatisfied_stubbing.describe(dry_call) 42 | ) 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/mocktail/sorbet/mocktail/handles_dry_call/fulfills_stubbing/describes_unsatisfied_stubbing.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | require_relative "../../share/cleans_backtrace" 4 | require_relative "../../share/bind" 5 | 6 | module Mocktail 7 | class DescribesUnsatisfiedStubbing 8 | extend T::Sig 9 | 10 | sig { void } 11 | def initialize 12 | @cleans_backtrace = T.let(CleansBacktrace.new, Mocktail::CleansBacktrace) 13 | end 14 | 15 | sig { params(dry_call: Mocktail::Call).returns(Mocktail::UnsatisfyingCall) } 16 | def describe(dry_call) 17 | UnsatisfyingCall.new( 18 | call: dry_call, 19 | other_stubbings: Mocktail.cabinet.stubbings.select { |stubbing| 20 | Bind.call(dry_call.double, :==, stubbing.recording.double) && 21 | dry_call.method == stubbing.recording.method 22 | }, 23 | backtrace: @cleans_backtrace.clean(Error.new).backtrace || [] 24 | ) 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/mocktail/sorbet/mocktail/handles_dry_call/fulfills_stubbing/finds_satisfaction.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | require_relative "../../share/determines_matching_calls" 4 | 5 | module Mocktail 6 | class FindsSatisfaction 7 | extend T::Sig 8 | 9 | sig { void } 10 | def initialize 11 | @determines_matching_calls = T.let(DeterminesMatchingCalls.new, Mocktail::DeterminesMatchingCalls) 12 | end 13 | 14 | sig { params(dry_call: Call).returns(T.nilable(Stubbing[T.anything])) } 15 | def find(dry_call) 16 | Mocktail.cabinet.stubbings.reverse.find { |stubbing| 17 | demo_config_times = stubbing.demo_config.times 18 | 19 | @determines_matching_calls.determine(dry_call, stubbing.recording, stubbing.demo_config) && 20 | (demo_config_times.nil? || demo_config_times > stubbing.satisfaction_count) 21 | } 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/mocktail/sorbet/mocktail/handles_dry_call/logs_call.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | module Mocktail 4 | class LogsCall 5 | extend T::Sig 6 | 7 | sig { params(dry_call: Call).void } 8 | def log(dry_call) 9 | Mocktail.cabinet.store_call(dry_call) 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/mocktail/sorbet/mocktail/handles_dry_call/validates_arguments.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | module Mocktail 4 | class ValidatesArguments 5 | extend T::Sig 6 | sig { void } 7 | def self.disable! 8 | Thread.current[:mocktail_arity_validation_disabled] = true 9 | end 10 | 11 | sig { void } 12 | def self.enable! 13 | Thread.current[:mocktail_arity_validation_disabled] = false 14 | end 15 | 16 | sig { returns(T::Boolean) } 17 | def self.disabled? 18 | !!Thread.current[:mocktail_arity_validation_disabled] 19 | end 20 | 21 | sig { params(disable: T.nilable(T::Boolean), blk: T.proc.returns(T.anything)).void } 22 | def self.optional(disable, &blk) 23 | return blk.call unless disable 24 | 25 | disable! 26 | ret = blk.call 27 | enable! 28 | ret 29 | end 30 | 31 | sig { void } 32 | def initialize 33 | @simulates_argument_error = T.let(SimulatesArgumentError.new, Mocktail::SimulatesArgumentError) 34 | end 35 | 36 | sig { params(dry_call: Call).returns(NilClass) } 37 | def validate(dry_call) 38 | return if self.class.disabled? 39 | 40 | if (error = @simulates_argument_error.simulate(dry_call)) 41 | raise error 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/mocktail/sorbet/mocktail/handles_dry_new_call.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | module Mocktail 4 | class HandlesDryNewCall 5 | extend T::Sig 6 | 7 | sig { void } 8 | def initialize 9 | @validates_arguments = T.let(ValidatesArguments.new, ValidatesArguments) 10 | @logs_call = T.let(LogsCall.new, LogsCall) 11 | @fulfills_stubbing = T.let(FulfillsStubbing.new, FulfillsStubbing) 12 | @imitates_type = T.let(ImitatesType.new, ImitatesType) 13 | end 14 | 15 | sig { params(type: T::Class[T.all(T, Object)], args: T::Array[T.anything], kwargs: T::Hash[Symbol, T.anything], block: T.nilable(Proc)).returns(T.anything) } 16 | def handle(type, args, kwargs, block) 17 | @validates_arguments.validate(Call.new( 18 | original_method: type.instance_method(:initialize), 19 | args: args, 20 | kwargs: kwargs, 21 | block: block 22 | )) 23 | 24 | new_call = Call.new( 25 | singleton: true, 26 | double: type, 27 | original_type: type, 28 | dry_type: type, 29 | method: :new, 30 | args: args, 31 | kwargs: kwargs, 32 | block: block 33 | ) 34 | @logs_call.log(new_call) 35 | if @fulfills_stubbing.satisfaction(new_call) 36 | @fulfills_stubbing.fulfill(new_call) 37 | else 38 | @imitates_type.imitate(type) 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/mocktail/sorbet/mocktail/imitates_type.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | require_relative "imitates_type/ensures_imitation_support" 4 | require_relative "imitates_type/makes_double" 5 | 6 | module Mocktail 7 | class ImitatesType 8 | extend T::Sig 9 | extend T::Generic 10 | 11 | sig { void } 12 | def initialize 13 | @ensures_imitation_support = T.let(EnsuresImitationSupport.new, EnsuresImitationSupport) 14 | @makes_double = T.let(MakesDouble.new, MakesDouble) 15 | end 16 | 17 | sig { 18 | type_parameters(:T) 19 | .params(type: T::Class[T.all(T.type_parameter(:T), Object)]) 20 | .returns(T.all(T.type_parameter(:T), Object)) 21 | } 22 | def imitate(type) 23 | @ensures_imitation_support.ensure(type) 24 | T.cast(@makes_double.make(type).tap do |double| 25 | Mocktail.cabinet.store_double(double) 26 | end.dry_instance, T.all(T.type_parameter(:T), Object)) 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/mocktail/sorbet/mocktail/imitates_type/ensures_imitation_support.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | module Mocktail 4 | class EnsuresImitationSupport 5 | extend T::Sig 6 | 7 | sig { params(type: T.any(T::Class[T.anything], Module)).void } 8 | def ensure(type) 9 | unless type.is_a?(Class) || type.is_a?(Module) 10 | raise UnsupportedMocktail.new <<~MSG.tr("\n", " ") 11 | Mocktail.of() can only mix mocktail instances of modules and classes. 12 | MSG 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/mocktail/sorbet/mocktail/imitates_type/makes_double.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | require_relative "makes_double/declares_dry_class" 4 | require_relative "makes_double/gathers_fakeable_instance_methods" 5 | 6 | module Mocktail 7 | class MakesDouble 8 | extend T::Sig 9 | 10 | sig { void } 11 | def initialize 12 | @declares_dry_class = T.let(DeclaresDryClass.new, DeclaresDryClass) 13 | @gathers_fakeable_instance_methods = T.let(GathersFakeableInstanceMethods.new, GathersFakeableInstanceMethods) 14 | end 15 | 16 | sig { params(type: T::Class[Object]).returns(Double) } 17 | def make(type) 18 | dry_methods = @gathers_fakeable_instance_methods.gather(type) 19 | dry_type = @declares_dry_class.declare(type, dry_methods) 20 | 21 | Double.new( 22 | original_type: type, 23 | dry_type: dry_type, 24 | dry_instance: dry_type.new, 25 | dry_methods: dry_methods 26 | ) 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/mocktail/sorbet/mocktail/imitates_type/makes_double/gathers_fakeable_instance_methods.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | module Mocktail 4 | class GathersFakeableInstanceMethods 5 | extend T::Sig 6 | 7 | sig { params(type: T.any(T::Class[T.anything], Module)).returns(T::Array[Symbol]) } 8 | def gather(type) 9 | methods = type.instance_methods + [ 10 | (:respond_to_missing? if type.private_method_defined?(:respond_to_missing?)) 11 | ].compact 12 | 13 | methods.reject { |m| 14 | ignore?(type, m) 15 | } 16 | end 17 | 18 | sig { params(type: T.any(T::Class[T.anything], Module), method_name: Symbol).returns(T::Boolean) } 19 | def ignore?(type, method_name) 20 | ignored_ancestors.include?(type.instance_method(method_name).owner) 21 | end 22 | 23 | sig { returns(T::Array[Module]) } 24 | def ignored_ancestors 25 | Object.ancestors 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/mocktail/sorbet/mocktail/initialize_based_on_type_system_mode_switching.rb: -------------------------------------------------------------------------------- 1 | # typed: false 2 | 3 | require_relative "typed" 4 | 5 | # Constant boolean, so won't statically type-check, but `T.unsafe` can't be used 6 | # because we haven't required sorbet-runtime yet 7 | if eval("Mocktail::TYPED", binding, __FILE__, __LINE__) 8 | require "sorbet-runtime" 9 | else 10 | require "#{Gem.loaded_specs["sorbet-eraser"].gem_dir}/lib/t" 11 | end 12 | -------------------------------------------------------------------------------- /lib/mocktail/sorbet/mocktail/initializes_mocktail.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | module Mocktail 4 | class InitializesMocktail 5 | extend T::Sig 6 | 7 | sig { void } 8 | def init 9 | [ 10 | Mocktail::Matchers::Any, 11 | Mocktail::Matchers::Includes, 12 | Mocktail::Matchers::IncludesString, 13 | Mocktail::Matchers::IncludesKey, 14 | Mocktail::Matchers::IncludesHash, 15 | Mocktail::Matchers::IsA, 16 | Mocktail::Matchers::Matches, 17 | Mocktail::Matchers::Not, 18 | Mocktail::Matchers::Numeric, 19 | Mocktail::Matchers::That 20 | ].each do |matcher_type| 21 | Mocktail.register_matcher(matcher_type) 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/mocktail/sorbet/mocktail/matcher_presentation.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | module Mocktail 4 | class MatcherPresentation 5 | extend T::Sig 6 | 7 | sig { params(name: Symbol, include_private: T::Boolean).returns(T::Boolean) } 8 | def respond_to_missing?(name, include_private = false) 9 | !!MatcherRegistry.instance.get(name) || super 10 | end 11 | 12 | sig { params(name: Symbol, args: T.anything, kwargs: T.anything, blk: T.nilable(Proc)).returns(T.anything) } 13 | def method_missing(name, *args, **kwargs, &blk) # standard:disable Style/ArgumentsForwarding 14 | if (matcher = MatcherRegistry.instance.get(name)) 15 | T.unsafe(matcher).new(*args, **kwargs, &blk) # standard:disable Style/ArgumentsForwarding 16 | else 17 | super 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/mocktail/sorbet/mocktail/matchers.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | module Mocktail 4 | module Matchers 5 | end 6 | end 7 | 8 | require_relative "matchers/base" 9 | require_relative "matchers/any" 10 | require_relative "matchers/captor" 11 | require_relative "matchers/includes" 12 | require_relative "matchers/includes_string" 13 | require_relative "matchers/includes_hash" 14 | require_relative "matchers/includes_key" 15 | require_relative "matchers/is_a" 16 | require_relative "matchers/matches" 17 | require_relative "matchers/not" 18 | require_relative "matchers/numeric" 19 | require_relative "matchers/that" 20 | -------------------------------------------------------------------------------- /lib/mocktail/sorbet/mocktail/matchers/any.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | module Mocktail::Matchers 4 | class Any < Base 5 | extend T::Sig 6 | 7 | sig { returns(Symbol) } 8 | def self.matcher_name 9 | :any 10 | end 11 | 12 | sig { void } 13 | def initialize 14 | # Empty initialize is necessary b/c Base default expects an argument 15 | end 16 | 17 | sig { params(actual: T.anything).returns(T::Boolean) } 18 | def match?(actual) 19 | true 20 | end 21 | 22 | sig { returns(String) } 23 | def inspect 24 | "any" 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/mocktail/sorbet/mocktail/matchers/base.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | module Mocktail::Matchers 4 | class Base 5 | extend T::Sig 6 | extend T::Helpers 7 | 8 | if T.unsafe(Mocktail::TYPED) && T::Private::RuntimeLevels.default_checked_level != :never 9 | abstract! 10 | end 11 | 12 | # Custom matchers can receive any args, kwargs, or block they want. Usually 13 | # single-argument, though, so that's defaulted here and in #insepct 14 | sig { params(expected: BasicObject).void } 15 | def initialize(expected) 16 | @expected = expected 17 | end 18 | 19 | sig { returns(Symbol) } 20 | def self.matcher_name 21 | raise Mocktail::InvalidMatcherError.new("The `matcher_name` class method must return a valid method name") 22 | end 23 | 24 | sig { params(actual: T.untyped).returns(T::Boolean) } 25 | def match?(actual) 26 | raise Mocktail::InvalidMatcherError.new("Matchers must implement `match?(argument)`") 27 | end 28 | 29 | sig { returns(String) } 30 | def inspect 31 | "#{self.class.matcher_name}(#{T.cast(@expected, Object).inspect})" 32 | end 33 | 34 | sig { returns(TrueClass) } 35 | def is_mocktail_matcher? 36 | true 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/mocktail/sorbet/mocktail/matchers/includes.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | module Mocktail::Matchers 4 | class Includes < Base 5 | extend T::Sig 6 | 7 | sig { returns(Symbol) } 8 | def self.matcher_name 9 | :includes 10 | end 11 | 12 | sig { params(expecteds: T.untyped).void } 13 | def initialize(*expecteds) 14 | @expecteds = T.let(expecteds, T::Array[T.untyped]) 15 | end 16 | 17 | sig { params(actual: T.untyped).returns(T::Boolean) } 18 | def match?(actual) 19 | @expecteds.all? { |expected| 20 | (actual.respond_to?(:include?) && actual.include?(expected)) || 21 | (actual.is_a?(Hash) && expected.is_a?(Hash) && expected.all? { |k, v| actual[k] == v }) 22 | } 23 | rescue 24 | false 25 | end 26 | 27 | sig { returns(String) } 28 | def inspect 29 | "#{self.class.matcher_name}(#{@expecteds.map(&:inspect).join(", ")})" 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/mocktail/sorbet/mocktail/matchers/includes_hash.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | module Mocktail::Matchers 4 | class IncludesHash < Includes 5 | extend T::Sig 6 | 7 | sig { returns(Symbol) } 8 | def self.matcher_name 9 | :includes_hash 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/mocktail/sorbet/mocktail/matchers/includes_key.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | module Mocktail::Matchers 4 | class IncludesKey < Includes 5 | extend T::Sig 6 | 7 | sig { returns(Symbol) } 8 | def self.matcher_name 9 | :includes_key 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/mocktail/sorbet/mocktail/matchers/includes_string.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | module Mocktail::Matchers 4 | class IncludesString < Includes 5 | extend T::Sig 6 | 7 | sig { returns(Symbol) } 8 | def self.matcher_name 9 | :includes_string 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/mocktail/sorbet/mocktail/matchers/is_a.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | module Mocktail::Matchers 4 | class IsA < Base 5 | extend T::Sig 6 | 7 | sig { returns(Symbol) } 8 | def self.matcher_name 9 | :is_a 10 | end 11 | 12 | sig { params(actual: T.untyped).returns(T::Boolean) } 13 | def match?(actual) 14 | actual.is_a?(@expected) 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/mocktail/sorbet/mocktail/matchers/matches.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | module Mocktail::Matchers 4 | class Matches < Base 5 | extend T::Sig 6 | 7 | sig { returns(Symbol) } 8 | def self.matcher_name 9 | :matches 10 | end 11 | 12 | sig { params(actual: T.untyped).returns(T::Boolean) } 13 | def match?(actual) 14 | actual.respond_to?(:match?) && actual.match?(@expected) 15 | rescue 16 | false 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/mocktail/sorbet/mocktail/matchers/not.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | module Mocktail::Matchers 4 | class Not < Base 5 | extend T::Sig 6 | 7 | sig { returns(Symbol) } 8 | def self.matcher_name 9 | :not 10 | end 11 | 12 | sig { params(actual: T.untyped).returns(T::Boolean) } 13 | def match?(actual) 14 | @expected != actual 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/mocktail/sorbet/mocktail/matchers/numeric.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | module Mocktail::Matchers 4 | class Numeric < Base 5 | extend T::Sig 6 | 7 | sig { returns(Symbol) } 8 | def self.matcher_name 9 | :numeric 10 | end 11 | 12 | sig { void } 13 | def initialize 14 | # Empty initialize is necessary b/c Base default expects an argument 15 | end 16 | 17 | sig { params(actual: T.untyped).returns(T::Boolean) } 18 | def match?(actual) 19 | actual.is_a?(::Numeric) 20 | end 21 | 22 | sig { returns(String) } 23 | def inspect 24 | "numeric" 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/mocktail/sorbet/mocktail/matchers/that.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | module Mocktail::Matchers 4 | class That < Base 5 | extend T::Sig 6 | 7 | sig { returns(Symbol) } 8 | def self.matcher_name 9 | :that 10 | end 11 | 12 | sig { params(blk: T.nilable(T.proc.params(actual: T.untyped).returns(T.untyped))).void } 13 | def initialize(&blk) 14 | if blk.nil? 15 | raise ArgumentError.new("The `that` matcher must be passed a block (e.g. `that { |arg| … }`)") 16 | end 17 | @blk = T.let(blk, T.proc.params(actual: T.untyped).returns(T.untyped)) 18 | end 19 | 20 | sig { params(actual: T.untyped).returns(T::Boolean) } 21 | def match?(actual) 22 | @blk.call(actual) 23 | rescue 24 | false 25 | end 26 | 27 | sig { returns(String) } 28 | def inspect 29 | "that {…}" 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/mocktail/sorbet/mocktail/registers_stubbing.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | require_relative "records_demonstration" 4 | 5 | module Mocktail 6 | class RegistersStubbing 7 | extend T::Sig 8 | 9 | sig { void } 10 | def initialize 11 | @records_demonstration = T.let(RecordsDemonstration.new, RecordsDemonstration) 12 | end 13 | 14 | sig { 15 | type_parameters(:T) 16 | .params( 17 | demonstration: T.proc.params(matchers: Mocktail::MatcherPresentation).returns(T.type_parameter(:T)), 18 | demo_config: DemoConfig 19 | ).returns(Mocktail::Stubbing[T.type_parameter(:T)]) 20 | } 21 | def register(demonstration, demo_config) 22 | Stubbing.new( 23 | demonstration: demonstration, 24 | demo_config: demo_config, 25 | recording: @records_demonstration.record(demonstration, demo_config) 26 | ).tap do |stubbing| 27 | Mocktail.cabinet.store_stubbing(stubbing) 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/mocktail/sorbet/mocktail/replaces_type.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | require_relative "replaces_type/redefines_new" 4 | require_relative "replaces_type/redefines_singleton_methods" 5 | require_relative "replaces_type/runs_sorbet_sig_blocks_before_replacement" 6 | 7 | module Mocktail 8 | class ReplacesType 9 | extend T::Sig 10 | 11 | sig { void } 12 | def initialize 13 | @top_shelf = T.let(TopShelf.instance, TopShelf) 14 | @runs_sorbet_sig_blocks_before_replacement = T.let(RunsSorbetSigBlocksBeforeReplacement.new, RunsSorbetSigBlocksBeforeReplacement) 15 | @redefines_new = T.let(RedefinesNew.new, RedefinesNew) 16 | @redefines_singleton_methods = T.let(RedefinesSingletonMethods.new, RedefinesSingletonMethods) 17 | end 18 | 19 | sig { params(type: T.any(T::Class[T.anything], Module)).void } 20 | def replace(type) 21 | unless T.unsafe(type).is_a?(Class) || T.unsafe(type).is_a?(Module) 22 | raise UnsupportedMocktail.new("Mocktail.replace() only supports classes and modules") 23 | end 24 | 25 | @runs_sorbet_sig_blocks_before_replacement.run(type) 26 | 27 | if type.is_a?(Class) 28 | @top_shelf.register_new_replacement!(type) 29 | @redefines_new.redefine(type) 30 | end 31 | 32 | @top_shelf.register_singleton_method_replacement!(type) 33 | @redefines_singleton_methods.redefine(type) 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/mocktail/sorbet/mocktail/replaces_type/redefines_new.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | module Mocktail 4 | class RedefinesNew 5 | extend T::Sig 6 | 7 | sig { void } 8 | def initialize 9 | @handles_dry_new_call = T.let(HandlesDryNewCall.new, HandlesDryNewCall) 10 | end 11 | 12 | sig { params(type: T.any(T::Class[T.anything], Module)).void } 13 | def redefine(type) 14 | type_replacement = TopShelf.instance.type_replacement_for(type) 15 | 16 | if type_replacement.replacement_new.nil? 17 | type_replacement.original_new = type.method(:new) 18 | type.singleton_class.send(:undef_method, :new) 19 | handles_dry_new_call = @handles_dry_new_call 20 | type.define_singleton_method :new, ->(*args, **kwargs, &block) { 21 | if TopShelf.instance.new_replaced?(type) || 22 | (type.is_a?(Class) && TopShelf.instance.of_next_registered?(type)) 23 | handles_dry_new_call.handle(T.cast(type, T::Class[T.all(T, Object)]), args, kwargs, block) 24 | else 25 | type_replacement.original_new.call(*args, **kwargs, &block) 26 | end 27 | } 28 | type_replacement.replacement_new = type.singleton_method(:new) 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/mocktail/sorbet/mocktail/resets_state.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | module Mocktail 4 | class ResetsState 5 | extend T::Sig 6 | 7 | sig { void } 8 | def reset 9 | TopShelf.instance.reset_current_thread! 10 | Mocktail.cabinet.reset! 11 | ValidatesArguments.enable! 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/mocktail/sorbet/mocktail/share/bind.rb: -------------------------------------------------------------------------------- 1 | # typed: true 2 | 3 | module Mocktail 4 | module Bind 5 | # sig intentionally omitted, because the wrapper will cause infinite recursion if certain methods are mocked 6 | def self.call(mock, method_name, *args, **kwargs, &blk) # standard:disable Style/ArgumentsForwarding 7 | if Mocktail.cabinet.double_for_instance(mock) 8 | T.unsafe(Object.instance_method(method_name)).bind_call(mock, *args, **kwargs, &blk) # standard:disable Style/ArgumentsForwarding 9 | elsif (mock.is_a?(Module) || mock.is_a?(Class)) && 10 | (type_replacement = TopShelf.instance.type_replacement_if_exists_for(mock)) && 11 | (og_method = type_replacement.original_methods&.find { |m| m.name == method_name }) 12 | T.unsafe(og_method).call(*args, **kwargs, &blk) # standard:disable Style/ArgumentsForwarding 13 | else 14 | T.unsafe(mock).__send__(method_name, *args, **kwargs, &blk) # standard:disable Style/ArgumentsForwarding 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/mocktail/sorbet/mocktail/share/cleans_backtrace.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | module Mocktail 4 | class CleansBacktrace 5 | extend T::Sig 6 | 7 | sig { 8 | type_parameters(:T) 9 | .params(error: T.all(T.type_parameter(:T), StandardError)) 10 | .returns(T.type_parameter(:T)) 11 | } 12 | def clean(error) 13 | raise error 14 | rescue error.class => e 15 | T.cast(e, T.all(T.type_parameter(:T), StandardError)).tap do |e| 16 | e.set_backtrace(e.backtrace.drop_while { |frame| 17 | frame.start_with?(BASE_PATH, BASE_PATH) || frame.match?(/[\\|\/]sorbet-runtime.*[\\|\/]lib[\\|\/]types[\\|\/]private/) 18 | }) 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/mocktail/sorbet/mocktail/share/creates_identifier.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | module Mocktail 4 | class CreatesIdentifier 5 | extend T::Sig 6 | 7 | KEYWORDS = T.let(%w[__FILE__ __LINE__ alias and begin BEGIN break case class def defined? do else elsif end END ensure false for if in module next nil not or redo rescue retry return self super then true undef unless until when while yield], T::Array[String]) 8 | 9 | sig { params(s: T.anything, default: String, max_length: Integer).returns(String) } 10 | def create(s, default: "identifier", max_length: 24) 11 | case s 12 | when Kernel 13 | id = (s.to_s.downcase 14 | .gsub(/:0x[0-9a-f]+/, "") # Lazy attempt to wipe any Object:0x802beef identifiers 15 | .gsub(/[^\w\s]/, "") 16 | .gsub(/^\d+/, "")[0...max_length] || "") 17 | .strip 18 | .gsub(/\s+/, "_") # snake_case 19 | 20 | if id.empty? 21 | default 22 | else 23 | unreserved(id, default) 24 | end 25 | else 26 | default 27 | end 28 | end 29 | 30 | private 31 | 32 | sig { params(id: String, default: String).returns(String) } 33 | def unreserved(id, default) 34 | return id unless KEYWORDS.include?(id) 35 | 36 | "#{id}_#{default}" 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/mocktail/sorbet/mocktail/share/stringifies_method_name.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | module Mocktail 4 | class StringifiesMethodName 5 | extend T::Sig 6 | 7 | sig { params(call: Call).returns(String) } 8 | def stringify(call) 9 | [ 10 | call.original_type&.name, 11 | call.singleton ? "." : "#", 12 | call.method 13 | ].join 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/mocktail/sorbet/mocktail/simulates_argument_error.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | require_relative "simulates_argument_error/transforms_params" 4 | require_relative "simulates_argument_error/reconciles_args_with_params" 5 | require_relative "simulates_argument_error/recreates_message" 6 | require_relative "share/cleans_backtrace" 7 | require_relative "share/stringifies_call" 8 | 9 | module Mocktail 10 | class SimulatesArgumentError 11 | extend T::Sig 12 | 13 | sig { void } 14 | def initialize 15 | @transforms_params = T.let(TransformsParams.new, TransformsParams) 16 | @reconciles_args_with_params = T.let(ReconcilesArgsWithParams.new, ReconcilesArgsWithParams) 17 | @recreates_message = T.let(RecreatesMessage.new, RecreatesMessage) 18 | @cleans_backtrace = T.let(CleansBacktrace.new, CleansBacktrace) 19 | @stringifies_call = T.let(StringifiesCall.new, StringifiesCall) 20 | end 21 | 22 | sig { params(dry_call: Call).returns(T.nilable(ArgumentError)) } 23 | def simulate(dry_call) 24 | signature = @transforms_params.transform(dry_call) 25 | 26 | unless @reconciles_args_with_params.reconcile(signature) 27 | @cleans_backtrace.clean( 28 | ArgumentError.new([ 29 | @recreates_message.recreate(signature), 30 | "[Mocktail call: `#{@stringifies_call.stringify(dry_call)}']" 31 | ].join(" ")) 32 | ) 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/mocktail/sorbet/mocktail/simulates_argument_error/reconciles_args_with_params.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | module Mocktail 4 | class ReconcilesArgsWithParams 5 | extend T::Sig 6 | 7 | sig { params(signature: Signature).returns(T::Boolean) } 8 | def reconcile(signature) 9 | args_match?(signature.positional_params, signature.positional_args) && 10 | kwargs_match?(signature.keyword_params, signature.keyword_args) 11 | end 12 | 13 | private 14 | 15 | sig { params(arg_params: Params, args: T::Array[T.untyped]).returns(T::Boolean) } 16 | def args_match?(arg_params, args) 17 | args.size >= arg_params.required.size && 18 | (arg_params.rest? || args.size <= arg_params.allowed.size) 19 | end 20 | 21 | sig { params(kwarg_params: Params, kwargs: T::Hash[Symbol, T.untyped]).returns(T::Boolean) } 22 | def kwargs_match?(kwarg_params, kwargs) 23 | kwarg_params.required.all? { |name| kwargs.key?(name) } && 24 | (kwarg_params.rest? || kwargs.keys.all? { |name| kwarg_params.allowed.include?(name) }) 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/mocktail/sorbet/mocktail/sorbet.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | require_relative "sorbet/mocktail" 4 | -------------------------------------------------------------------------------- /lib/mocktail/sorbet/mocktail/typed.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | module Mocktail 4 | TYPED = true 5 | end 6 | -------------------------------------------------------------------------------- /lib/mocktail/sorbet/mocktail/value.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | require_relative "value/cabinet" 4 | require_relative "value/call" 5 | require_relative "value/demo_config" 6 | require_relative "value/explanation" 7 | require_relative "value/explanation_data" 8 | require_relative "value/double" 9 | require_relative "value/double_data" 10 | require_relative "value/fake_method_data" 11 | require_relative "value/matcher_registry" 12 | require_relative "value/no_explanation_data" 13 | require_relative "value/signature" 14 | require_relative "value/stubbing" 15 | require_relative "value/type_replacement" 16 | require_relative "value/type_replacement_data" 17 | require_relative "value/unsatisfying_call_explanation" 18 | require_relative "value/unsatisfying_call" 19 | require_relative "value/top_shelf" 20 | -------------------------------------------------------------------------------- /lib/mocktail/sorbet/mocktail/value/demo_config.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | module Mocktail 4 | class DemoConfig < T::Struct 5 | const :ignore_block, T::Boolean, default: false 6 | const :ignore_extra_args, T::Boolean, default: false 7 | const :ignore_arity, T::Boolean, default: false 8 | const :times, T.nilable(Integer), default: nil 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/mocktail/sorbet/mocktail/value/double.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | module Mocktail 4 | class Double < T::Struct 5 | const :original_type, T.any(T::Class[T.anything], Module) 6 | const :dry_type, T::Class[T.anything] 7 | const :dry_instance, Object 8 | const :dry_methods, T::Array[Symbol] 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/mocktail/sorbet/mocktail/value/double_data.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | require_relative "call" 4 | require_relative "stubbing" 5 | 6 | module Mocktail 7 | class DoubleData < T::Struct 8 | include ExplanationData 9 | 10 | const :type, T.any(T::Class[T.anything], Module) 11 | const :double, Object 12 | const :calls, T::Array[Call] 13 | const :stubbings, T::Array[Stubbing[T.anything]] 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/mocktail/sorbet/mocktail/value/explanation_data.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | module Mocktail 4 | module ExplanationData 5 | extend T::Helpers 6 | extend T::Sig 7 | 8 | interface! 9 | include Kernel 10 | 11 | sig { abstract.returns T::Array[Mocktail::Call] } 12 | def calls 13 | end 14 | 15 | sig { abstract.returns T::Array[Mocktail::Stubbing[T.anything]] } 16 | def stubbings 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/mocktail/sorbet/mocktail/value/fake_method_data.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | module Mocktail 4 | class FakeMethodData < T::Struct 5 | include ExplanationData 6 | 7 | const :receiver, T.anything 8 | const :calls, T::Array[Call] 9 | const :stubbings, T::Array[Stubbing[T.anything]] 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/mocktail/sorbet/mocktail/value/matcher_registry.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | module Mocktail 4 | class MatcherRegistry 5 | extend T::Sig 6 | 7 | sig { returns(MatcherRegistry) } 8 | def self.instance 9 | @matcher_registry ||= T.let(new, T.nilable(T.attached_class)) 10 | end 11 | 12 | sig { void } 13 | def initialize 14 | @matchers = T.let({}, T::Hash[Symbol, T.class_of(Matchers::Base)]) 15 | end 16 | 17 | sig { params(matcher_type: T.class_of(Matchers::Base)).void } 18 | def add(matcher_type) 19 | @matchers[matcher_type.matcher_name] = matcher_type 20 | end 21 | 22 | sig { params(name: Symbol).returns(T.nilable(T.class_of(Matchers::Base))) } 23 | def get(name) 24 | @matchers[name] 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/mocktail/sorbet/mocktail/value/no_explanation_data.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | module Mocktail 4 | class NoExplanationData < T::Struct 5 | extend T::Sig 6 | include ExplanationData 7 | 8 | const :thing, Object 9 | 10 | sig { override.returns(T::Array[Mocktail::Call]) } 11 | def calls 12 | raise Error.new("No calls have been recorded for #{thing.inspect}, because Mocktail doesn't know what it is.") 13 | end 14 | 15 | sig { override.returns T::Array[Mocktail::Stubbing[T.anything]] } 16 | def stubbings 17 | raise Error.new("No stubbings exist on #{thing.inspect}, because Mocktail doesn't know what it is.") 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/mocktail/sorbet/mocktail/value/signature.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | module Mocktail 4 | class Params < T::Struct 5 | extend T::Sig 6 | 7 | prop :all, T::Array[Symbol], default: [] 8 | prop :required, T::Array[Symbol], default: [] 9 | prop :optional, T::Array[Symbol], default: [] 10 | prop :rest, T.nilable(Symbol) 11 | 12 | sig { returns(T::Array[Symbol]) } 13 | def allowed 14 | all.select { |name| required.include?(name) || optional.include?(name) } 15 | end 16 | 17 | sig { returns(T::Boolean) } 18 | def rest? 19 | !!rest 20 | end 21 | end 22 | 23 | class Signature < T::Struct 24 | const :positional_params, Params 25 | const :positional_args, T::Array[T.anything] 26 | const :keyword_params, Params 27 | const :keyword_args, T::Hash[Symbol, T.anything] 28 | const :block_param, T.nilable(Symbol) 29 | const :block_arg, T.nilable(Proc), default: nil 30 | 31 | DEFAULT_REST_ARGS = "args" 32 | DEFAULT_REST_KWARGS = "kwargs" 33 | DEFAULT_BLOCK_PARAM = "blk" 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/mocktail/sorbet/mocktail/value/stubbing.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | module Mocktail 4 | class Stubbing < T::Struct 5 | extend T::Sig 6 | extend T::Generic 7 | MethodReturnType = type_member 8 | 9 | const :demonstration, T.proc.params(matchers: Mocktail::MatcherPresentation).returns(MethodReturnType) 10 | const :demo_config, DemoConfig 11 | prop :satisfaction_count, Integer, default: 0 12 | const :recording, Call 13 | prop :effect, T.nilable(T.proc.params(call: Mocktail::Call).returns(MethodReturnType)) 14 | 15 | sig { void } 16 | def satisfied! 17 | self.satisfaction_count += 1 18 | end 19 | 20 | sig { params(block: T.proc.params(call: Mocktail::Call).returns(MethodReturnType)).void } 21 | def with(&block) 22 | self.effect = block 23 | nil 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/mocktail/sorbet/mocktail/value/type_replacement.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | module Mocktail 4 | class TypeReplacement < T::Struct 5 | const :type, T.any(T::Class[T.anything], Module) 6 | prop :original_methods, T.nilable(T::Array[Method]) 7 | prop :replacement_methods, T.nilable(T::Array[Method]) 8 | prop :original_new, T.nilable(Method) 9 | prop :replacement_new, T.nilable(Method) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/mocktail/sorbet/mocktail/value/type_replacement_data.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | module Mocktail 4 | class TypeReplacementData < T::Struct 5 | extend T::Sig 6 | 7 | const :type, T.any(T::Class[T.anything], Module) 8 | const :replaced_method_names, T::Array[Symbol] 9 | const :calls, T::Array[Call] 10 | const :stubbings, T::Array[Stubbing[T.anything]] 11 | 12 | include ExplanationData 13 | 14 | sig { returns(T.any(T::Class[T.anything], Module)) } 15 | def double 16 | type 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/mocktail/sorbet/mocktail/value/unsatisfying_call.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | module Mocktail 4 | class UnsatisfyingCall < T::Struct 5 | const :call, Call 6 | const :other_stubbings, T::Array[Stubbing[T.anything]] 7 | const :backtrace, T::Array[String] 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/mocktail/sorbet/mocktail/value/unsatisfying_call_explanation.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | module Mocktail 4 | class UnsatisfyingCallExplanation 5 | extend T::Sig 6 | 7 | sig { returns(UnsatisfyingCall) } 8 | attr_reader :reference 9 | 10 | sig { returns(String) } 11 | attr_reader :message 12 | 13 | sig { params(reference: UnsatisfyingCall, message: String).void } 14 | def initialize(reference, message) 15 | @reference = reference 16 | @message = message 17 | end 18 | 19 | sig { returns(T.class_of(UnsatisfyingCallExplanation)) } 20 | def type 21 | self.class 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/mocktail/sorbet/mocktail/verifies_call.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | require_relative "records_demonstration" 4 | require_relative "verifies_call/finds_verifiable_calls" 5 | require_relative "verifies_call/raises_verification_error" 6 | 7 | module Mocktail 8 | class VerifiesCall 9 | extend T::Sig 10 | 11 | sig { void } 12 | def initialize 13 | @records_demonstration = T.let(RecordsDemonstration.new, RecordsDemonstration) 14 | @finds_verifiable_calls = T.let(FindsVerifiableCalls.new, FindsVerifiableCalls) 15 | @raises_verification_error = T.let(RaisesVerificationError.new, RaisesVerificationError) 16 | end 17 | 18 | sig { params(demo: T.proc.params(matchers: Mocktail::MatcherPresentation).void, demo_config: DemoConfig).void } 19 | def verify(demo, demo_config) 20 | recording = @records_demonstration.record(demo, demo_config) 21 | verifiable_calls = @finds_verifiable_calls.find(recording, demo_config) 22 | 23 | unless verification_satisfied?(verifiable_calls.size, demo_config) 24 | @raises_verification_error.raise(recording, verifiable_calls, demo_config) 25 | end 26 | nil 27 | end 28 | 29 | private 30 | 31 | sig { params(verifiable_call_count: Integer, demo_config: DemoConfig).returns(T::Boolean) } 32 | def verification_satisfied?(verifiable_call_count, demo_config) 33 | (demo_config.times.nil? && verifiable_call_count > 0) || 34 | (demo_config.times == verifiable_call_count) 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/mocktail/sorbet/mocktail/verifies_call/finds_verifiable_calls.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | require_relative "../share/determines_matching_calls" 4 | 5 | module Mocktail 6 | class FindsVerifiableCalls 7 | extend T::Sig 8 | 9 | sig { void } 10 | def initialize 11 | @determines_matching_calls = T.let(DeterminesMatchingCalls.new, DeterminesMatchingCalls) 12 | end 13 | 14 | sig { params(recording: Call, demo_config: DemoConfig).returns(T::Array[Call]) } 15 | def find(recording, demo_config) 16 | Mocktail.cabinet.calls.select { |call| 17 | @determines_matching_calls.determine(call, recording, demo_config) 18 | } 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/mocktail/sorbet/mocktail/verifies_call/raises_verification_error/gathers_calls_of_method.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | module Mocktail 4 | class GathersCallsOfMethod 5 | extend T::Sig 6 | 7 | sig { params(dry_call: Call).returns(T::Array[Call]) } 8 | def gather(dry_call) 9 | Mocktail.cabinet.calls.select { |call| 10 | call.double == dry_call.double && 11 | call.method == dry_call.method 12 | } 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/mocktail/sorbet/mocktail/version.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | module Mocktail 4 | # The gemspec will define Module::VERSION as loaded from lib/, but if the 5 | # user loads mocktail/sorbet, its version file will be effectively redefining 6 | # it. Undef it first to ensure we don't spew warnings 7 | if defined?(VERSION) 8 | Mocktail.send(:remove_const, :VERSION) 9 | end 10 | 11 | VERSION = "2.0.0" 12 | end 13 | -------------------------------------------------------------------------------- /lib/mocktail/typed.rb: -------------------------------------------------------------------------------- 1 | module Mocktail 2 | TYPED = false 3 | end 4 | -------------------------------------------------------------------------------- /lib/mocktail/value.rb: -------------------------------------------------------------------------------- 1 | require_relative "value/cabinet" 2 | require_relative "value/call" 3 | require_relative "value/demo_config" 4 | require_relative "value/explanation" 5 | require_relative "value/explanation_data" 6 | require_relative "value/double" 7 | require_relative "value/double_data" 8 | require_relative "value/fake_method_data" 9 | require_relative "value/matcher_registry" 10 | require_relative "value/no_explanation_data" 11 | require_relative "value/signature" 12 | require_relative "value/stubbing" 13 | require_relative "value/type_replacement" 14 | require_relative "value/type_replacement_data" 15 | require_relative "value/unsatisfying_call_explanation" 16 | require_relative "value/unsatisfying_call" 17 | require_relative "value/top_shelf" 18 | -------------------------------------------------------------------------------- /lib/mocktail/value/call.rb: -------------------------------------------------------------------------------- 1 | module Mocktail 2 | class Call < T::Struct 3 | extend T::Sig 4 | 5 | const :singleton 6 | const :double, default: nil 7 | const :original_type 8 | const :dry_type 9 | const :method, without_accessors: true 10 | const :original_method 11 | const :args, default: [] 12 | const :kwargs, default: {} 13 | # At present, there's no way to type optional/variadic params in blocks 14 | # (i.e. `T.proc.params(*T.untyped).returns(T.untyped)` doesn't work) 15 | # 16 | # See: https://github.com/sorbet/sorbet/issues/1142#issuecomment-1586195730 17 | const :block 18 | 19 | attr_reader :method 20 | 21 | # Because T::Struct compares with referential equality, we need 22 | # to redefine the equality methods to compare the values of the attributes. 23 | 24 | def ==(other) 25 | eql?(other) 26 | end 27 | 28 | def eql?(other) 29 | case other 30 | when Call 31 | [ 32 | :singleton, :double, :original_type, :dry_type, 33 | :method, :original_method, :args, :kwargs, :block 34 | ].all? { |attr| 35 | instance_variable_get(:"@#{attr}") == other.send(attr) 36 | } 37 | else 38 | false 39 | end 40 | end 41 | 42 | def hash 43 | [@singleton, @double, @original_type, @dry_type, @method, @original_method, @args, @kwargs, @block].hash 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/mocktail/value/demo_config.rb: -------------------------------------------------------------------------------- 1 | module Mocktail 2 | class DemoConfig < T::Struct 3 | const :ignore_block, default: false 4 | const :ignore_extra_args, default: false 5 | const :ignore_arity, default: false 6 | const :times, default: nil 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/mocktail/value/double.rb: -------------------------------------------------------------------------------- 1 | module Mocktail 2 | class Double < T::Struct 3 | const :original_type 4 | const :dry_type 5 | const :dry_instance 6 | const :dry_methods 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/mocktail/value/double_data.rb: -------------------------------------------------------------------------------- 1 | require_relative "call" 2 | require_relative "stubbing" 3 | 4 | module Mocktail 5 | class DoubleData < T::Struct 6 | include ExplanationData 7 | 8 | const :type 9 | const :double 10 | const :calls 11 | const :stubbings 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/mocktail/value/explanation.rb: -------------------------------------------------------------------------------- 1 | module Mocktail 2 | class Explanation 3 | extend T::Sig 4 | 5 | attr_reader :reference 6 | 7 | attr_reader :message 8 | 9 | def initialize(reference, message) 10 | @reference = reference 11 | @message = message 12 | end 13 | 14 | def type 15 | self.class 16 | end 17 | end 18 | 19 | class NoExplanation < Explanation 20 | attr_reader :reference 21 | 22 | def initialize(reference, message) 23 | @reference = reference 24 | @message = message 25 | end 26 | end 27 | 28 | class DoubleExplanation < Explanation 29 | attr_reader :reference 30 | 31 | def initialize(reference, message) 32 | @reference = reference 33 | @message = message 34 | end 35 | end 36 | 37 | class ReplacedTypeExplanation < Explanation 38 | attr_reader :reference 39 | 40 | def initialize(reference, message) 41 | @reference = reference 42 | @message = message 43 | end 44 | end 45 | 46 | class FakeMethodExplanation < Explanation 47 | attr_reader :reference 48 | 49 | def initialize(reference, message) 50 | @reference = reference 51 | @message = message 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /lib/mocktail/value/explanation_data.rb: -------------------------------------------------------------------------------- 1 | module Mocktail 2 | module ExplanationData 3 | extend T::Helpers 4 | extend T::Sig 5 | 6 | include Kernel 7 | 8 | def calls 9 | end 10 | 11 | def stubbings 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/mocktail/value/fake_method_data.rb: -------------------------------------------------------------------------------- 1 | module Mocktail 2 | class FakeMethodData < T::Struct 3 | include ExplanationData 4 | 5 | const :receiver 6 | const :calls 7 | const :stubbings 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/mocktail/value/matcher_registry.rb: -------------------------------------------------------------------------------- 1 | module Mocktail 2 | class MatcherRegistry 3 | extend T::Sig 4 | 5 | def self.instance 6 | @matcher_registry ||= new 7 | end 8 | 9 | def initialize 10 | @matchers = {} 11 | end 12 | 13 | def add(matcher_type) 14 | @matchers[matcher_type.matcher_name] = matcher_type 15 | end 16 | 17 | def get(name) 18 | @matchers[name] 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/mocktail/value/no_explanation_data.rb: -------------------------------------------------------------------------------- 1 | module Mocktail 2 | class NoExplanationData < T::Struct 3 | extend T::Sig 4 | include ExplanationData 5 | 6 | const :thing 7 | 8 | def calls 9 | raise Error.new("No calls have been recorded for #{thing.inspect}, because Mocktail doesn't know what it is.") 10 | end 11 | 12 | def stubbings 13 | raise Error.new("No stubbings exist on #{thing.inspect}, because Mocktail doesn't know what it is.") 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/mocktail/value/signature.rb: -------------------------------------------------------------------------------- 1 | module Mocktail 2 | class Params < T::Struct 3 | extend T::Sig 4 | 5 | prop :all, default: [] 6 | prop :required, default: [] 7 | prop :optional, default: [] 8 | prop :rest 9 | 10 | def allowed 11 | all.select { |name| required.include?(name) || optional.include?(name) } 12 | end 13 | 14 | def rest? 15 | !!rest 16 | end 17 | end 18 | 19 | class Signature < T::Struct 20 | const :positional_params 21 | const :positional_args 22 | const :keyword_params 23 | const :keyword_args 24 | const :block_param 25 | const :block_arg, default: nil 26 | 27 | DEFAULT_REST_ARGS = "args" 28 | DEFAULT_REST_KWARGS = "kwargs" 29 | DEFAULT_BLOCK_PARAM = "blk" 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/mocktail/value/stubbing.rb: -------------------------------------------------------------------------------- 1 | module Mocktail 2 | class Stubbing < T::Struct 3 | extend T::Sig 4 | extend T::Generic 5 | MethodReturnType = type_member 6 | 7 | const :demonstration 8 | const :demo_config 9 | prop :satisfaction_count, default: 0 10 | const :recording 11 | prop :effect 12 | 13 | def satisfied! 14 | self.satisfaction_count += 1 15 | end 16 | 17 | def with(&block) 18 | self.effect = block 19 | nil 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/mocktail/value/type_replacement.rb: -------------------------------------------------------------------------------- 1 | module Mocktail 2 | class TypeReplacement < T::Struct 3 | const :type 4 | prop :original_methods 5 | prop :replacement_methods 6 | prop :original_new 7 | prop :replacement_new 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/mocktail/value/type_replacement_data.rb: -------------------------------------------------------------------------------- 1 | module Mocktail 2 | class TypeReplacementData < T::Struct 3 | extend T::Sig 4 | 5 | const :type 6 | const :replaced_method_names 7 | const :calls 8 | const :stubbings 9 | 10 | include ExplanationData 11 | 12 | def double 13 | type 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/mocktail/value/unsatisfying_call.rb: -------------------------------------------------------------------------------- 1 | module Mocktail 2 | class UnsatisfyingCall < T::Struct 3 | const :call 4 | const :other_stubbings 5 | const :backtrace 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/mocktail/value/unsatisfying_call_explanation.rb: -------------------------------------------------------------------------------- 1 | module Mocktail 2 | class UnsatisfyingCallExplanation 3 | extend T::Sig 4 | 5 | attr_reader :reference 6 | 7 | attr_reader :message 8 | 9 | def initialize(reference, message) 10 | @reference = reference 11 | @message = message 12 | end 13 | 14 | def type 15 | self.class 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/mocktail/verifies_call.rb: -------------------------------------------------------------------------------- 1 | require_relative "records_demonstration" 2 | require_relative "verifies_call/finds_verifiable_calls" 3 | require_relative "verifies_call/raises_verification_error" 4 | 5 | module Mocktail 6 | class VerifiesCall 7 | extend T::Sig 8 | 9 | def initialize 10 | @records_demonstration = RecordsDemonstration.new 11 | @finds_verifiable_calls = FindsVerifiableCalls.new 12 | @raises_verification_error = RaisesVerificationError.new 13 | end 14 | 15 | def verify(demo, demo_config) 16 | recording = @records_demonstration.record(demo, demo_config) 17 | verifiable_calls = @finds_verifiable_calls.find(recording, demo_config) 18 | 19 | unless verification_satisfied?(verifiable_calls.size, demo_config) 20 | @raises_verification_error.raise(recording, verifiable_calls, demo_config) 21 | end 22 | nil 23 | end 24 | 25 | private 26 | 27 | def verification_satisfied?(verifiable_call_count, demo_config) 28 | (demo_config.times.nil? && verifiable_call_count > 0) || 29 | (demo_config.times == verifiable_call_count) 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/mocktail/verifies_call/finds_verifiable_calls.rb: -------------------------------------------------------------------------------- 1 | require_relative "../share/determines_matching_calls" 2 | 3 | module Mocktail 4 | class FindsVerifiableCalls 5 | extend T::Sig 6 | 7 | def initialize 8 | @determines_matching_calls = DeterminesMatchingCalls.new 9 | end 10 | 11 | def find(recording, demo_config) 12 | Mocktail.cabinet.calls.select { |call| 13 | @determines_matching_calls.determine(call, recording, demo_config) 14 | } 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/mocktail/verifies_call/raises_verification_error/gathers_calls_of_method.rb: -------------------------------------------------------------------------------- 1 | module Mocktail 2 | class GathersCallsOfMethod 3 | extend T::Sig 4 | 5 | def gather(dry_call) 6 | Mocktail.cabinet.calls.select { |call| 7 | call.double == dry_call.double && 8 | call.method == dry_call.method 9 | } 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/mocktail/version.rb: -------------------------------------------------------------------------------- 1 | module Mocktail 2 | # The gemspec will define Module::VERSION as loaded from lib/, but if the 3 | # user loads mocktail/sorbet, its version file will be effectively redefining 4 | # it. Undef it first to ensure we don't spew warnings 5 | if defined?(VERSION) 6 | Mocktail.send(:remove_const, :VERSION) 7 | end 8 | 9 | VERSION = "2.0.0" 10 | end 11 | -------------------------------------------------------------------------------- /mocktail.gemspec: -------------------------------------------------------------------------------- 1 | begin 2 | require_relative "lib/mocktail/version" 3 | rescue LoadError 4 | require_relative "src/mocktail/version" 5 | end 6 | 7 | Gem::Specification.new do |spec| 8 | spec.name = "mocktail" 9 | spec.version = Mocktail::VERSION 10 | spec.authors = ["Justin Searls"] 11 | spec.email = ["searls@gmail.com"] 12 | 13 | spec.summary = "Take your objects, and make them a double" 14 | spec.homepage = "https://github.com/testdouble/mocktail" 15 | spec.required_ruby_version = Gem::Requirement.new(">= 3.0.0") 16 | 17 | spec.metadata["homepage_uri"] = spec.homepage 18 | spec.metadata["source_code_uri"] = spec.homepage 19 | spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/main/CHANGELOG.md" 20 | spec.metadata["rubygems_mfa_required"] = "true" 21 | 22 | # Specify which files should be added to the gem when it is released. 23 | # The `git ls-files -z` loads the files in the RubyGem that have been added into git. 24 | spec.files = Dir.chdir(File.expand_path(__dir__)) do 25 | `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) } 26 | end 27 | spec.bindir = "exe" 28 | spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) } 29 | spec.require_paths = ["lib"] 30 | 31 | spec.add_dependency "sorbet-runtime", "~> 0.5.9204" 32 | spec.add_dependency "sorbet-eraser", "~> 0.3.1" 33 | 34 | # For more information and examples about making a new gem, checkout our 35 | # guide at: https://bundler.io/guides/creating_gem.html 36 | end 37 | -------------------------------------------------------------------------------- /rbi/sorbet-runtime.rbi: -------------------------------------------------------------------------------- 1 | # typed: true 2 | 3 | # sorbet gem fails to export some of these constants, so we need to in order to 4 | # pass static typecheck 5 | module T 6 | module Private 7 | module RuntimeLevels 8 | class << self 9 | sig { returns(Symbol) } 10 | def default_checked_level 11 | end 12 | end 13 | end 14 | 15 | module Methods 16 | class Signature 17 | sig { returns(T::Array[T::Array[Symbol]]) } 18 | def parameters 19 | end 20 | end 21 | 22 | module MethodHooks 23 | end 24 | 25 | module SingletonMethodHooks 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /script/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -xeuo pipefail 4 | 5 | bundle 6 | 7 | cd sub_projects/rbi_generator/ 8 | bundle 9 | cd ../.. 10 | 11 | cd sub_projects/sorbet_user/ 12 | bundle 13 | cd ../.. 14 | 15 | cd sub_projects/untyped_user/ 16 | bundle 17 | cd ../.. 18 | -------------------------------------------------------------------------------- /script/spoom_me: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -xeuo pipefail 4 | 5 | bundle exec spoom coverage --save 6 | bundle exec spoom coverage report 7 | bundle exec spoom coverage open 8 | -------------------------------------------------------------------------------- /script/strip_sigils: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Removes all the sigil comments from the top of our sorbet-free code 4 | 5 | # Find all files that start with "# typed" in the first line 6 | files=$(grep -rl '^# typed' --exclude-dir=lib/mocktail/sorbet lib/*) 7 | 8 | # Loop through each file and delete the first line 9 | for file in $files 10 | do 11 | # Check if we're on a Mac 12 | if [[ "$OSTYPE" == "darwin"* ]]; then 13 | sed -i '' '1d' $file 14 | else 15 | sed -i '1d' $file 16 | fi 17 | done 18 | -------------------------------------------------------------------------------- /script/test: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -xeuo pipefail 4 | 5 | echo "-----> Running Mocktail's test against src" 6 | COVER=true bundle exec rake 7 | 8 | echo "-----> Type-checking Mocktail source" 9 | bundle exec srb tc 10 | 11 | echo "-----> Building src/mocktail into lib/mocktail and lib/mocktail/sorbet" 12 | ./script/build 13 | 14 | echo "-----> Running Mocktail's test against lib" 15 | MOCKTAIL_TEST_SRC_DIRECTORY="lib" bundle exec rake 16 | 17 | echo "-----> Running Mocktail's test against lib/mocktail/sorbet" 18 | MOCKTAIL_TEST_SRC_DIRECTORY="lib/mocktail/sorbet" bundle exec rake 19 | 20 | echo "-----> Ensuring that double-requiring Mocktail produces the right warning and bails out of redefining constants" 21 | script/test_double_require_warnings 22 | 23 | echo "-----> Type-checking the copy of Mocktail in lib/mocktail/sorbet" 24 | bundle exec srb tc --ignore src/ --dir lib/mocktail/sorbet 25 | 26 | echo "-----> Running sample project tests" 27 | cd sub_projects/sorbet_user 28 | ./script/test 29 | cd ../.. 30 | 31 | cd sub_projects/untyped_user 32 | ./script/test 33 | cd ../.. 34 | -------------------------------------------------------------------------------- /script/update: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -xeuo pipefail 4 | 5 | echo "-----> Updating root gems" 6 | bundle update 7 | 8 | echo "-----> Updating 'sub_projects/rbi_generator' gems" 9 | cd sub_projects/rbi_generator 10 | bundle update 11 | cd ../.. 12 | 13 | echo "-----> Updating 'sub_projects/sorbet_user' gems" 14 | cd sub_projects/sorbet_user 15 | bundle update 16 | cd ../.. 17 | 18 | echo "-----> Updating 'sub_projects/untyped_user' gems" 19 | cd sub_projects/untyped_user 20 | bundle update 21 | cd ../.. 22 | -------------------------------------------------------------------------------- /sorbet/config: -------------------------------------------------------------------------------- 1 | --dir 2 | . 3 | --ignore=tmp/ 4 | --ignore=Rakefile 5 | --ignore=vendor/ 6 | --ignore=sub_projects/sorbet_user/ 7 | --ignore=sub_projects/rbi_generator/ 8 | --ignore=lib/ 9 | # Ignore b/c these stubs will confuse the typechecker and generate a bunch of errors, even tho #typed: false 10 | --ignore=test/support/sorbet_stubs.rb 11 | # Ignore b/c this pregenerated rbi is for consumers of the gem, not the gem itself 12 | --ignore=rbi/mocktail-pregenerated.rbi 13 | -------------------------------------------------------------------------------- /sorbet/rbi/gems/rubocop-performance@1.18.0.rbi: -------------------------------------------------------------------------------- 1 | # typed: true 2 | 3 | # DO NOT EDIT MANUALLY 4 | # This is an autogenerated file for types exported from the `rubocop-performance` gem. 5 | # Please instead update this file by running `bin/tapioca gem rubocop-performance`. 6 | 7 | # THIS IS AN EMPTY RBI FILE. 8 | # see https://github.com/Shopify/tapioca#manually-requiring-parts-of-a-gem 9 | -------------------------------------------------------------------------------- /sorbet/rbi/gems/simplecov_json_formatter@0.1.4.rbi: -------------------------------------------------------------------------------- 1 | # typed: true 2 | 3 | # DO NOT EDIT MANUALLY 4 | # This is an autogenerated file for types exported from the `simplecov_json_formatter` gem. 5 | # Please instead update this file by running `bin/tapioca gem simplecov_json_formatter`. 6 | 7 | # THIS IS AN EMPTY RBI FILE. 8 | # see https://github.com/Shopify/tapioca#manually-requiring-parts-of-a-gem 9 | -------------------------------------------------------------------------------- /sorbet/rbi/gems/standard-performance@1.1.0.rbi: -------------------------------------------------------------------------------- 1 | # typed: true 2 | 3 | # DO NOT EDIT MANUALLY 4 | # This is an autogenerated file for types exported from the `standard-performance` gem. 5 | # Please instead update this file by running `bin/tapioca gem standard-performance`. 6 | 7 | # THIS IS AN EMPTY RBI FILE. 8 | # see https://github.com/Shopify/tapioca#manually-requiring-parts-of-a-gem 9 | -------------------------------------------------------------------------------- /sorbet/tapioca/config.yml: -------------------------------------------------------------------------------- 1 | gem: 2 | # Add your `gem` command parameters here: 3 | # 4 | # exclude: 5 | # - gem_name 6 | # doc: true 7 | # workers: 5 8 | dsl: 9 | # Add your `dsl` command parameters here: 10 | # 11 | # exclude: 12 | # - SomeGeneratorName 13 | # workers: 5 14 | -------------------------------------------------------------------------------- /sorbet/tapioca/require.rb: -------------------------------------------------------------------------------- 1 | # typed: true 2 | # frozen_string_literal: true 3 | 4 | # Add your extra requires here (`bin/tapioca require` can be used to bootstrap this list) 5 | -------------------------------------------------------------------------------- /spoom_data/05f0c49.json: -------------------------------------------------------------------------------- 1 | {"timestamp":1687395183,"version_static":"0.5.10884","version_runtime":"0.5.10884","duration":331818,"commit_sha":"05f0c49","commit_timestamp":1687393452,"files":151,"rbi_files":38,"modules":475,"classes":5849,"singleton_classes":3386,"methods_without_sig":21736,"methods_with_sig":1731,"calls_untyped":1335,"calls_typed":4174,"sigils":{"false":2,"true":88,"strict":61},"methods_with_sig_excluding_rbis":289,"methods_without_sig_excluding_rbis":578,"sigils_excluding_rbis":{"false":1,"true":54,"strict":60}} -------------------------------------------------------------------------------- /spoom_data/1657012.json: -------------------------------------------------------------------------------- 1 | {"timestamp":1687484428,"version_static":"0.5.10884","version_runtime":"0.5.10884","duration":887210,"commit_sha":"1657012","commit_timestamp":1687483861,"files":151,"rbi_files":38,"modules":477,"classes":5854,"singleton_classes":3390,"methods_without_sig":21679,"methods_with_sig":1796,"calls_untyped":1142,"calls_typed":4724,"sigils":{"false":2,"true":66,"strict":83},"methods_with_sig_excluding_rbis":354,"methods_without_sig_excluding_rbis":521,"sigils_excluding_rbis":{"false":1,"true":32,"strict":82}} -------------------------------------------------------------------------------- /spoom_data/167434b.json: -------------------------------------------------------------------------------- 1 | {"timestamp":1687366215,"version_static":"0.5.10884","version_runtime":"0.5.10884","duration":860074,"commit_sha":"167434b","commit_timestamp":1687365976,"files":151,"rbi_files":38,"modules":475,"classes":5847,"singleton_classes":3385,"methods_without_sig":21852,"methods_with_sig":1613,"calls_untyped":1720,"calls_typed":3328,"sigils":{"false":2,"true":127,"strict":22},"methods_with_sig_excluding_rbis":171,"methods_without_sig_excluding_rbis":694,"sigils_excluding_rbis":{"false":1,"true":93,"strict":21}} -------------------------------------------------------------------------------- /spoom_data/1d0ba5b.json: -------------------------------------------------------------------------------- 1 | {"timestamp":1687361195,"version_static":"0.5.10884","version_runtime":"0.5.10884","duration":831677,"commit_sha":"1d0ba5b","commit_timestamp":1687361189,"files":151,"rbi_files":38,"modules":475,"classes":5847,"singleton_classes":3385,"methods_without_sig":21869,"methods_with_sig":1596,"calls_untyped":1782,"calls_typed":3150,"sigils":{"false":2,"true":129,"strict":20},"methods_with_sig_excluding_rbis":154,"methods_without_sig_excluding_rbis":711,"sigils_excluding_rbis":{"false":1,"true":95,"strict":19}} -------------------------------------------------------------------------------- /spoom_data/1ffa724.json: -------------------------------------------------------------------------------- 1 | {"timestamp":1687486316,"version_static":"0.5.10884","version_runtime":"0.5.10884","duration":812087,"commit_sha":"1ffa724","commit_timestamp":1687486065,"files":151,"rbi_files":38,"modules":477,"classes":5854,"singleton_classes":3390,"methods_without_sig":21567,"methods_with_sig":1907,"calls_untyped":1145,"calls_typed":5117,"sigils":{"false":2,"true":58,"strict":91},"methods_with_sig_excluding_rbis":465,"methods_without_sig_excluding_rbis":409,"sigils_excluding_rbis":{"false":1,"true":24,"strict":90}} -------------------------------------------------------------------------------- /spoom_data/2129b3d.json: -------------------------------------------------------------------------------- 1 | {"timestamp":1687365970,"version_static":"0.5.10884","version_runtime":"0.5.10884","duration":767087,"commit_sha":"2129b3d","commit_timestamp":1687361189,"files":151,"rbi_files":38,"modules":475,"classes":5847,"singleton_classes":3385,"methods_without_sig":21857,"methods_with_sig":1608,"calls_untyped":1742,"calls_typed":3272,"sigils":{"false":2,"true":128,"strict":21},"methods_with_sig_excluding_rbis":166,"methods_without_sig_excluding_rbis":699,"sigils_excluding_rbis":{"false":1,"true":94,"strict":20}} -------------------------------------------------------------------------------- /spoom_data/24b1c92.json: -------------------------------------------------------------------------------- 1 | {"timestamp":1687351288,"version_static":"0.5.10884","version_runtime":"0.5.10884","duration":807698,"commit_sha":"24b1c92","commit_timestamp":1687349568,"files":151,"rbi_files":38,"modules":475,"classes":5847,"singleton_classes":3385,"methods_without_sig":21912,"methods_with_sig":1560,"calls_untyped":1231,"calls_typed":2265,"sigils":{"false":58,"true":79,"strict":14},"methods_with_sig_excluding_rbis":118,"methods_without_sig_excluding_rbis":754,"sigils_excluding_rbis":{"false":57,"true":45,"strict":13}} -------------------------------------------------------------------------------- /spoom_data/2c46aee.json: -------------------------------------------------------------------------------- 1 | {"timestamp":1687382282,"version_static":"0.5.10884","version_runtime":"0.5.10884","duration":747771,"commit_sha":"2c46aee","commit_timestamp":1687378443,"files":151,"rbi_files":38,"modules":475,"classes":5849,"singleton_classes":3386,"methods_without_sig":21757,"methods_with_sig":1710,"calls_untyped":1443,"calls_typed":3989,"sigils":{"false":2,"true":97,"strict":52},"methods_with_sig_excluding_rbis":268,"methods_without_sig_excluding_rbis":599,"sigils_excluding_rbis":{"false":1,"true":63,"strict":51}} -------------------------------------------------------------------------------- /spoom_data/305ec0b.json: -------------------------------------------------------------------------------- 1 | {"timestamp":1687351598,"version_static":"0.5.10884","version_runtime":"0.5.10884","duration":841583,"commit_sha":"305ec0b","commit_timestamp":1687351561,"files":151,"rbi_files":38,"modules":475,"classes":5847,"singleton_classes":3385,"methods_without_sig":21912,"methods_with_sig":1560,"calls_untyped":1530,"calls_typed":2525,"sigils":{"false":29,"true":108,"strict":14},"methods_with_sig_excluding_rbis":118,"methods_without_sig_excluding_rbis":754,"sigils_excluding_rbis":{"false":28,"true":74,"strict":13}} -------------------------------------------------------------------------------- /spoom_data/30e9528.json: -------------------------------------------------------------------------------- 1 | {"timestamp":1687355661,"version_static":"0.5.10884","version_runtime":"0.5.10884","duration":818651,"commit_sha":"30e9528","commit_timestamp":1687353030,"files":151,"rbi_files":38,"modules":475,"classes":5847,"singleton_classes":3385,"methods_without_sig":21897,"methods_with_sig":1574,"calls_untyped":1860,"calls_typed":2984,"sigils":{"false":2,"true":135,"strict":14},"methods_with_sig_excluding_rbis":132,"methods_without_sig_excluding_rbis":739,"sigils_excluding_rbis":{"false":1,"true":101,"strict":13}} -------------------------------------------------------------------------------- /spoom_data/4638cd5.json: -------------------------------------------------------------------------------- 1 | {"timestamp":1687460236,"version_static":"0.5.10884","version_runtime":"0.5.10884","duration":836744,"commit_sha":"4638cd5","commit_timestamp":1687451837,"files":151,"rbi_files":38,"modules":475,"classes":5852,"singleton_classes":3388,"methods_without_sig":21698,"methods_with_sig":1773,"calls_untyped":1132,"calls_typed":4626,"sigils":{"false":2,"true":69,"strict":80},"methods_with_sig_excluding_rbis":331,"methods_without_sig_excluding_rbis":540,"sigils_excluding_rbis":{"false":1,"true":35,"strict":79}} -------------------------------------------------------------------------------- /spoom_data/47c7dad.json: -------------------------------------------------------------------------------- 1 | {"timestamp":1687486055,"version_static":"0.5.10884","version_runtime":"0.5.10884","duration":593753,"commit_sha":"47c7dad","commit_timestamp":1687484487,"files":151,"rbi_files":38,"modules":477,"classes":5854,"singleton_classes":3390,"methods_without_sig":21583,"methods_with_sig":1891,"calls_untyped":1141,"calls_typed":5062,"sigils":{"false":2,"true":59,"strict":90},"methods_with_sig_excluding_rbis":449,"methods_without_sig_excluding_rbis":425,"sigils_excluding_rbis":{"false":1,"true":25,"strict":89}} -------------------------------------------------------------------------------- /spoom_data/4b1edef.json: -------------------------------------------------------------------------------- 1 | {"timestamp":1687351537,"version_static":"0.5.10884","version_runtime":"0.5.10884","duration":783385,"commit_sha":"4b1edef","commit_timestamp":1687351517,"files":151,"rbi_files":38,"modules":475,"classes":5847,"singleton_classes":3385,"methods_without_sig":21912,"methods_with_sig":1560,"calls_untyped":1338,"calls_typed":2399,"sigils":{"false":38,"true":99,"strict":14},"methods_with_sig_excluding_rbis":118,"methods_without_sig_excluding_rbis":754,"sigils_excluding_rbis":{"false":37,"true":65,"strict":13}} -------------------------------------------------------------------------------- /spoom_data/4de157f.json: -------------------------------------------------------------------------------- 1 | {"timestamp":1687351638,"version_static":"0.5.10884","version_runtime":"0.5.10884","duration":801064,"commit_sha":"4de157f","commit_timestamp":1687351618,"files":151,"rbi_files":38,"modules":475,"classes":5847,"singleton_classes":3385,"methods_without_sig":21912,"methods_with_sig":1560,"calls_untyped":1658,"calls_typed":2640,"sigils":{"false":21,"true":116,"strict":14},"methods_with_sig_excluding_rbis":118,"methods_without_sig_excluding_rbis":754,"sigils_excluding_rbis":{"false":20,"true":82,"strict":13}} -------------------------------------------------------------------------------- /spoom_data/526e7db.json: -------------------------------------------------------------------------------- 1 | {"timestamp":1687368777,"version_static":"0.5.10884","version_runtime":"0.5.10884","duration":861951,"commit_sha":"526e7db","commit_timestamp":1687366591,"files":151,"rbi_files":38,"modules":475,"classes":5847,"singleton_classes":3385,"methods_without_sig":21807,"methods_with_sig":1659,"calls_untyped":1656,"calls_typed":3567,"sigils":{"false":2,"true":112,"strict":37},"methods_with_sig_excluding_rbis":217,"methods_without_sig_excluding_rbis":649,"sigils_excluding_rbis":{"false":1,"true":78,"strict":36}} -------------------------------------------------------------------------------- /spoom_data/5d093b9.json: -------------------------------------------------------------------------------- 1 | {"timestamp":1687349145,"version_static":"0.5.10880","version_runtime":"0.5.10880","duration":735258,"commit_sha":"5d093b9","commit_timestamp":1687348946,"files":151,"rbi_files":38,"modules":475,"classes":5847,"singleton_classes":3385,"methods_without_sig":21912,"methods_with_sig":1560,"calls_untyped":1175,"calls_typed":2214,"sigils":{"false":65,"true":72,"strict":14},"methods_with_sig_excluding_rbis":118,"methods_without_sig_excluding_rbis":754,"sigils_excluding_rbis":{"false":64,"true":38,"strict":13}} -------------------------------------------------------------------------------- /spoom_data/5db3b43.json: -------------------------------------------------------------------------------- 1 | {"timestamp":1687359678,"version_static":"0.5.10884","version_runtime":"0.5.10884","duration":764678,"commit_sha":"5db3b43","commit_timestamp":1687357731,"files":151,"rbi_files":38,"modules":475,"classes":5847,"singleton_classes":3385,"methods_without_sig":21892,"methods_with_sig":1579,"calls_untyped":1843,"calls_typed":3044,"sigils":{"false":2,"true":129,"strict":20},"methods_with_sig_excluding_rbis":137,"methods_without_sig_excluding_rbis":734,"sigils_excluding_rbis":{"false":1,"true":95,"strict":19}} -------------------------------------------------------------------------------- /spoom_data/5fe2a65.json: -------------------------------------------------------------------------------- 1 | {"timestamp":1687451653,"version_static":"0.5.10884","version_runtime":"0.5.10884","duration":719107,"commit_sha":"5fe2a65","commit_timestamp":1687439603,"files":151,"rbi_files":38,"modules":475,"classes":5852,"singleton_classes":3388,"methods_without_sig":21699,"methods_with_sig":1772,"calls_untyped":1220,"calls_typed":4530,"sigils":{"false":2,"true":71,"strict":78},"methods_with_sig_excluding_rbis":330,"methods_without_sig_excluding_rbis":541,"sigils_excluding_rbis":{"false":1,"true":37,"strict":77}} -------------------------------------------------------------------------------- /spoom_data/6891312.json: -------------------------------------------------------------------------------- 1 | {"timestamp":1687351617,"version_static":"0.5.10884","version_runtime":"0.5.10884","duration":816443,"commit_sha":"6891312","commit_timestamp":1687351600,"files":151,"rbi_files":38,"modules":475,"classes":5847,"singleton_classes":3385,"methods_without_sig":21912,"methods_with_sig":1560,"calls_untyped":1608,"calls_typed":2600,"sigils":{"false":25,"true":112,"strict":14},"methods_with_sig_excluding_rbis":118,"methods_without_sig_excluding_rbis":754,"sigils_excluding_rbis":{"false":24,"true":78,"strict":13}} -------------------------------------------------------------------------------- /spoom_data/6b0fef4.json: -------------------------------------------------------------------------------- 1 | {"timestamp":1687352974,"version_static":"0.5.10884","version_runtime":"0.5.10884","duration":722787,"commit_sha":"6b0fef4","commit_timestamp":1687351658,"files":151,"rbi_files":38,"modules":475,"classes":5847,"singleton_classes":3385,"methods_without_sig":21911,"methods_with_sig":1560,"calls_untyped":1871,"calls_typed":2918,"sigils":{"false":2,"true":135,"strict":14},"methods_with_sig_excluding_rbis":118,"methods_without_sig_excluding_rbis":753,"sigils_excluding_rbis":{"false":1,"true":101,"strict":13}} -------------------------------------------------------------------------------- /spoom_data/6b83d12.json: -------------------------------------------------------------------------------- 1 | {"timestamp":1687397494,"version_static":"0.5.10884","version_runtime":"0.5.10884","duration":328052,"commit_sha":"6b83d12","commit_timestamp":1687395190,"files":151,"rbi_files":38,"modules":475,"classes":5849,"singleton_classes":3386,"methods_without_sig":21729,"methods_with_sig":1738,"calls_untyped":1319,"calls_typed":4227,"sigils":{"false":2,"true":84,"strict":65},"methods_with_sig_excluding_rbis":296,"methods_without_sig_excluding_rbis":571,"sigils_excluding_rbis":{"false":1,"true":50,"strict":64}} -------------------------------------------------------------------------------- /spoom_data/74c83c2.json: -------------------------------------------------------------------------------- 1 | {"timestamp":1687351469,"version_static":"0.5.10884","version_runtime":"0.5.10884","duration":1106142,"commit_sha":"74c83c2","commit_timestamp":1687351457,"files":151,"rbi_files":38,"modules":475,"classes":5847,"singleton_classes":3385,"methods_without_sig":21912,"methods_with_sig":1560,"calls_untyped":1277,"calls_typed":2305,"sigils":{"false":53,"true":84,"strict":14},"methods_with_sig_excluding_rbis":118,"methods_without_sig_excluding_rbis":754,"sigils_excluding_rbis":{"false":52,"true":50,"strict":13}} -------------------------------------------------------------------------------- /spoom_data/7644ff4.json: -------------------------------------------------------------------------------- 1 | {"timestamp":1687464474,"version_static":"0.5.10884","version_runtime":"0.5.10884","duration":493280,"commit_sha":"7644ff4","commit_timestamp":1687460258,"files":151,"rbi_files":38,"modules":475,"classes":5852,"singleton_classes":3388,"methods_without_sig":21698,"methods_with_sig":1773,"calls_untyped":1132,"calls_typed":4626,"sigils":{"false":2,"true":69,"strict":80},"methods_with_sig_excluding_rbis":331,"methods_without_sig_excluding_rbis":540,"sigils_excluding_rbis":{"false":1,"true":35,"strict":79}} -------------------------------------------------------------------------------- /spoom_data/79054db.json: -------------------------------------------------------------------------------- 1 | {"timestamp":1687369207,"version_static":"0.5.10884","version_runtime":"0.5.10884","duration":868372,"commit_sha":"79054db","commit_timestamp":1687368803,"files":151,"rbi_files":38,"modules":475,"classes":5847,"singleton_classes":3385,"methods_without_sig":21801,"methods_with_sig":1665,"calls_untyped":1637,"calls_typed":3618,"sigils":{"false":2,"true":109,"strict":40},"methods_with_sig_excluding_rbis":223,"methods_without_sig_excluding_rbis":643,"sigils_excluding_rbis":{"false":1,"true":75,"strict":39}} -------------------------------------------------------------------------------- /spoom_data/814e515.json: -------------------------------------------------------------------------------- 1 | {"timestamp":1687357653,"version_static":"0.5.10884","version_runtime":"0.5.10884","duration":857199,"commit_sha":"814e515","commit_timestamp":1687355717,"files":151,"rbi_files":38,"modules":475,"classes":5847,"singleton_classes":3385,"methods_without_sig":21896,"methods_with_sig":1575,"calls_untyped":1858,"calls_typed":2990,"sigils":{"false":2,"true":130,"strict":19},"methods_with_sig_excluding_rbis":133,"methods_without_sig_excluding_rbis":738,"sigils_excluding_rbis":{"false":1,"true":96,"strict":18}} -------------------------------------------------------------------------------- /spoom_data/88c3b60.json: -------------------------------------------------------------------------------- 1 | {"timestamp":1687570426,"version_static":"0.5.10884","version_runtime":"0.5.10884","duration":673846,"commit_sha":"88c3b60","commit_timestamp":1687569798,"files":152,"rbi_files":38,"modules":479,"classes":5856,"singleton_classes":3392,"methods_without_sig":21461,"methods_with_sig":2022,"calls_untyped":1078,"calls_typed":5706,"sigils":{"false":1,"true":39,"strict":112},"methods_with_sig_excluding_rbis":580,"methods_without_sig_excluding_rbis":303,"sigils_excluding_rbis":{"true":5,"strict":111}} -------------------------------------------------------------------------------- /spoom_data/8bd4b6e.json: -------------------------------------------------------------------------------- 1 | {"timestamp":1687400339,"version_static":"0.5.10884","version_runtime":"0.5.10884","duration":333951,"commit_sha":"8bd4b6e","commit_timestamp":1687398430,"files":151,"rbi_files":38,"modules":475,"classes":5850,"singleton_classes":3387,"methods_without_sig":21713,"methods_with_sig":1755,"calls_untyped":1268,"calls_typed":4368,"sigils":{"false":2,"true":79,"strict":70},"methods_with_sig_excluding_rbis":313,"methods_without_sig_excluding_rbis":555,"sigils_excluding_rbis":{"false":1,"true":45,"strict":69}} -------------------------------------------------------------------------------- /spoom_data/93f8153.json: -------------------------------------------------------------------------------- 1 | {"timestamp":1687350992,"version_static":"0.5.10837","version_runtime":"0.5.10837","duration":1287636,"commit_sha":"93f8153","commit_timestamp":1684959835,"files":141,"rbi_files":38,"modules":507,"classes":6274,"singleton_classes":3640,"methods_without_sig":24048,"methods_with_sig":1451,"calls_untyped":2198,"calls_typed":1884,"sigils":{"false":1,"true":139,"strict":1},"methods_with_sig_excluding_rbis":9,"methods_without_sig_excluding_rbis":807,"sigils_excluding_rbis":{"true":103}} -------------------------------------------------------------------------------- /spoom_data/95242fe.json: -------------------------------------------------------------------------------- 1 | {"timestamp":1687370956,"version_static":"0.5.10884","version_runtime":"0.5.10884","duration":923251,"commit_sha":"95242fe","commit_timestamp":1687369230,"files":151,"rbi_files":38,"modules":475,"classes":5847,"singleton_classes":3385,"methods_without_sig":21793,"methods_with_sig":1673,"calls_untyped":1586,"calls_typed":3689,"sigils":{"false":2,"true":106,"strict":43},"methods_with_sig_excluding_rbis":231,"methods_without_sig_excluding_rbis":635,"sigils_excluding_rbis":{"false":1,"true":72,"strict":42}} -------------------------------------------------------------------------------- /spoom_data/97f4c09.json: -------------------------------------------------------------------------------- 1 | {"timestamp":1687351516,"version_static":"0.5.10884","version_runtime":"0.5.10884","duration":728450,"commit_sha":"97f4c09","commit_timestamp":1687351457,"files":151,"rbi_files":38,"modules":475,"classes":5847,"singleton_classes":3385,"methods_without_sig":21912,"methods_with_sig":1560,"calls_untyped":1300,"calls_typed":2375,"sigils":{"false":40,"true":97,"strict":14},"methods_with_sig_excluding_rbis":118,"methods_without_sig_excluding_rbis":754,"sigils_excluding_rbis":{"false":39,"true":63,"strict":13}} -------------------------------------------------------------------------------- /spoom_data/a13d150.json: -------------------------------------------------------------------------------- 1 | {"timestamp":1687383676,"version_static":"0.5.10884","version_runtime":"0.5.10884","duration":325174,"commit_sha":"a13d150","commit_timestamp":1687382464,"files":151,"rbi_files":38,"modules":475,"classes":5849,"singleton_classes":3386,"methods_without_sig":21749,"methods_with_sig":1718,"calls_untyped":1411,"calls_typed":4053,"sigils":{"false":2,"true":94,"strict":55},"methods_with_sig_excluding_rbis":276,"methods_without_sig_excluding_rbis":591,"sigils_excluding_rbis":{"false":1,"true":60,"strict":54}} -------------------------------------------------------------------------------- /spoom_data/a17f215.json: -------------------------------------------------------------------------------- 1 | {"timestamp":1687483833,"version_static":"0.5.10884","version_runtime":"0.5.10884","duration":811150,"commit_sha":"a17f215","commit_timestamp":1687473251,"files":151,"rbi_files":38,"modules":476,"classes":5853,"singleton_classes":3389,"methods_without_sig":21692,"methods_with_sig":1782,"calls_untyped":1146,"calls_typed":4676,"sigils":{"false":2,"true":68,"strict":81},"methods_with_sig_excluding_rbis":340,"methods_without_sig_excluding_rbis":534,"sigils_excluding_rbis":{"false":1,"true":34,"strict":80}} -------------------------------------------------------------------------------- /spoom_data/b705a9d.json: -------------------------------------------------------------------------------- 1 | {"timestamp":1687398424,"version_static":"0.5.10884","version_runtime":"0.5.10884","duration":328786,"commit_sha":"b705a9d","commit_timestamp":1687397513,"files":151,"rbi_files":38,"modules":475,"classes":5849,"singleton_classes":3386,"methods_without_sig":21719,"methods_with_sig":1748,"calls_untyped":1287,"calls_typed":4310,"sigils":{"false":2,"true":81,"strict":68},"methods_with_sig_excluding_rbis":306,"methods_without_sig_excluding_rbis":561,"sigils_excluding_rbis":{"false":1,"true":47,"strict":67}} -------------------------------------------------------------------------------- /spoom_data/ba19195.json: -------------------------------------------------------------------------------- 1 | {"timestamp":1687473241,"version_static":"0.5.10884","version_runtime":"0.5.10884","duration":817055,"commit_sha":"ba19195","commit_timestamp":1687465345,"files":151,"rbi_files":38,"modules":475,"classes":5852,"singleton_classes":3388,"methods_without_sig":21698,"methods_with_sig":1773,"calls_untyped":1143,"calls_typed":4620,"sigils":{"false":2,"true":69,"strict":80},"methods_with_sig_excluding_rbis":331,"methods_without_sig_excluding_rbis":540,"sigils_excluding_rbis":{"false":1,"true":35,"strict":79}} -------------------------------------------------------------------------------- /spoom_data/bef51ca.json: -------------------------------------------------------------------------------- 1 | {"timestamp":1687970236,"version_static":"0.5.10885","version_runtime":"0.5.10885","duration":359270,"commit_sha":"bef51ca","commit_timestamp":1687969999,"files":157,"rbi_files":40,"modules":479,"classes":5886,"singleton_classes":3422,"methods_without_sig":20858,"methods_with_sig":2672,"calls_untyped":1087,"calls_typed":5722,"sigils":{"false":2,"true":41,"strict":114},"methods_with_sig_excluding_rbis":905,"methods_without_sig_excluding_rbis":24,"sigils_excluding_rbis":{"false":1,"true":6,"strict":113}} -------------------------------------------------------------------------------- /spoom_data/dbb595a.json: -------------------------------------------------------------------------------- 1 | {"timestamp":1687366577,"version_static":"0.5.10884","version_runtime":"0.5.10884","duration":960253,"commit_sha":"dbb595a","commit_timestamp":1687366226,"files":151,"rbi_files":38,"modules":475,"classes":5847,"singleton_classes":3385,"methods_without_sig":21849,"methods_with_sig":1616,"calls_untyped":1712,"calls_typed":3353,"sigils":{"false":2,"true":126,"strict":23},"methods_with_sig_excluding_rbis":174,"methods_without_sig_excluding_rbis":691,"sigils_excluding_rbis":{"false":1,"true":92,"strict":22}} -------------------------------------------------------------------------------- /spoom_data/e8fab92.json: -------------------------------------------------------------------------------- 1 | {"timestamp":1687393344,"version_static":"0.5.10884","version_runtime":"0.5.10884","duration":334583,"commit_sha":"e8fab92","commit_timestamp":1687383706,"files":151,"rbi_files":38,"modules":475,"classes":5849,"singleton_classes":3386,"methods_without_sig":21747,"methods_with_sig":1720,"calls_untyped":1408,"calls_typed":4069,"sigils":{"false":2,"true":90,"strict":59},"methods_with_sig_excluding_rbis":278,"methods_without_sig_excluding_rbis":589,"sigils_excluding_rbis":{"false":1,"true":56,"strict":58}} -------------------------------------------------------------------------------- /spoom_data/f166c87.json: -------------------------------------------------------------------------------- 1 | {"timestamp":1687439502,"version_static":"0.5.10884","version_runtime":"0.5.10884","duration":729730,"commit_sha":"f166c87","commit_timestamp":1687400347,"files":151,"rbi_files":38,"modules":475,"classes":5852,"singleton_classes":3388,"methods_without_sig":21711,"methods_with_sig":1760,"calls_untyped":1275,"calls_typed":4416,"sigils":{"false":2,"true":77,"strict":72},"methods_with_sig_excluding_rbis":318,"methods_without_sig_excluding_rbis":553,"sigils_excluding_rbis":{"false":1,"true":43,"strict":71}} -------------------------------------------------------------------------------- /spoom_data/f57992d.json: -------------------------------------------------------------------------------- 1 | {"timestamp":1687569700,"version_static":"0.5.10884","version_runtime":"0.5.10884","duration":780592,"commit_sha":"f57992d","commit_timestamp":1687568962,"files":152,"rbi_files":38,"modules":479,"classes":5856,"singleton_classes":3392,"methods_without_sig":21479,"methods_with_sig":2004,"calls_untyped":1112,"calls_typed":5624,"sigils":{"false":1,"true":42,"strict":109},"methods_with_sig_excluding_rbis":562,"methods_without_sig_excluding_rbis":321,"sigils_excluding_rbis":{"true":8,"strict":108}} -------------------------------------------------------------------------------- /spoom_data/f5a1e40.json: -------------------------------------------------------------------------------- 1 | {"timestamp":1687351657,"version_static":"0.5.10884","version_runtime":"0.5.10884","duration":744268,"commit_sha":"f5a1e40","commit_timestamp":1687351639,"files":151,"rbi_files":38,"modules":475,"classes":5847,"singleton_classes":3385,"methods_without_sig":21912,"methods_with_sig":1560,"calls_untyped":1803,"calls_typed":2789,"sigils":{"false":9,"true":128,"strict":14},"methods_with_sig_excluding_rbis":118,"methods_without_sig_excluding_rbis":754,"sigils_excluding_rbis":{"false":8,"true":94,"strict":13}} -------------------------------------------------------------------------------- /spoom_data/f6d6431.json: -------------------------------------------------------------------------------- 1 | {"timestamp":1687378433,"version_static":"0.5.10884","version_runtime":"0.5.10884","duration":689648,"commit_sha":"f6d6431","commit_timestamp":1687370982,"files":151,"rbi_files":38,"modules":475,"classes":5847,"singleton_classes":3385,"methods_without_sig":21777,"methods_with_sig":1689,"calls_untyped":1478,"calls_typed":3868,"sigils":{"false":2,"true":101,"strict":48},"methods_with_sig_excluding_rbis":247,"methods_without_sig_excluding_rbis":619,"sigils_excluding_rbis":{"false":1,"true":67,"strict":47}} -------------------------------------------------------------------------------- /spoom_data/f72b67c.json: -------------------------------------------------------------------------------- 1 | {"timestamp":1687351560,"version_static":"0.5.10884","version_runtime":"0.5.10884","duration":754455,"commit_sha":"f72b67c","commit_timestamp":1687351538,"files":151,"rbi_files":38,"modules":475,"classes":5847,"singleton_classes":3385,"methods_without_sig":21912,"methods_with_sig":1560,"calls_untyped":1438,"calls_typed":2456,"sigils":{"false":33,"true":104,"strict":14},"methods_with_sig_excluding_rbis":118,"methods_without_sig_excluding_rbis":754,"sigils_excluding_rbis":{"false":32,"true":70,"strict":13}} -------------------------------------------------------------------------------- /spoom_data/fc2f231.json: -------------------------------------------------------------------------------- 1 | {"timestamp":1687379796,"version_static":"0.5.10884","version_runtime":"0.5.10884","duration":872975,"commit_sha":"fc2f231","commit_timestamp":1687378443,"files":151,"rbi_files":38,"modules":475,"classes":5849,"singleton_classes":3386,"methods_without_sig":21775,"methods_with_sig":1693,"calls_untyped":1464,"calls_typed":3911,"sigils":{"false":2,"true":100,"strict":49},"methods_with_sig_excluding_rbis":251,"methods_without_sig_excluding_rbis":617,"sigils_excluding_rbis":{"false":1,"true":66,"strict":48}} -------------------------------------------------------------------------------- /src/mocktail/collects_calls.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | module Mocktail 4 | class CollectsCalls 5 | extend T::Sig 6 | 7 | sig { params(double: Object, method_name: T.nilable(Symbol)).returns(T::Array[Call]) } 8 | def collect(double, method_name) 9 | calls = ExplainsThing.new.explain(double).reference.calls 10 | 11 | if method_name.nil? 12 | calls 13 | else 14 | calls.select { |call| call.method.to_s == method_name.to_s } 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /src/mocktail/errors.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | module Mocktail 4 | class Error < StandardError; end 5 | 6 | class UnexpectedError < Error; end 7 | 8 | class UnsupportedMocktail < Error; end 9 | 10 | class MissingDemonstrationError < Error; end 11 | 12 | class AmbiguousDemonstrationError < Error; end 13 | 14 | class InvalidMatcherError < Error; end 15 | 16 | class VerificationError < Error; end 17 | 18 | class TypeCheckingError < Error; end 19 | end 20 | -------------------------------------------------------------------------------- /src/mocktail/explains_nils.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | require_relative "share/stringifies_method_name" 4 | require_relative "share/stringifies_call" 5 | 6 | module Mocktail 7 | class ExplainsNils 8 | extend T::Sig 9 | 10 | sig { void } 11 | def initialize 12 | @stringifies_method_name = T.let(StringifiesMethodName.new, StringifiesMethodName) 13 | @stringifies_call = T.let(StringifiesCall.new, StringifiesCall) 14 | end 15 | 16 | sig { returns(T::Array[UnsatisfyingCallExplanation]) } 17 | def explain 18 | Mocktail.cabinet.unsatisfying_calls.map { |unsatisfying_call| 19 | dry_call = unsatisfying_call.call 20 | other_stubbings = unsatisfying_call.other_stubbings 21 | 22 | UnsatisfyingCallExplanation.new(unsatisfying_call, <<~MSG) 23 | `nil' was returned by a mocked `#{@stringifies_method_name.stringify(dry_call)}' method 24 | because none of its configured stubbings were satisfied. 25 | 26 | The actual call: 27 | 28 | #{@stringifies_call.stringify(dry_call, always_parens: true)} 29 | 30 | The call site: 31 | 32 | #{unsatisfying_call.backtrace.first} 33 | 34 | #{@stringifies_call.stringify_multiple(other_stubbings.map(&:recording), 35 | nonzero_message: "Stubbings configured prior to this call but not satisfied by it", 36 | zero_message: "No stubbings were configured on this method")} 37 | MSG 38 | } 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /src/mocktail/grabs_original_method_parameters.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | module Mocktail 4 | class GrabsOriginalMethodParameters 5 | extend T::Sig 6 | 7 | # Sorbet wraps the original method in a sig wrapper, so we need to unwrap it. 8 | # The value returned from `owner.instance_method(method_name)` does not have 9 | # the real parameters values available, as they'll have been erased 10 | # 11 | # If the method isn't wrapped by Sorbet, this will return the #instance_method, 12 | # per usual 13 | sig { params(method: T.nilable(T.any(UnboundMethod, Method))).returns(T::Array[T::Array[Symbol]]) } 14 | def grab(method) 15 | return [] unless method 16 | 17 | if (wrapped_method = sorbet_wrapped_method(method)) 18 | wrapped_method.parameters 19 | else 20 | method.parameters 21 | end 22 | end 23 | 24 | private 25 | 26 | sig { params(method: T.any(UnboundMethod, Method)).returns(T.nilable(T::Private::Methods::Signature)) } 27 | def sorbet_wrapped_method(method) 28 | return unless defined?(::T::Private::Methods) 29 | 30 | T::Private::Methods.signature_for_method(method) 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /src/mocktail/handles_dry_call.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | require_relative "handles_dry_call/fulfills_stubbing" 4 | require_relative "handles_dry_call/logs_call" 5 | require_relative "handles_dry_call/validates_arguments" 6 | 7 | module Mocktail 8 | class HandlesDryCall 9 | extend T::Sig 10 | 11 | sig { void } 12 | def initialize 13 | @validates_arguments = T.let(ValidatesArguments.new, ValidatesArguments) 14 | @logs_call = T.let(LogsCall.new, LogsCall) 15 | @fulfills_stubbing = T.let(FulfillsStubbing.new, FulfillsStubbing) 16 | end 17 | 18 | sig { params(dry_call: Call).returns(T.anything) } 19 | def handle(dry_call) 20 | @validates_arguments.validate(dry_call) 21 | @logs_call.log(dry_call) 22 | @fulfills_stubbing.fulfill(dry_call) 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /src/mocktail/handles_dry_call/fulfills_stubbing.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | require_relative "fulfills_stubbing/finds_satisfaction" 4 | require_relative "fulfills_stubbing/describes_unsatisfied_stubbing" 5 | 6 | module Mocktail 7 | class FulfillsStubbing 8 | extend T::Sig 9 | 10 | sig { void } 11 | def initialize 12 | @finds_satisfaction = T.let(FindsSatisfaction.new, Mocktail::FindsSatisfaction) 13 | @describes_unsatisfied_stubbing = T.let(DescribesUnsatisfiedStubbing.new, Mocktail::DescribesUnsatisfiedStubbing) 14 | end 15 | 16 | sig { params(dry_call: Call).returns(T.anything) } 17 | def fulfill(dry_call) 18 | if (stubbing = satisfaction(dry_call)) 19 | stubbing.satisfied! 20 | stubbing.effect&.call(dry_call) 21 | else 22 | store_unsatisfying_call!(dry_call) 23 | nil 24 | end 25 | end 26 | 27 | sig { params(dry_call: Call).returns(T.nilable(Stubbing[T.anything])) } 28 | def satisfaction(dry_call) 29 | return if Mocktail.cabinet.demonstration_in_progress? 30 | 31 | @finds_satisfaction.find(dry_call) 32 | end 33 | 34 | private 35 | 36 | sig { params(dry_call: Call).void } 37 | def store_unsatisfying_call!(dry_call) 38 | return if Mocktail.cabinet.demonstration_in_progress? 39 | 40 | Mocktail.cabinet.store_unsatisfying_call( 41 | @describes_unsatisfied_stubbing.describe(dry_call) 42 | ) 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /src/mocktail/handles_dry_call/fulfills_stubbing/describes_unsatisfied_stubbing.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | require_relative "../../share/cleans_backtrace" 4 | require_relative "../../share/bind" 5 | 6 | module Mocktail 7 | class DescribesUnsatisfiedStubbing 8 | extend T::Sig 9 | 10 | sig { void } 11 | def initialize 12 | @cleans_backtrace = T.let(CleansBacktrace.new, Mocktail::CleansBacktrace) 13 | end 14 | 15 | sig { params(dry_call: Mocktail::Call).returns(Mocktail::UnsatisfyingCall) } 16 | def describe(dry_call) 17 | UnsatisfyingCall.new( 18 | call: dry_call, 19 | other_stubbings: Mocktail.cabinet.stubbings.select { |stubbing| 20 | Bind.call(dry_call.double, :==, stubbing.recording.double) && 21 | dry_call.method == stubbing.recording.method 22 | }, 23 | backtrace: @cleans_backtrace.clean(Error.new).backtrace || [] 24 | ) 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /src/mocktail/handles_dry_call/fulfills_stubbing/finds_satisfaction.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | require_relative "../../share/determines_matching_calls" 4 | 5 | module Mocktail 6 | class FindsSatisfaction 7 | extend T::Sig 8 | 9 | sig { void } 10 | def initialize 11 | @determines_matching_calls = T.let(DeterminesMatchingCalls.new, Mocktail::DeterminesMatchingCalls) 12 | end 13 | 14 | sig { params(dry_call: Call).returns(T.nilable(Stubbing[T.anything])) } 15 | def find(dry_call) 16 | Mocktail.cabinet.stubbings.reverse.find { |stubbing| 17 | demo_config_times = stubbing.demo_config.times 18 | 19 | @determines_matching_calls.determine(dry_call, stubbing.recording, stubbing.demo_config) && 20 | (demo_config_times.nil? || demo_config_times > stubbing.satisfaction_count) 21 | } 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /src/mocktail/handles_dry_call/logs_call.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | module Mocktail 4 | class LogsCall 5 | extend T::Sig 6 | 7 | sig { params(dry_call: Call).void } 8 | def log(dry_call) 9 | Mocktail.cabinet.store_call(dry_call) 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /src/mocktail/handles_dry_call/validates_arguments.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | module Mocktail 4 | class ValidatesArguments 5 | extend T::Sig 6 | sig { void } 7 | def self.disable! 8 | Thread.current[:mocktail_arity_validation_disabled] = true 9 | end 10 | 11 | sig { void } 12 | def self.enable! 13 | Thread.current[:mocktail_arity_validation_disabled] = false 14 | end 15 | 16 | sig { returns(T::Boolean) } 17 | def self.disabled? 18 | !!Thread.current[:mocktail_arity_validation_disabled] 19 | end 20 | 21 | sig { params(disable: T.nilable(T::Boolean), blk: T.proc.returns(T.anything)).void } 22 | def self.optional(disable, &blk) 23 | return blk.call unless disable 24 | 25 | disable! 26 | ret = blk.call 27 | enable! 28 | ret 29 | end 30 | 31 | sig { void } 32 | def initialize 33 | @simulates_argument_error = T.let(SimulatesArgumentError.new, Mocktail::SimulatesArgumentError) 34 | end 35 | 36 | sig { params(dry_call: Call).returns(NilClass) } 37 | def validate(dry_call) 38 | return if self.class.disabled? 39 | 40 | if (error = @simulates_argument_error.simulate(dry_call)) 41 | raise error 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /src/mocktail/handles_dry_new_call.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | module Mocktail 4 | class HandlesDryNewCall 5 | extend T::Sig 6 | 7 | sig { void } 8 | def initialize 9 | @validates_arguments = T.let(ValidatesArguments.new, ValidatesArguments) 10 | @logs_call = T.let(LogsCall.new, LogsCall) 11 | @fulfills_stubbing = T.let(FulfillsStubbing.new, FulfillsStubbing) 12 | @imitates_type = T.let(ImitatesType.new, ImitatesType) 13 | end 14 | 15 | sig { params(type: T::Class[T.all(T, Object)], args: T::Array[T.anything], kwargs: T::Hash[Symbol, T.anything], block: T.nilable(Proc)).returns(T.anything) } 16 | def handle(type, args, kwargs, block) 17 | @validates_arguments.validate(Call.new( 18 | original_method: type.instance_method(:initialize), 19 | args: args, 20 | kwargs: kwargs, 21 | block: block 22 | )) 23 | 24 | new_call = Call.new( 25 | singleton: true, 26 | double: type, 27 | original_type: type, 28 | dry_type: type, 29 | method: :new, 30 | args: args, 31 | kwargs: kwargs, 32 | block: block 33 | ) 34 | @logs_call.log(new_call) 35 | if @fulfills_stubbing.satisfaction(new_call) 36 | @fulfills_stubbing.fulfill(new_call) 37 | else 38 | @imitates_type.imitate(type) 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /src/mocktail/imitates_type.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | require_relative "imitates_type/ensures_imitation_support" 4 | require_relative "imitates_type/makes_double" 5 | 6 | module Mocktail 7 | class ImitatesType 8 | extend T::Sig 9 | extend T::Generic 10 | 11 | sig { void } 12 | def initialize 13 | @ensures_imitation_support = T.let(EnsuresImitationSupport.new, EnsuresImitationSupport) 14 | @makes_double = T.let(MakesDouble.new, MakesDouble) 15 | end 16 | 17 | sig { 18 | type_parameters(:T) 19 | .params(type: T::Class[T.all(T.type_parameter(:T), Object)]) 20 | .returns(T.all(T.type_parameter(:T), Object)) 21 | } 22 | def imitate(type) 23 | @ensures_imitation_support.ensure(type) 24 | T.cast(@makes_double.make(type).tap do |double| 25 | Mocktail.cabinet.store_double(double) 26 | end.dry_instance, T.all(T.type_parameter(:T), Object)) 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /src/mocktail/imitates_type/ensures_imitation_support.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | module Mocktail 4 | class EnsuresImitationSupport 5 | extend T::Sig 6 | 7 | sig { params(type: T.any(T::Class[T.anything], Module)).void } 8 | def ensure(type) 9 | unless type.is_a?(Class) || type.is_a?(Module) 10 | raise UnsupportedMocktail.new <<~MSG.tr("\n", " ") 11 | Mocktail.of() can only mix mocktail instances of modules and classes. 12 | MSG 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /src/mocktail/imitates_type/makes_double.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | require_relative "makes_double/declares_dry_class" 4 | require_relative "makes_double/gathers_fakeable_instance_methods" 5 | 6 | module Mocktail 7 | class MakesDouble 8 | extend T::Sig 9 | 10 | sig { void } 11 | def initialize 12 | @declares_dry_class = T.let(DeclaresDryClass.new, DeclaresDryClass) 13 | @gathers_fakeable_instance_methods = T.let(GathersFakeableInstanceMethods.new, GathersFakeableInstanceMethods) 14 | end 15 | 16 | sig { params(type: T::Class[Object]).returns(Double) } 17 | def make(type) 18 | dry_methods = @gathers_fakeable_instance_methods.gather(type) 19 | dry_type = @declares_dry_class.declare(type, dry_methods) 20 | 21 | Double.new( 22 | original_type: type, 23 | dry_type: dry_type, 24 | dry_instance: dry_type.new, 25 | dry_methods: dry_methods 26 | ) 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /src/mocktail/imitates_type/makes_double/gathers_fakeable_instance_methods.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | module Mocktail 4 | class GathersFakeableInstanceMethods 5 | extend T::Sig 6 | 7 | sig { params(type: T.any(T::Class[T.anything], Module)).returns(T::Array[Symbol]) } 8 | def gather(type) 9 | methods = type.instance_methods + [ 10 | (:respond_to_missing? if type.private_method_defined?(:respond_to_missing?)) 11 | ].compact 12 | 13 | methods.reject { |m| 14 | ignore?(type, m) 15 | } 16 | end 17 | 18 | sig { params(type: T.any(T::Class[T.anything], Module), method_name: Symbol).returns(T::Boolean) } 19 | def ignore?(type, method_name) 20 | ignored_ancestors.include?(type.instance_method(method_name).owner) 21 | end 22 | 23 | sig { returns(T::Array[Module]) } 24 | def ignored_ancestors 25 | Object.ancestors 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /src/mocktail/initialize_based_on_type_system_mode_switching.rb: -------------------------------------------------------------------------------- 1 | # typed: false 2 | 3 | require_relative "typed" 4 | 5 | # Constant boolean, so won't statically type-check, but `T.unsafe` can't be used 6 | # because we haven't required sorbet-runtime yet 7 | if eval("Mocktail::TYPED", binding, __FILE__, __LINE__) 8 | require "sorbet-runtime" 9 | else 10 | require "#{Gem.loaded_specs["sorbet-eraser"].gem_dir}/lib/t" 11 | end 12 | -------------------------------------------------------------------------------- /src/mocktail/initializes_mocktail.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | module Mocktail 4 | class InitializesMocktail 5 | extend T::Sig 6 | 7 | sig { void } 8 | def init 9 | [ 10 | Mocktail::Matchers::Any, 11 | Mocktail::Matchers::Includes, 12 | Mocktail::Matchers::IncludesString, 13 | Mocktail::Matchers::IncludesKey, 14 | Mocktail::Matchers::IncludesHash, 15 | Mocktail::Matchers::IsA, 16 | Mocktail::Matchers::Matches, 17 | Mocktail::Matchers::Not, 18 | Mocktail::Matchers::Numeric, 19 | Mocktail::Matchers::That 20 | ].each do |matcher_type| 21 | Mocktail.register_matcher(matcher_type) 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /src/mocktail/matcher_presentation.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | module Mocktail 4 | class MatcherPresentation 5 | extend T::Sig 6 | 7 | sig { params(name: Symbol, include_private: T::Boolean).returns(T::Boolean) } 8 | def respond_to_missing?(name, include_private = false) 9 | !!MatcherRegistry.instance.get(name) || super 10 | end 11 | 12 | sig { params(name: Symbol, args: T.anything, kwargs: T.anything, blk: T.nilable(Proc)).returns(T.anything) } 13 | def method_missing(name, *args, **kwargs, &blk) # standard:disable Style/ArgumentsForwarding 14 | if (matcher = MatcherRegistry.instance.get(name)) 15 | T.unsafe(matcher).new(*args, **kwargs, &blk) # standard:disable Style/ArgumentsForwarding 16 | else 17 | super 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /src/mocktail/matchers.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | module Mocktail 4 | module Matchers 5 | end 6 | end 7 | 8 | require_relative "matchers/base" 9 | require_relative "matchers/any" 10 | require_relative "matchers/captor" 11 | require_relative "matchers/includes" 12 | require_relative "matchers/includes_string" 13 | require_relative "matchers/includes_hash" 14 | require_relative "matchers/includes_key" 15 | require_relative "matchers/is_a" 16 | require_relative "matchers/matches" 17 | require_relative "matchers/not" 18 | require_relative "matchers/numeric" 19 | require_relative "matchers/that" 20 | -------------------------------------------------------------------------------- /src/mocktail/matchers/any.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | module Mocktail::Matchers 4 | class Any < Base 5 | extend T::Sig 6 | 7 | sig { returns(Symbol) } 8 | def self.matcher_name 9 | :any 10 | end 11 | 12 | sig { void } 13 | def initialize 14 | # Empty initialize is necessary b/c Base default expects an argument 15 | end 16 | 17 | sig { params(actual: T.anything).returns(T::Boolean) } 18 | def match?(actual) 19 | true 20 | end 21 | 22 | sig { returns(String) } 23 | def inspect 24 | "any" 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /src/mocktail/matchers/base.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | module Mocktail::Matchers 4 | class Base 5 | extend T::Sig 6 | extend T::Helpers 7 | 8 | if T.unsafe(Mocktail::TYPED) && T::Private::RuntimeLevels.default_checked_level != :never 9 | abstract! 10 | end 11 | 12 | # Custom matchers can receive any args, kwargs, or block they want. Usually 13 | # single-argument, though, so that's defaulted here and in #insepct 14 | sig { params(expected: BasicObject).void } 15 | def initialize(expected) 16 | @expected = expected 17 | end 18 | 19 | sig { returns(Symbol) } 20 | def self.matcher_name 21 | raise Mocktail::InvalidMatcherError.new("The `matcher_name` class method must return a valid method name") 22 | end 23 | 24 | sig { params(actual: T.untyped).returns(T::Boolean) } 25 | def match?(actual) 26 | raise Mocktail::InvalidMatcherError.new("Matchers must implement `match?(argument)`") 27 | end 28 | 29 | sig { returns(String) } 30 | def inspect 31 | "#{self.class.matcher_name}(#{T.cast(@expected, Object).inspect})" 32 | end 33 | 34 | sig { returns(TrueClass) } 35 | def is_mocktail_matcher? 36 | true 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /src/mocktail/matchers/includes.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | module Mocktail::Matchers 4 | class Includes < Base 5 | extend T::Sig 6 | 7 | sig { returns(Symbol) } 8 | def self.matcher_name 9 | :includes 10 | end 11 | 12 | sig { params(expecteds: T.untyped).void } 13 | def initialize(*expecteds) 14 | @expecteds = T.let(expecteds, T::Array[T.untyped]) 15 | end 16 | 17 | sig { params(actual: T.untyped).returns(T::Boolean) } 18 | def match?(actual) 19 | @expecteds.all? { |expected| 20 | (actual.respond_to?(:include?) && actual.include?(expected)) || 21 | (actual.is_a?(Hash) && expected.is_a?(Hash) && expected.all? { |k, v| actual[k] == v }) 22 | } 23 | rescue 24 | false 25 | end 26 | 27 | sig { returns(String) } 28 | def inspect 29 | "#{self.class.matcher_name}(#{@expecteds.map(&:inspect).join(", ")})" 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /src/mocktail/matchers/includes_hash.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | module Mocktail::Matchers 4 | class IncludesHash < Includes 5 | extend T::Sig 6 | 7 | sig { returns(Symbol) } 8 | def self.matcher_name 9 | :includes_hash 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /src/mocktail/matchers/includes_key.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | module Mocktail::Matchers 4 | class IncludesKey < Includes 5 | extend T::Sig 6 | 7 | sig { returns(Symbol) } 8 | def self.matcher_name 9 | :includes_key 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /src/mocktail/matchers/includes_string.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | module Mocktail::Matchers 4 | class IncludesString < Includes 5 | extend T::Sig 6 | 7 | sig { returns(Symbol) } 8 | def self.matcher_name 9 | :includes_string 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /src/mocktail/matchers/is_a.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | module Mocktail::Matchers 4 | class IsA < Base 5 | extend T::Sig 6 | 7 | sig { returns(Symbol) } 8 | def self.matcher_name 9 | :is_a 10 | end 11 | 12 | sig { params(actual: T.untyped).returns(T::Boolean) } 13 | def match?(actual) 14 | actual.is_a?(@expected) 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /src/mocktail/matchers/matches.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | module Mocktail::Matchers 4 | class Matches < Base 5 | extend T::Sig 6 | 7 | sig { returns(Symbol) } 8 | def self.matcher_name 9 | :matches 10 | end 11 | 12 | sig { params(actual: T.untyped).returns(T::Boolean) } 13 | def match?(actual) 14 | actual.respond_to?(:match?) && actual.match?(@expected) 15 | rescue 16 | false 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /src/mocktail/matchers/not.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | module Mocktail::Matchers 4 | class Not < Base 5 | extend T::Sig 6 | 7 | sig { returns(Symbol) } 8 | def self.matcher_name 9 | :not 10 | end 11 | 12 | sig { params(actual: T.untyped).returns(T::Boolean) } 13 | def match?(actual) 14 | @expected != actual 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /src/mocktail/matchers/numeric.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | module Mocktail::Matchers 4 | class Numeric < Base 5 | extend T::Sig 6 | 7 | sig { returns(Symbol) } 8 | def self.matcher_name 9 | :numeric 10 | end 11 | 12 | sig { void } 13 | def initialize 14 | # Empty initialize is necessary b/c Base default expects an argument 15 | end 16 | 17 | sig { params(actual: T.untyped).returns(T::Boolean) } 18 | def match?(actual) 19 | actual.is_a?(::Numeric) 20 | end 21 | 22 | sig { returns(String) } 23 | def inspect 24 | "numeric" 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /src/mocktail/matchers/that.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | module Mocktail::Matchers 4 | class That < Base 5 | extend T::Sig 6 | 7 | sig { returns(Symbol) } 8 | def self.matcher_name 9 | :that 10 | end 11 | 12 | sig { params(blk: T.nilable(T.proc.params(actual: T.untyped).returns(T.untyped))).void } 13 | def initialize(&blk) 14 | if blk.nil? 15 | raise ArgumentError.new("The `that` matcher must be passed a block (e.g. `that { |arg| … }`)") 16 | end 17 | @blk = T.let(blk, T.proc.params(actual: T.untyped).returns(T.untyped)) 18 | end 19 | 20 | sig { params(actual: T.untyped).returns(T::Boolean) } 21 | def match?(actual) 22 | @blk.call(actual) 23 | rescue 24 | false 25 | end 26 | 27 | sig { returns(String) } 28 | def inspect 29 | "that {…}" 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /src/mocktail/registers_stubbing.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | require_relative "records_demonstration" 4 | 5 | module Mocktail 6 | class RegistersStubbing 7 | extend T::Sig 8 | 9 | sig { void } 10 | def initialize 11 | @records_demonstration = T.let(RecordsDemonstration.new, RecordsDemonstration) 12 | end 13 | 14 | sig { 15 | type_parameters(:T) 16 | .params( 17 | demonstration: T.proc.params(matchers: Mocktail::MatcherPresentation).returns(T.type_parameter(:T)), 18 | demo_config: DemoConfig 19 | ).returns(Mocktail::Stubbing[T.type_parameter(:T)]) 20 | } 21 | def register(demonstration, demo_config) 22 | Stubbing.new( 23 | demonstration: demonstration, 24 | demo_config: demo_config, 25 | recording: @records_demonstration.record(demonstration, demo_config) 26 | ).tap do |stubbing| 27 | Mocktail.cabinet.store_stubbing(stubbing) 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /src/mocktail/replaces_type.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | require_relative "replaces_type/redefines_new" 4 | require_relative "replaces_type/redefines_singleton_methods" 5 | require_relative "replaces_type/runs_sorbet_sig_blocks_before_replacement" 6 | 7 | module Mocktail 8 | class ReplacesType 9 | extend T::Sig 10 | 11 | sig { void } 12 | def initialize 13 | @top_shelf = T.let(TopShelf.instance, TopShelf) 14 | @runs_sorbet_sig_blocks_before_replacement = T.let(RunsSorbetSigBlocksBeforeReplacement.new, RunsSorbetSigBlocksBeforeReplacement) 15 | @redefines_new = T.let(RedefinesNew.new, RedefinesNew) 16 | @redefines_singleton_methods = T.let(RedefinesSingletonMethods.new, RedefinesSingletonMethods) 17 | end 18 | 19 | sig { params(type: T.any(T::Class[T.anything], Module)).void } 20 | def replace(type) 21 | unless T.unsafe(type).is_a?(Class) || T.unsafe(type).is_a?(Module) 22 | raise UnsupportedMocktail.new("Mocktail.replace() only supports classes and modules") 23 | end 24 | 25 | @runs_sorbet_sig_blocks_before_replacement.run(type) 26 | 27 | if type.is_a?(Class) 28 | @top_shelf.register_new_replacement!(type) 29 | @redefines_new.redefine(type) 30 | end 31 | 32 | @top_shelf.register_singleton_method_replacement!(type) 33 | @redefines_singleton_methods.redefine(type) 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /src/mocktail/replaces_type/redefines_new.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | module Mocktail 4 | class RedefinesNew 5 | extend T::Sig 6 | 7 | sig { void } 8 | def initialize 9 | @handles_dry_new_call = T.let(HandlesDryNewCall.new, HandlesDryNewCall) 10 | end 11 | 12 | sig { params(type: T.any(T::Class[T.anything], Module)).void } 13 | def redefine(type) 14 | type_replacement = TopShelf.instance.type_replacement_for(type) 15 | 16 | if type_replacement.replacement_new.nil? 17 | type_replacement.original_new = type.method(:new) 18 | type.singleton_class.send(:undef_method, :new) 19 | handles_dry_new_call = @handles_dry_new_call 20 | type.define_singleton_method :new, ->(*args, **kwargs, &block) { 21 | if TopShelf.instance.new_replaced?(type) || 22 | (type.is_a?(Class) && TopShelf.instance.of_next_registered?(type)) 23 | handles_dry_new_call.handle(T.cast(type, T::Class[T.all(T, Object)]), args, kwargs, block) 24 | else 25 | type_replacement.original_new.call(*args, **kwargs, &block) 26 | end 27 | } 28 | type_replacement.replacement_new = type.singleton_method(:new) 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /src/mocktail/resets_state.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | module Mocktail 4 | class ResetsState 5 | extend T::Sig 6 | 7 | sig { void } 8 | def reset 9 | TopShelf.instance.reset_current_thread! 10 | Mocktail.cabinet.reset! 11 | ValidatesArguments.enable! 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /src/mocktail/share/bind.rb: -------------------------------------------------------------------------------- 1 | # typed: true 2 | 3 | module Mocktail 4 | module Bind 5 | # sig intentionally omitted, because the wrapper will cause infinite recursion if certain methods are mocked 6 | def self.call(mock, method_name, *args, **kwargs, &blk) # standard:disable Style/ArgumentsForwarding 7 | if Mocktail.cabinet.double_for_instance(mock) 8 | T.unsafe(Object.instance_method(method_name)).bind_call(mock, *args, **kwargs, &blk) # standard:disable Style/ArgumentsForwarding 9 | elsif (mock.is_a?(Module) || mock.is_a?(Class)) && 10 | (type_replacement = TopShelf.instance.type_replacement_if_exists_for(mock)) && 11 | (og_method = type_replacement.original_methods&.find { |m| m.name == method_name }) 12 | T.unsafe(og_method).call(*args, **kwargs, &blk) # standard:disable Style/ArgumentsForwarding 13 | else 14 | T.unsafe(mock).__send__(method_name, *args, **kwargs, &blk) # standard:disable Style/ArgumentsForwarding 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /src/mocktail/share/cleans_backtrace.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | module Mocktail 4 | class CleansBacktrace 5 | extend T::Sig 6 | 7 | sig { 8 | type_parameters(:T) 9 | .params(error: T.all(T.type_parameter(:T), StandardError)) 10 | .returns(T.type_parameter(:T)) 11 | } 12 | def clean(error) 13 | raise error 14 | rescue error.class => e 15 | T.cast(e, T.all(T.type_parameter(:T), StandardError)).tap do |e| 16 | e.set_backtrace(e.backtrace.drop_while { |frame| 17 | frame.start_with?(BASE_PATH, BASE_PATH) || frame.match?(/[\\|\/]sorbet-runtime.*[\\|\/]lib[\\|\/]types[\\|\/]private/) 18 | }) 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /src/mocktail/share/creates_identifier.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | module Mocktail 4 | class CreatesIdentifier 5 | extend T::Sig 6 | 7 | KEYWORDS = T.let(%w[__FILE__ __LINE__ alias and begin BEGIN break case class def defined? do else elsif end END ensure false for if in module next nil not or redo rescue retry return self super then true undef unless until when while yield], T::Array[String]) 8 | 9 | sig { params(s: T.anything, default: String, max_length: Integer).returns(String) } 10 | def create(s, default: "identifier", max_length: 24) 11 | case s 12 | when Kernel 13 | id = (s.to_s.downcase 14 | .gsub(/:0x[0-9a-f]+/, "") # Lazy attempt to wipe any Object:0x802beef identifiers 15 | .gsub(/[^\w\s]/, "") 16 | .gsub(/^\d+/, "")[0...max_length] || "") 17 | .strip 18 | .gsub(/\s+/, "_") # snake_case 19 | 20 | if id.empty? 21 | default 22 | else 23 | unreserved(id, default) 24 | end 25 | else 26 | default 27 | end 28 | end 29 | 30 | private 31 | 32 | sig { params(id: String, default: String).returns(String) } 33 | def unreserved(id, default) 34 | return id unless KEYWORDS.include?(id) 35 | 36 | "#{id}_#{default}" 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /src/mocktail/share/stringifies_method_name.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | module Mocktail 4 | class StringifiesMethodName 5 | extend T::Sig 6 | 7 | sig { params(call: Call).returns(String) } 8 | def stringify(call) 9 | [ 10 | call.original_type&.name, 11 | call.singleton ? "." : "#", 12 | call.method 13 | ].join 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /src/mocktail/simulates_argument_error.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | require_relative "simulates_argument_error/transforms_params" 4 | require_relative "simulates_argument_error/reconciles_args_with_params" 5 | require_relative "simulates_argument_error/recreates_message" 6 | require_relative "share/cleans_backtrace" 7 | require_relative "share/stringifies_call" 8 | 9 | module Mocktail 10 | class SimulatesArgumentError 11 | extend T::Sig 12 | 13 | sig { void } 14 | def initialize 15 | @transforms_params = T.let(TransformsParams.new, TransformsParams) 16 | @reconciles_args_with_params = T.let(ReconcilesArgsWithParams.new, ReconcilesArgsWithParams) 17 | @recreates_message = T.let(RecreatesMessage.new, RecreatesMessage) 18 | @cleans_backtrace = T.let(CleansBacktrace.new, CleansBacktrace) 19 | @stringifies_call = T.let(StringifiesCall.new, StringifiesCall) 20 | end 21 | 22 | sig { params(dry_call: Call).returns(T.nilable(ArgumentError)) } 23 | def simulate(dry_call) 24 | signature = @transforms_params.transform(dry_call) 25 | 26 | unless @reconciles_args_with_params.reconcile(signature) 27 | @cleans_backtrace.clean( 28 | ArgumentError.new([ 29 | @recreates_message.recreate(signature), 30 | "[Mocktail call: `#{@stringifies_call.stringify(dry_call)}']" 31 | ].join(" ")) 32 | ) 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /src/mocktail/simulates_argument_error/reconciles_args_with_params.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | module Mocktail 4 | class ReconcilesArgsWithParams 5 | extend T::Sig 6 | 7 | sig { params(signature: Signature).returns(T::Boolean) } 8 | def reconcile(signature) 9 | args_match?(signature.positional_params, signature.positional_args) && 10 | kwargs_match?(signature.keyword_params, signature.keyword_args) 11 | end 12 | 13 | private 14 | 15 | sig { params(arg_params: Params, args: T::Array[T.untyped]).returns(T::Boolean) } 16 | def args_match?(arg_params, args) 17 | args.size >= arg_params.required.size && 18 | (arg_params.rest? || args.size <= arg_params.allowed.size) 19 | end 20 | 21 | sig { params(kwarg_params: Params, kwargs: T::Hash[Symbol, T.untyped]).returns(T::Boolean) } 22 | def kwargs_match?(kwarg_params, kwargs) 23 | kwarg_params.required.all? { |name| kwargs.key?(name) } && 24 | (kwarg_params.rest? || kwargs.keys.all? { |name| kwarg_params.allowed.include?(name) }) 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /src/mocktail/sorbet.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | require_relative "sorbet/mocktail" 4 | -------------------------------------------------------------------------------- /src/mocktail/typed.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | module Mocktail 4 | TYPED = true 5 | end 6 | -------------------------------------------------------------------------------- /src/mocktail/value.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | require_relative "value/cabinet" 4 | require_relative "value/call" 5 | require_relative "value/demo_config" 6 | require_relative "value/explanation" 7 | require_relative "value/explanation_data" 8 | require_relative "value/double" 9 | require_relative "value/double_data" 10 | require_relative "value/fake_method_data" 11 | require_relative "value/matcher_registry" 12 | require_relative "value/no_explanation_data" 13 | require_relative "value/signature" 14 | require_relative "value/stubbing" 15 | require_relative "value/type_replacement" 16 | require_relative "value/type_replacement_data" 17 | require_relative "value/unsatisfying_call_explanation" 18 | require_relative "value/unsatisfying_call" 19 | require_relative "value/top_shelf" 20 | -------------------------------------------------------------------------------- /src/mocktail/value/demo_config.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | module Mocktail 4 | class DemoConfig < T::Struct 5 | const :ignore_block, T::Boolean, default: false 6 | const :ignore_extra_args, T::Boolean, default: false 7 | const :ignore_arity, T::Boolean, default: false 8 | const :times, T.nilable(Integer), default: nil 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /src/mocktail/value/double.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | module Mocktail 4 | class Double < T::Struct 5 | const :original_type, T.any(T::Class[T.anything], Module) 6 | const :dry_type, T::Class[T.anything] 7 | const :dry_instance, Object 8 | const :dry_methods, T::Array[Symbol] 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /src/mocktail/value/double_data.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | require_relative "call" 4 | require_relative "stubbing" 5 | 6 | module Mocktail 7 | class DoubleData < T::Struct 8 | include ExplanationData 9 | 10 | const :type, T.any(T::Class[T.anything], Module) 11 | const :double, Object 12 | const :calls, T::Array[Call] 13 | const :stubbings, T::Array[Stubbing[T.anything]] 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /src/mocktail/value/explanation_data.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | module Mocktail 4 | module ExplanationData 5 | extend T::Helpers 6 | extend T::Sig 7 | 8 | interface! 9 | include Kernel 10 | 11 | sig { abstract.returns T::Array[Mocktail::Call] } 12 | def calls 13 | end 14 | 15 | sig { abstract.returns T::Array[Mocktail::Stubbing[T.anything]] } 16 | def stubbings 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /src/mocktail/value/fake_method_data.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | module Mocktail 4 | class FakeMethodData < T::Struct 5 | include ExplanationData 6 | 7 | const :receiver, T.anything 8 | const :calls, T::Array[Call] 9 | const :stubbings, T::Array[Stubbing[T.anything]] 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /src/mocktail/value/matcher_registry.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | module Mocktail 4 | class MatcherRegistry 5 | extend T::Sig 6 | 7 | sig { returns(MatcherRegistry) } 8 | def self.instance 9 | @matcher_registry ||= T.let(new, T.nilable(T.attached_class)) 10 | end 11 | 12 | sig { void } 13 | def initialize 14 | @matchers = T.let({}, T::Hash[Symbol, T.class_of(Matchers::Base)]) 15 | end 16 | 17 | sig { params(matcher_type: T.class_of(Matchers::Base)).void } 18 | def add(matcher_type) 19 | @matchers[matcher_type.matcher_name] = matcher_type 20 | end 21 | 22 | sig { params(name: Symbol).returns(T.nilable(T.class_of(Matchers::Base))) } 23 | def get(name) 24 | @matchers[name] 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /src/mocktail/value/no_explanation_data.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | module Mocktail 4 | class NoExplanationData < T::Struct 5 | extend T::Sig 6 | include ExplanationData 7 | 8 | const :thing, Object 9 | 10 | sig { override.returns(T::Array[Mocktail::Call]) } 11 | def calls 12 | raise Error.new("No calls have been recorded for #{thing.inspect}, because Mocktail doesn't know what it is.") 13 | end 14 | 15 | sig { override.returns T::Array[Mocktail::Stubbing[T.anything]] } 16 | def stubbings 17 | raise Error.new("No stubbings exist on #{thing.inspect}, because Mocktail doesn't know what it is.") 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /src/mocktail/value/signature.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | module Mocktail 4 | class Params < T::Struct 5 | extend T::Sig 6 | 7 | prop :all, T::Array[Symbol], default: [] 8 | prop :required, T::Array[Symbol], default: [] 9 | prop :optional, T::Array[Symbol], default: [] 10 | prop :rest, T.nilable(Symbol) 11 | 12 | sig { returns(T::Array[Symbol]) } 13 | def allowed 14 | all.select { |name| required.include?(name) || optional.include?(name) } 15 | end 16 | 17 | sig { returns(T::Boolean) } 18 | def rest? 19 | !!rest 20 | end 21 | end 22 | 23 | class Signature < T::Struct 24 | const :positional_params, Params 25 | const :positional_args, T::Array[T.anything] 26 | const :keyword_params, Params 27 | const :keyword_args, T::Hash[Symbol, T.anything] 28 | const :block_param, T.nilable(Symbol) 29 | const :block_arg, T.nilable(Proc), default: nil 30 | 31 | DEFAULT_REST_ARGS = "args" 32 | DEFAULT_REST_KWARGS = "kwargs" 33 | DEFAULT_BLOCK_PARAM = "blk" 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /src/mocktail/value/stubbing.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | module Mocktail 4 | class Stubbing < T::Struct 5 | extend T::Sig 6 | extend T::Generic 7 | MethodReturnType = type_member 8 | 9 | const :demonstration, T.proc.params(matchers: Mocktail::MatcherPresentation).returns(MethodReturnType) 10 | const :demo_config, DemoConfig 11 | prop :satisfaction_count, Integer, default: 0 12 | const :recording, Call 13 | prop :effect, T.nilable(T.proc.params(call: Mocktail::Call).returns(MethodReturnType)) 14 | 15 | sig { void } 16 | def satisfied! 17 | self.satisfaction_count += 1 18 | end 19 | 20 | sig { params(block: T.proc.params(call: Mocktail::Call).returns(MethodReturnType)).void } 21 | def with(&block) 22 | self.effect = block 23 | nil 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /src/mocktail/value/type_replacement.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | module Mocktail 4 | class TypeReplacement < T::Struct 5 | const :type, T.any(T::Class[T.anything], Module) 6 | prop :original_methods, T.nilable(T::Array[Method]) 7 | prop :replacement_methods, T.nilable(T::Array[Method]) 8 | prop :original_new, T.nilable(Method) 9 | prop :replacement_new, T.nilable(Method) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /src/mocktail/value/type_replacement_data.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | module Mocktail 4 | class TypeReplacementData < T::Struct 5 | extend T::Sig 6 | 7 | const :type, T.any(T::Class[T.anything], Module) 8 | const :replaced_method_names, T::Array[Symbol] 9 | const :calls, T::Array[Call] 10 | const :stubbings, T::Array[Stubbing[T.anything]] 11 | 12 | include ExplanationData 13 | 14 | sig { returns(T.any(T::Class[T.anything], Module)) } 15 | def double 16 | type 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /src/mocktail/value/unsatisfying_call.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | module Mocktail 4 | class UnsatisfyingCall < T::Struct 5 | const :call, Call 6 | const :other_stubbings, T::Array[Stubbing[T.anything]] 7 | const :backtrace, T::Array[String] 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /src/mocktail/value/unsatisfying_call_explanation.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | module Mocktail 4 | class UnsatisfyingCallExplanation 5 | extend T::Sig 6 | 7 | sig { returns(UnsatisfyingCall) } 8 | attr_reader :reference 9 | 10 | sig { returns(String) } 11 | attr_reader :message 12 | 13 | sig { params(reference: UnsatisfyingCall, message: String).void } 14 | def initialize(reference, message) 15 | @reference = reference 16 | @message = message 17 | end 18 | 19 | sig { returns(T.class_of(UnsatisfyingCallExplanation)) } 20 | def type 21 | self.class 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /src/mocktail/verifies_call.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | require_relative "records_demonstration" 4 | require_relative "verifies_call/finds_verifiable_calls" 5 | require_relative "verifies_call/raises_verification_error" 6 | 7 | module Mocktail 8 | class VerifiesCall 9 | extend T::Sig 10 | 11 | sig { void } 12 | def initialize 13 | @records_demonstration = T.let(RecordsDemonstration.new, RecordsDemonstration) 14 | @finds_verifiable_calls = T.let(FindsVerifiableCalls.new, FindsVerifiableCalls) 15 | @raises_verification_error = T.let(RaisesVerificationError.new, RaisesVerificationError) 16 | end 17 | 18 | sig { params(demo: T.proc.params(matchers: Mocktail::MatcherPresentation).void, demo_config: DemoConfig).void } 19 | def verify(demo, demo_config) 20 | recording = @records_demonstration.record(demo, demo_config) 21 | verifiable_calls = @finds_verifiable_calls.find(recording, demo_config) 22 | 23 | unless verification_satisfied?(verifiable_calls.size, demo_config) 24 | @raises_verification_error.raise(recording, verifiable_calls, demo_config) 25 | end 26 | nil 27 | end 28 | 29 | private 30 | 31 | sig { params(verifiable_call_count: Integer, demo_config: DemoConfig).returns(T::Boolean) } 32 | def verification_satisfied?(verifiable_call_count, demo_config) 33 | (demo_config.times.nil? && verifiable_call_count > 0) || 34 | (demo_config.times == verifiable_call_count) 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /src/mocktail/verifies_call/finds_verifiable_calls.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | require_relative "../share/determines_matching_calls" 4 | 5 | module Mocktail 6 | class FindsVerifiableCalls 7 | extend T::Sig 8 | 9 | sig { void } 10 | def initialize 11 | @determines_matching_calls = T.let(DeterminesMatchingCalls.new, DeterminesMatchingCalls) 12 | end 13 | 14 | sig { params(recording: Call, demo_config: DemoConfig).returns(T::Array[Call]) } 15 | def find(recording, demo_config) 16 | Mocktail.cabinet.calls.select { |call| 17 | @determines_matching_calls.determine(call, recording, demo_config) 18 | } 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /src/mocktail/verifies_call/raises_verification_error/gathers_calls_of_method.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | module Mocktail 4 | class GathersCallsOfMethod 5 | extend T::Sig 6 | 7 | sig { params(dry_call: Call).returns(T::Array[Call]) } 8 | def gather(dry_call) 9 | Mocktail.cabinet.calls.select { |call| 10 | call.double == dry_call.double && 11 | call.method == dry_call.method 12 | } 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /src/mocktail/version.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | module Mocktail 4 | # The gemspec will define Module::VERSION as loaded from lib/, but if the 5 | # user loads mocktail/sorbet, its version file will be effectively redefining 6 | # it. Undef it first to ensure we don't spew warnings 7 | if defined?(VERSION) 8 | Mocktail.send(:remove_const, :VERSION) 9 | end 10 | 11 | VERSION = "2.0.0" 12 | end 13 | -------------------------------------------------------------------------------- /sub_projects/rbi_generator/Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "mocktail", require: false, path: "../.." 4 | gem "tapioca", require: false 5 | -------------------------------------------------------------------------------- /sub_projects/rbi_generator/sorbet/rbi/gems/.gitattributes: -------------------------------------------------------------------------------- 1 | **/*.rbi linguist-generated=true 2 | -------------------------------------------------------------------------------- /sub_projects/sorbet_user/Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "mocktail", path: "../..", require: "mocktail/sorbet" 4 | 5 | gem "standard" 6 | gem "standard-sorbet" 7 | gem "minitest" 8 | gem "rake" 9 | gem "m" 10 | 11 | gem "sorbet" 12 | gem "sorbet-runtime" 13 | gem "tapioca" 14 | -------------------------------------------------------------------------------- /sub_projects/sorbet_user/Rakefile: -------------------------------------------------------------------------------- 1 | require "rake/testtask" 2 | require "standard/rake" 3 | 4 | Rake::TestTask.new(:test) do |t| 5 | t.libs << "test" 6 | t.libs << "lib" 7 | t.test_files = FileList["test/**/*_test.rb"] 8 | end 9 | 10 | task default: [:test, "standard:fix"] 11 | -------------------------------------------------------------------------------- /sub_projects/sorbet_user/bin/tapioca: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'tapioca' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) 12 | 13 | bundle_binstub = File.expand_path("bundle", __dir__) 14 | 15 | if File.file?(bundle_binstub) 16 | if File.read(bundle_binstub, 300).include?("This file was generated by Bundler") 17 | load(bundle_binstub) 18 | else 19 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. 20 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") 21 | end 22 | end 23 | 24 | require "rubygems" 25 | require "bundler/setup" 26 | 27 | load Gem.bin_path("tapioca", "tapioca") 28 | -------------------------------------------------------------------------------- /sub_projects/sorbet_user/rbi/mocktail.rbi: -------------------------------------------------------------------------------- 1 | # typed: true 2 | 3 | class Mocktail::MatcherPresentation 4 | sig { 5 | returns(T.untyped) 6 | } 7 | def is_5 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /sub_projects/sorbet_user/script/test: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -xeuo pipefail 4 | 5 | echo "-----> [Sorbet Example Project] Nuking and regenerating Mocktail RBI files" 6 | rm -rf sorbet/rbi/gems/mocktail* 7 | bin/tapioca gems 8 | 9 | echo "-----> [Sorbet Example Project] Type-checking" 10 | bundle exec srb tc 11 | 12 | echo "-----> [Sorbet Example Project] Running tests" 13 | bundle exec rake 14 | -------------------------------------------------------------------------------- /sub_projects/sorbet_user/sorbet/config: -------------------------------------------------------------------------------- 1 | --dir 2 | . 3 | --ignore=tmp/ 4 | --ignore=vendor/ 5 | -------------------------------------------------------------------------------- /sub_projects/sorbet_user/sorbet/rbi/gems/.gitattributes: -------------------------------------------------------------------------------- 1 | **/*.rbi linguist-generated=true 2 | -------------------------------------------------------------------------------- /sub_projects/sorbet_user/sorbet/rbi/gems/method_source@1.1.0.rbi: -------------------------------------------------------------------------------- 1 | # typed: true 2 | 3 | # DO NOT EDIT MANUALLY 4 | # This is an autogenerated file for types exported from the `method_source` gem. 5 | # Please instead update this file by running `bin/tapioca gem method_source`. 6 | 7 | 8 | # THIS IS AN EMPTY RBI FILE. 9 | # see https://github.com/Shopify/tapioca#manually-requiring-parts-of-a-gem 10 | -------------------------------------------------------------------------------- /sub_projects/sorbet_user/sorbet/rbi/gems/rubocop-performance@1.24.0.rbi: -------------------------------------------------------------------------------- 1 | # typed: true 2 | 3 | # DO NOT EDIT MANUALLY 4 | # This is an autogenerated file for types exported from the `rubocop-performance` gem. 5 | # Please instead update this file by running `bin/tapioca gem rubocop-performance`. 6 | 7 | 8 | # THIS IS AN EMPTY RBI FILE. 9 | # see https://github.com/Shopify/tapioca#manually-requiring-parts-of-a-gem 10 | -------------------------------------------------------------------------------- /sub_projects/sorbet_user/sorbet/rbi/gems/rubocop-sorbet@0.9.0.rbi: -------------------------------------------------------------------------------- 1 | # typed: true 2 | 3 | # DO NOT EDIT MANUALLY 4 | # This is an autogenerated file for types exported from the `rubocop-sorbet` gem. 5 | # Please instead update this file by running `bin/tapioca gem rubocop-sorbet`. 6 | 7 | 8 | # THIS IS AN EMPTY RBI FILE. 9 | # see https://github.com/Shopify/tapioca#manually-requiring-parts-of-a-gem 10 | -------------------------------------------------------------------------------- /sub_projects/sorbet_user/sorbet/rbi/gems/sorbet-eraser@0.3.1.rbi: -------------------------------------------------------------------------------- 1 | # typed: true 2 | 3 | # DO NOT EDIT MANUALLY 4 | # This is an autogenerated file for types exported from the `sorbet-eraser` gem. 5 | # Please instead update this file by running `bin/tapioca gem sorbet-eraser`. 6 | 7 | # THIS IS AN EMPTY RBI FILE. 8 | # see https://github.com/Shopify/tapioca#manually-requiring-parts-of-a-gem 9 | -------------------------------------------------------------------------------- /sub_projects/sorbet_user/sorbet/rbi/gems/standard-custom@1.0.2.rbi: -------------------------------------------------------------------------------- 1 | # typed: true 2 | 3 | # DO NOT EDIT MANUALLY 4 | # This is an autogenerated file for types exported from the `standard-custom` gem. 5 | # Please instead update this file by running `bin/tapioca gem standard-custom`. 6 | 7 | # THIS IS AN EMPTY RBI FILE. 8 | # see https://github.com/Shopify/tapioca#manually-requiring-parts-of-a-gem 9 | -------------------------------------------------------------------------------- /sub_projects/sorbet_user/sorbet/rbi/gems/standard-performance@1.7.0.rbi: -------------------------------------------------------------------------------- 1 | # typed: true 2 | 3 | # DO NOT EDIT MANUALLY 4 | # This is an autogenerated file for types exported from the `standard-performance` gem. 5 | # Please instead update this file by running `bin/tapioca gem standard-performance`. 6 | 7 | 8 | # THIS IS AN EMPTY RBI FILE. 9 | # see https://github.com/Shopify/tapioca#manually-requiring-parts-of-a-gem 10 | -------------------------------------------------------------------------------- /sub_projects/sorbet_user/sorbet/tapioca/config.yml: -------------------------------------------------------------------------------- 1 | gem: 2 | # Add your `gem` command parameters here: 3 | # 4 | # exclude: 5 | # - gem_name 6 | # doc: true 7 | # workers: 5 8 | dsl: 9 | # Add your `dsl` command parameters here: 10 | # 11 | # exclude: 12 | # - SomeGeneratorName 13 | # workers: 5 14 | -------------------------------------------------------------------------------- /sub_projects/sorbet_user/sorbet/tapioca/require.rb: -------------------------------------------------------------------------------- 1 | # typed: true 2 | # frozen_string_literal: true 3 | 4 | # Add your extra requires here (`bin/tapioca require` can be used to bootstrap this list) 5 | -------------------------------------------------------------------------------- /sub_projects/sorbet_user/test/ensure_type_safety_test.rb: -------------------------------------------------------------------------------- 1 | # typed: false 2 | 3 | require "test_helper" 4 | 5 | class EnsureTypeSafetyTest < Minitest::Test 6 | def test_basic_case 7 | msg = assert_type_failure "\"foo\" + 1" 8 | 9 | assert_includes msg, "Expected `String` but found `Integer(1)`" 10 | end 11 | 12 | def test_strict_case 13 | msg = assert_strict_type_failure <<~RUBY 14 | def foo 15 | "no sig" 16 | end 17 | RUBY 18 | 19 | assert_includes msg, "The method `foo` does not have a `sig`" 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /sub_projects/sorbet_user/test/test_helper.rb: -------------------------------------------------------------------------------- 1 | # typed: true 2 | 3 | require "mocktail/sorbet" # <-- the special sauce! 4 | require "minitest/autorun" 5 | 6 | require "tempfile" 7 | require "open3" 8 | module SorbetInsurance 9 | include Kernel 10 | 11 | def assert_type_failure(ruby_code, mode: "true") 12 | Tempfile.create do |file| 13 | file.write("# typed: strict\n\n" + ruby_code) 14 | file.flush 15 | stdout, stderr, status = Open3.capture3("bundle exec srb tc #{file.path}") 16 | raise "Type passing succeeded but expected failure (stdout: #{stdout})" if status.success? 17 | "#{stdout}\n#{stderr}" 18 | end 19 | end 20 | 21 | def assert_strict_type_failure(ruby_code) 22 | assert_type_failure(ruby_code, mode: "strict") 23 | end 24 | end 25 | 26 | class Minitest::Test 27 | include SorbetInsurance 28 | include Mocktail::DSL 29 | 30 | make_my_diffs_pretty! 31 | 32 | def teardown 33 | Mocktail.reset 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /sub_projects/untyped_user/Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "mocktail", path: "../.." 4 | gem "minitest" 5 | -------------------------------------------------------------------------------- /sub_projects/untyped_user/Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: ../.. 3 | specs: 4 | mocktail (2.0.0) 5 | sorbet-eraser (~> 0.3.1) 6 | sorbet-runtime (~> 0.5.9204) 7 | 8 | GEM 9 | remote: https://rubygems.org/ 10 | specs: 11 | minitest (5.25.5) 12 | sorbet-eraser (0.3.1) 13 | sorbet-runtime (0.5.11971) 14 | 15 | PLATFORMS 16 | arm64-darwin-22 17 | arm64-darwin-24 18 | 19 | DEPENDENCIES 20 | minitest 21 | mocktail! 22 | 23 | BUNDLED WITH 24 | 2.4.13 25 | -------------------------------------------------------------------------------- /sub_projects/untyped_user/script/test: -------------------------------------------------------------------------------- 1 | #/usr/bin/env bash 2 | 3 | set -xeuo pipefail 4 | 5 | bundle exec ruby antitype_test.rb 6 | -------------------------------------------------------------------------------- /test/mocktail_test.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | require "test_helper" 4 | 5 | class MocktailTest < Minitest::Test 6 | extend T::Sig 7 | 8 | sig { void } 9 | def test_that_it_has_a_version_number 10 | refute_nil ::Mocktail::VERSION 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /test/safe/dsl_test.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | require "test_helper" 4 | 5 | class DslTest < Minitest::Test 6 | extend T::Sig 7 | 8 | sig { void } 9 | def test_that_stubs_and_verifies_have_matching_options 10 | assert_equal unwrap(Mocktail.method(:stubs)).parameters, unwrap(Mocktail.method(:verify)).parameters 11 | end 12 | 13 | sig { params(method: Method).returns(T.any(T::Private::Methods::Signature, Method)) } 14 | def unwrap(method) 15 | T::Private::Methods.signature_for_method(method) || method 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /test/safe/kwargs_vs_options_hash_test.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | require "test_helper" 4 | 5 | class KwargsVsOptionsHashTest < Minitest::Test 6 | include Mocktail::DSL 7 | extend T::Sig 8 | 9 | class Charity 10 | extend T::Sig 11 | 12 | sig { params(amount: T.untyped).returns(T.untyped) } 13 | def donate(amount:) 14 | raise "Unimplemented" 15 | end 16 | 17 | sig { params(opts: T.untyped).returns(T.untyped) } 18 | def give(opts) 19 | raise "Unimplemented" 20 | end 21 | end 22 | 23 | sig { void } 24 | def test_handles_kwargs 25 | aclu = Mocktail.of(Charity) 26 | 27 | stubs { |m| aclu.donate(amount: m.numeric) }.with { :receipt } 28 | 29 | assert_equal :receipt, aclu.donate(amount: 100) 30 | assert_nil aclu.donate(amount: "money?") 31 | end 32 | 33 | sig { void } 34 | def test_handles_options_hashes 35 | wbc = Mocktail.of(Charity) 36 | 37 | stubs { wbc.give(to: "poor") }.with { :stringy_thanks } 38 | stubs { wbc.give({to: :poor}) }.with { :symbol_thanks } 39 | 40 | assert_equal :stringy_thanks, wbc.give(to: "poor") 41 | assert_equal :stringy_thanks, wbc.give({to: "poor"}) 42 | assert_equal :symbol_thanks, wbc.give(to: :poor) 43 | assert_equal :symbol_thanks, wbc.give({to: :poor}) 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /test/support/let.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | module Let 4 | extend T::Sig 5 | include Kernel 6 | 7 | sig { 8 | type_parameters(:T) 9 | .params(method_name: Symbol, initializer: T.proc.returns(T.type_parameter(:T))) 10 | .returns(T.type_parameter(:T)) 11 | } 12 | def let(method_name, &initializer) 13 | current_test_name = case self 14 | when Minitest::Test 15 | name 16 | else 17 | "not_a_test" 18 | end 19 | 20 | @__memos ||= T.let({}, T.nilable(T::Hash[Symbol, T::Hash[Symbol, T.untyped]])) 21 | method_memos = @__memos[current_test_name] ||= {} 22 | method_memos[method_name] ||= initializer.call 23 | define_singleton_method method_name do 24 | method_memos[method_name] 25 | end 26 | method_memos[method_name] 27 | end 28 | 29 | # sig { 30 | # type_parameters(:T) 31 | # .params(initializer: T.proc.returns(T.type_parameter(:T))) 32 | # .returns(T.type_parameter(:T)) 33 | # } 34 | # def subject(&initializer) 35 | # let(:subject, &initializer) 36 | # end 37 | end 38 | -------------------------------------------------------------------------------- /test/support/sorbet_stubs.rb: -------------------------------------------------------------------------------- 1 | # typed: false 2 | 3 | module T 4 | def self.must(*args, **kwargs, &blk) 5 | args&.first 6 | end 7 | 8 | def self.unsafe(*args, **kwargs, &blk) 9 | args&.first 10 | end 11 | 12 | def self.let(*args, **kwargs, &blk) 13 | args&.first 14 | end 15 | 16 | def self.cast(*args, **kwargs, &blk) 17 | args&.first 18 | end 19 | 20 | def self.all(*args, **kwargs, &blk) 21 | end 22 | 23 | def self.untyped 24 | end 25 | 26 | def self.assert_type!(*args, **kwargs, &blk) 27 | end 28 | 29 | module Array 30 | def self.[](*args, **kwargs, &blk) 31 | end 32 | end 33 | 34 | module Class 35 | def self.[](*args, **kwargs, &blk) 36 | end 37 | end 38 | 39 | module Sig 40 | def sig(*args, **kwargs, &blk) 41 | end 42 | end 43 | end 44 | 45 | module SorbetOverride 46 | def self.disable_inline_type_checks(&blk) 47 | blk.call 48 | end 49 | 50 | def self.disable_call_validation_checks(&blk) 51 | blk.call 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | if ENV["COVER"] 4 | require "simplecov" 5 | SimpleCov.start do 6 | SimpleCov.add_filter "/test/" 7 | end 8 | end 9 | 10 | ENV["MOCKTAIL_DEBUG_ACCIDENTAL_INTERNAL_MOCK_CALLS"] = "true" 11 | 12 | $LOAD_PATH.unshift File.expand_path("../#{ENV["MOCKTAIL_TEST_SRC_DIRECTORY"] || "src"}", __dir__) 13 | require "mocktail" 14 | require "minitest/autorun" 15 | 16 | # T is not defined yet, so we can't use T.unsafe to pass typechecking 17 | if eval("Mocktail::TYPED", binding, __FILE__, __LINE__) 18 | require_relative "support/sorbet_override" 19 | else 20 | require_relative "support/sorbet_stubs" 21 | end 22 | 23 | class Minitest::Test 24 | extend T::Sig 25 | 26 | protected 27 | 28 | make_my_diffs_pretty! 29 | 30 | sig { params(blk: T.proc.void).returns(Thread) } 31 | def thread(&blk) 32 | Thread.new(&blk).tap do |t| 33 | t.abort_on_exception = true 34 | end 35 | end 36 | 37 | sig { returns(T::Boolean) } 38 | def runtime_type_checking_disabled? 39 | !T.unsafe(Mocktail::TYPED) || 40 | T::Private::RuntimeLevels.default_checked_level == :never 41 | end 42 | 43 | sig { params(thing: T.anything).void } 44 | def assert_nil_or_void(thing) 45 | if runtime_type_checking_disabled? 46 | assert_nil(thing) 47 | else 48 | assert_same thing, T::Private::Types::Void::VOID 49 | end 50 | end 51 | 52 | sig { void } 53 | def teardown 54 | Mocktail.reset 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /test/unit/matcher_presentation_test.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | require "test_helper" 4 | 5 | module Mocktail 6 | class MatcherPresentationTest < Minitest::Test 7 | extend T::Sig 8 | 9 | sig { params(name: String).void } 10 | def initialize(name) 11 | super 12 | 13 | @subject = T.let(MatcherPresentation.new, MatcherPresentation) 14 | end 15 | 16 | sig { void } 17 | def test_respond_to? 18 | assert @subject.respond_to?(:any) 19 | refute @subject.respond_to?(:nonsense) 20 | 21 | assert_raises(NoMethodError) { T.unsafe(@subject).nonsense } 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /test/unit/matchers/base_test.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | require "test_helper" 4 | 5 | module Mocktail::Matchers 6 | class BaseTest < Minitest::Test 7 | extend T::Sig 8 | 9 | sig { void } 10 | def test_default_method_stubs 11 | skip unless runtime_type_checking_disabled? 12 | 13 | subject = T.unsafe(Base).new(:an_arg) 14 | 15 | e = assert_raises(Mocktail::Error) { Base.matcher_name } 16 | assert_equal "The `matcher_name` class method must return a valid method name", e.message 17 | 18 | e = assert_raises(Mocktail::Error) { subject.match?(:other_arg) } 19 | assert_equal "Matchers must implement `match?(argument)`", e.message 20 | 21 | assert subject.is_mocktail_matcher? 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /test/unit/matchers/captor_test.rb: -------------------------------------------------------------------------------- 1 | # typed: true 2 | 3 | require "test_helper" 4 | 5 | module Mocktail::Matchers 6 | class CaptorTest < Minitest::Test 7 | extend T::Sig 8 | 9 | sig { void } 10 | def test_basic_captor 11 | captor = Captor.new 12 | 13 | refute captor.captured? 14 | assert_nil captor.value 15 | 16 | assert_equal :capture, Captor::Capture.matcher_name 17 | assert captor.capture.match?(42) # side effect! 18 | assert captor.captured? 19 | assert_equal 42, captor.value 20 | 21 | assert_equal "capture", captor.capture.inspect 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /test/unit/matchers/matches_test.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | require "test_helper" 4 | 5 | module Mocktail::Matchers 6 | class GoodMatch 7 | extend T::Sig 8 | 9 | sig { params(other: T.untyped).returns(T.untyped) } 10 | def match?(other) 11 | true 12 | end 13 | end 14 | 15 | class BadMatch 16 | extend T::Sig 17 | 18 | sig { params(args: T.untyped).returns(T.untyped) } 19 | def match?(*args) 20 | raise "💥" 21 | end 22 | end 23 | 24 | class WrongMatch 25 | extend T::Sig 26 | 27 | sig { returns(T.untyped) } 28 | def match? 29 | end 30 | end 31 | 32 | class MatchesTest < Minitest::Test 33 | extend T::Sig 34 | 35 | sig { void } 36 | def test_some_matches 37 | assert_equal "matches(\"name\")", Matches.new("name").inspect 38 | 39 | assert Matches.new("foo").match?("foobar") 40 | assert Matches.new(/\d/).match?("4") 41 | refute Matches.new(/\s/).match?("nospace") 42 | assert Matches.new("foo").match?(GoodMatch.new) 43 | refute Matches.new("foo").match?(BadMatch.new) 44 | refute Matches.new("foo").match?(WrongMatch.new) 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /test/unit/matchers/numeric_test.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | require "test_helper" 4 | require "bigdecimal" 5 | 6 | module Mocktail::Matchers 7 | class NumericTest < Minitest::Test 8 | extend T::Sig 9 | 10 | sig { void } 11 | def test_numeric_types 12 | subject = Numeric.new 13 | 14 | assert_equal :numeric, Numeric.matcher_name 15 | assert_equal "numeric", subject.inspect 16 | assert subject.is_mocktail_matcher? 17 | assert subject.match?(1) 18 | assert subject.match?(1.0) 19 | assert subject.match?(BigDecimal("1.0")) 20 | assert subject.match?(::Numeric.new) 21 | refute subject.match?("Hi") 22 | refute subject.match?(Integer) 23 | refute subject.match?(BigDecimal) 24 | refute subject.match?(::Numeric) 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /test/unit/matchers/that_test.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | require "test_helper" 4 | 5 | module Mocktail::Matchers 6 | class ThatTest < Minitest::Test 7 | extend T::Sig 8 | 9 | sig { void } 10 | def test_basic_that 11 | subject = That.new { |arg| arg == 42 } 12 | 13 | assert_equal :that, That.matcher_name 14 | assert_equal "that {…}", subject.inspect 15 | assert subject.is_mocktail_matcher? 16 | assert subject.match?(42) 17 | refute subject.match?(43) 18 | end 19 | 20 | sig { void } 21 | def test_blockless_that 22 | e = assert_raises(ArgumentError) { That.new } 23 | assert_equal "The `that` matcher must be passed a block (e.g. `that { |arg| … }`)", e.message 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /test/unit/share/bind_test.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | require "test_helper" 4 | 5 | module Mocktail 6 | class BindTest < Minitest::Test 7 | extend T::Sig 8 | 9 | class FortyTwo 10 | extend T::Sig 11 | 12 | sig { params(other: T.untyped).returns(T.untyped) } 13 | def ==(other) 14 | other == 42 15 | end 16 | 17 | sig { returns(T.untyped) } 18 | def self.ancestors 19 | [42, Integer, Numeric, Object] 20 | end 21 | end 22 | 23 | sig { void } 24 | def test_binds_if_thing_is_a_mock 25 | mock = Mocktail.of(FortyTwo) 26 | 27 | assert Bind.call(mock, :==, mock) 28 | end 29 | 30 | sig { void } 31 | def test_calls_through_if_thing_is_not_a_mock 32 | real = FortyTwo.new 33 | 34 | assert Bind.call(real, :==, 42) # <-- is what we want in practice 35 | end 36 | 37 | sig { void } 38 | def test_bind_calls_class_methods_if_faked 39 | Mocktail.replace(FortyTwo) 40 | 41 | assert_nil FortyTwo.ancestors 42 | assert_equal Bind.call(FortyTwo, :ancestors), [42, Integer, Numeric, Object] 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /test/unit/simulates_argument_error/reconciles_args_with_params_test.rb: -------------------------------------------------------------------------------- 1 | # typed: strict 2 | 3 | require "test_helper" 4 | 5 | module Mocktail 6 | class ReconcilesArgsWithParamsTest < Minitest::Test 7 | extend T::Sig 8 | 9 | sig { params(name: String).void } 10 | def initialize(name) 11 | super 12 | 13 | @subject = T.let(ReconcilesArgsWithParams.new, ReconcilesArgsWithParams) 14 | end 15 | 16 | sig { void } 17 | def test_ensure_unknown_keyword_fails 18 | assert_equal false, @subject.reconcile( 19 | Signature.new( 20 | positional_params: Params.new(all: []), 21 | positional_args: [], 22 | keyword_params: Params.new(all: [:a], optional: [:a]), 23 | keyword_args: {b: 42} 24 | ) 25 | ) 26 | end 27 | end 28 | end 29 | --------------------------------------------------------------------------------