mocktail 1.2.3 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/main.yml +6 -5
- data/.gitignore +3 -0
- data/.standard.yml +8 -0
- data/CHANGELOG.md +14 -0
- data/Gemfile +6 -1
- data/Gemfile.lock +98 -25
- data/README.md +18 -922
- data/Rakefile +0 -1
- data/bin/console +1 -2
- data/bin/tapioca +29 -0
- data/lib/mocktail/collects_calls.rb +2 -0
- data/lib/mocktail/debug.rb +13 -10
- data/lib/mocktail/dsl.rb +2 -0
- data/lib/mocktail/errors.rb +2 -0
- data/lib/mocktail/explains_nils.rb +2 -0
- data/lib/mocktail/explains_thing.rb +7 -4
- data/lib/mocktail/grabs_original_method_parameters.rb +30 -0
- data/lib/mocktail/handles_dry_call/fulfills_stubbing/describes_unsatisfied_stubbing.rb +3 -1
- data/lib/mocktail/handles_dry_call/fulfills_stubbing/finds_satisfaction.rb +5 -1
- data/lib/mocktail/handles_dry_call/fulfills_stubbing.rb +2 -0
- data/lib/mocktail/handles_dry_call/logs_call.rb +2 -0
- data/lib/mocktail/handles_dry_call/validates_arguments.rb +6 -4
- data/lib/mocktail/handles_dry_call.rb +2 -0
- data/lib/mocktail/handles_dry_new_call.rb +2 -0
- data/lib/mocktail/imitates_type/ensures_imitation_support.rb +2 -0
- data/lib/mocktail/imitates_type/makes_double/declares_dry_class/reconstructs_call.rb +4 -1
- data/lib/mocktail/imitates_type/makes_double/declares_dry_class.rb +32 -20
- data/lib/mocktail/imitates_type/makes_double/gathers_fakeable_instance_methods.rb +2 -0
- data/lib/mocktail/imitates_type/makes_double.rb +3 -0
- data/lib/mocktail/imitates_type.rb +3 -1
- data/lib/mocktail/initialize_based_on_type_system_mode_switching.rb +9 -0
- data/lib/mocktail/initializes_mocktail.rb +5 -0
- data/lib/mocktail/matcher_presentation.rb +4 -2
- data/lib/mocktail/matchers/any.rb +4 -3
- data/lib/mocktail/matchers/base.rb +10 -2
- data/lib/mocktail/matchers/captor.rb +9 -0
- data/lib/mocktail/matchers/includes.rb +2 -0
- data/lib/mocktail/matchers/includes_hash.rb +9 -0
- data/lib/mocktail/matchers/includes_key.rb +9 -0
- data/lib/mocktail/matchers/includes_string.rb +9 -0
- data/lib/mocktail/matchers/is_a.rb +2 -0
- data/lib/mocktail/matchers/matches.rb +2 -0
- data/lib/mocktail/matchers/not.rb +2 -0
- data/lib/mocktail/matchers/numeric.rb +5 -4
- data/lib/mocktail/matchers/that.rb +2 -0
- data/lib/mocktail/matchers.rb +3 -0
- data/lib/mocktail/raises_neato_no_method_error.rb +2 -0
- data/lib/mocktail/records_demonstration.rb +2 -0
- data/lib/mocktail/registers_matcher.rb +8 -3
- data/lib/mocktail/registers_stubbing.rb +2 -0
- data/lib/mocktail/replaces_next.rb +7 -1
- data/lib/mocktail/replaces_type/redefines_new.rb +3 -1
- data/lib/mocktail/replaces_type/redefines_singleton_methods.rb +14 -2
- data/lib/mocktail/replaces_type/runs_sorbet_sig_blocks_before_replacement.rb +37 -0
- data/lib/mocktail/replaces_type.rb +6 -0
- data/lib/mocktail/resets_state.rb +2 -0
- data/lib/mocktail/share/bind.rb +7 -5
- data/lib/mocktail/share/cleans_backtrace.rb +3 -5
- data/lib/mocktail/share/creates_identifier.rb +16 -9
- data/lib/mocktail/share/determines_matching_calls.rb +4 -2
- data/lib/mocktail/share/stringifies_call.rb +6 -2
- data/lib/mocktail/share/stringifies_method_name.rb +3 -1
- data/lib/mocktail/simulates_argument_error/reconciles_args_with_params.rb +2 -0
- data/lib/mocktail/simulates_argument_error/recreates_message.rb +2 -0
- data/lib/mocktail/simulates_argument_error/transforms_params.rb +15 -8
- data/lib/mocktail/simulates_argument_error.rb +2 -0
- data/lib/mocktail/sorbet/mocktail/collects_calls.rb +18 -0
- data/lib/mocktail/sorbet/mocktail/debug.rb +54 -0
- data/lib/mocktail/sorbet/mocktail/dsl.rb +46 -0
- data/lib/mocktail/sorbet/mocktail/errors.rb +19 -0
- data/lib/mocktail/sorbet/mocktail/explains_nils.rb +41 -0
- data/lib/mocktail/sorbet/mocktail/explains_thing.rb +137 -0
- data/lib/mocktail/sorbet/mocktail/grabs_original_method_parameters.rb +33 -0
- data/lib/mocktail/sorbet/mocktail/handles_dry_call/fulfills_stubbing/describes_unsatisfied_stubbing.rb +27 -0
- data/lib/mocktail/sorbet/mocktail/handles_dry_call/fulfills_stubbing/finds_satisfaction.rb +24 -0
- data/lib/mocktail/sorbet/mocktail/handles_dry_call/fulfills_stubbing.rb +45 -0
- data/lib/mocktail/sorbet/mocktail/handles_dry_call/logs_call.rb +12 -0
- data/lib/mocktail/sorbet/mocktail/handles_dry_call/validates_arguments.rb +45 -0
- data/lib/mocktail/sorbet/mocktail/handles_dry_call.rb +25 -0
- data/lib/mocktail/sorbet/mocktail/handles_dry_new_call.rb +42 -0
- data/lib/mocktail/sorbet/mocktail/imitates_type/ensures_imitation_support.rb +16 -0
- data/lib/mocktail/sorbet/mocktail/imitates_type/makes_double/declares_dry_class/reconstructs_call.rb +73 -0
- data/lib/mocktail/sorbet/mocktail/imitates_type/makes_double/declares_dry_class.rb +136 -0
- data/lib/mocktail/sorbet/mocktail/imitates_type/makes_double/gathers_fakeable_instance_methods.rb +28 -0
- data/lib/mocktail/sorbet/mocktail/imitates_type/makes_double.rb +29 -0
- data/lib/mocktail/sorbet/mocktail/imitates_type.rb +29 -0
- data/lib/mocktail/sorbet/mocktail/initialize_based_on_type_system_mode_switching.rb +11 -0
- data/lib/mocktail/sorbet/mocktail/initializes_mocktail.rb +25 -0
- data/lib/mocktail/sorbet/mocktail/matcher_presentation.rb +21 -0
- data/lib/mocktail/sorbet/mocktail/matchers/any.rb +27 -0
- data/lib/mocktail/sorbet/mocktail/matchers/base.rb +39 -0
- data/lib/mocktail/sorbet/mocktail/matchers/captor.rb +76 -0
- data/lib/mocktail/sorbet/mocktail/matchers/includes.rb +32 -0
- data/lib/mocktail/sorbet/mocktail/matchers/includes_hash.rb +12 -0
- data/lib/mocktail/sorbet/mocktail/matchers/includes_key.rb +12 -0
- data/lib/mocktail/sorbet/mocktail/matchers/includes_string.rb +12 -0
- data/lib/mocktail/sorbet/mocktail/matchers/is_a.rb +17 -0
- data/lib/mocktail/sorbet/mocktail/matchers/matches.rb +19 -0
- data/lib/mocktail/sorbet/mocktail/matchers/not.rb +17 -0
- data/lib/mocktail/sorbet/mocktail/matchers/numeric.rb +27 -0
- data/lib/mocktail/sorbet/mocktail/matchers/that.rb +32 -0
- data/lib/mocktail/sorbet/mocktail/matchers.rb +19 -0
- data/lib/mocktail/sorbet/mocktail/raises_neato_no_method_error.rb +93 -0
- data/lib/mocktail/sorbet/mocktail/records_demonstration.rb +43 -0
- data/lib/mocktail/sorbet/mocktail/registers_matcher.rb +65 -0
- data/lib/mocktail/sorbet/mocktail/registers_stubbing.rb +31 -0
- data/lib/mocktail/sorbet/mocktail/replaces_next.rb +55 -0
- data/lib/mocktail/sorbet/mocktail/replaces_type/redefines_new.rb +32 -0
- data/lib/mocktail/sorbet/mocktail/replaces_type/redefines_singleton_methods.rb +80 -0
- data/lib/mocktail/sorbet/mocktail/replaces_type/runs_sorbet_sig_blocks_before_replacement.rb +39 -0
- data/lib/mocktail/sorbet/mocktail/replaces_type.rb +36 -0
- data/lib/mocktail/sorbet/mocktail/resets_state.rb +14 -0
- data/lib/mocktail/sorbet/mocktail/share/bind.rb +18 -0
- data/lib/mocktail/sorbet/mocktail/share/cleans_backtrace.rb +22 -0
- data/lib/mocktail/sorbet/mocktail/share/creates_identifier.rb +39 -0
- data/lib/mocktail/sorbet/mocktail/share/determines_matching_calls.rb +72 -0
- data/lib/mocktail/sorbet/mocktail/share/stringifies_call.rb +85 -0
- data/lib/mocktail/sorbet/mocktail/share/stringifies_method_name.rb +16 -0
- data/lib/mocktail/sorbet/mocktail/simulates_argument_error/reconciles_args_with_params.rb +27 -0
- data/lib/mocktail/sorbet/mocktail/simulates_argument_error/recreates_message.rb +34 -0
- data/lib/mocktail/sorbet/mocktail/simulates_argument_error/transforms_params.rb +58 -0
- data/lib/mocktail/sorbet/mocktail/simulates_argument_error.rb +36 -0
- data/lib/mocktail/sorbet/mocktail/sorbet.rb +3 -0
- data/lib/mocktail/sorbet/mocktail/stringifies_method_signature.rb +53 -0
- data/lib/mocktail/sorbet/mocktail/typed.rb +5 -0
- data/lib/mocktail/sorbet/mocktail/value/cabinet.rb +91 -0
- data/lib/mocktail/sorbet/mocktail/value/call.rb +51 -0
- data/lib/mocktail/sorbet/mocktail/value/demo_config.rb +10 -0
- data/lib/mocktail/sorbet/mocktail/value/double.rb +10 -0
- data/lib/mocktail/sorbet/mocktail/value/double_data.rb +15 -0
- data/lib/mocktail/sorbet/mocktail/value/explanation.rb +68 -0
- data/lib/mocktail/sorbet/mocktail/value/explanation_data.rb +19 -0
- data/lib/mocktail/sorbet/mocktail/value/fake_method_data.rb +11 -0
- data/lib/mocktail/sorbet/mocktail/value/matcher_registry.rb +27 -0
- data/lib/mocktail/sorbet/mocktail/value/no_explanation_data.rb +20 -0
- data/lib/mocktail/sorbet/mocktail/value/signature.rb +35 -0
- data/lib/mocktail/sorbet/mocktail/value/stubbing.rb +26 -0
- data/lib/mocktail/sorbet/mocktail/value/top_shelf.rb +79 -0
- data/lib/mocktail/sorbet/mocktail/value/type_replacement.rb +11 -0
- data/lib/mocktail/sorbet/mocktail/value/type_replacement_data.rb +19 -0
- data/lib/mocktail/sorbet/mocktail/value/unsatisfying_call.rb +9 -0
- data/lib/mocktail/sorbet/mocktail/value/unsatisfying_call_explanation.rb +24 -0
- data/lib/mocktail/sorbet/mocktail/value.rb +19 -0
- data/lib/mocktail/sorbet/mocktail/verifies_call/finds_verifiable_calls.rb +21 -0
- data/lib/mocktail/sorbet/mocktail/verifies_call/raises_verification_error/gathers_calls_of_method.rb +15 -0
- data/lib/mocktail/sorbet/mocktail/verifies_call/raises_verification_error.rb +74 -0
- data/lib/mocktail/sorbet/mocktail/verifies_call.rb +37 -0
- data/lib/mocktail/sorbet/mocktail/version.rb +12 -0
- data/lib/mocktail/sorbet/mocktail.rb +154 -0
- data/lib/mocktail/sorbet.rb +1 -0
- data/lib/mocktail/stringifies_method_signature.rb +2 -0
- data/lib/mocktail/typed.rb +3 -0
- data/lib/mocktail/value/cabinet.rb +8 -1
- data/lib/mocktail/value/call.rb +44 -12
- data/lib/mocktail/value/demo_config.rb +6 -7
- data/lib/mocktail/value/double.rb +6 -7
- data/lib/mocktail/value/double_data.rb +11 -7
- data/lib/mocktail/value/explanation.rb +28 -3
- data/lib/mocktail/value/explanation_data.rb +14 -0
- data/lib/mocktail/value/fake_method_data.rb +7 -6
- data/lib/mocktail/value/matcher_registry.rb +2 -0
- data/lib/mocktail/value/no_explanation_data.rb +16 -0
- data/lib/mocktail/value/signature.rb +19 -27
- data/lib/mocktail/value/stubbing.rb +11 -12
- data/lib/mocktail/value/top_shelf.rb +5 -0
- data/lib/mocktail/value/type_replacement.rb +7 -8
- data/lib/mocktail/value/type_replacement_data.rb +10 -7
- data/lib/mocktail/value/unsatisfying_call.rb +5 -6
- data/lib/mocktail/value/unsatisfying_call_explanation.rb +18 -0
- data/lib/mocktail/value.rb +5 -2
- data/lib/mocktail/verifies_call/finds_verifiable_calls.rb +2 -0
- data/lib/mocktail/verifies_call/raises_verification_error/gathers_calls_of_method.rb +2 -0
- data/lib/mocktail/verifies_call/raises_verification_error.rb +2 -0
- data/lib/mocktail/verifies_call.rb +3 -0
- data/lib/mocktail/version.rb +8 -1
- data/lib/mocktail.rb +46 -5
- data/mocktail.gemspec +8 -4
- data/rbi/mocktail-pregenerated.rbi +1865 -0
- data/rbi/mocktail.rbi +77 -0
- data/rbi/sorbet-runtime.rbi +29 -0
- data/spoom_report.html +1248 -0
- metadata +130 -3
data/lib/mocktail/sorbet/mocktail/imitates_type/makes_double/gathers_fakeable_instance_methods.rb
ADDED
@@ -0,0 +1,28 @@
|
|
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
|
@@ -0,0 +1,29 @@
|
|
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
|
@@ -0,0 +1,29 @@
|
|
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
|
@@ -0,0 +1,11 @@
|
|
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
|
@@ -0,0 +1,25 @@
|
|
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
|
@@ -0,0 +1,21 @@
|
|
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
|
@@ -0,0 +1,27 @@
|
|
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
|
@@ -0,0 +1,39 @@
|
|
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
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# typed: strict
|
2
|
+
|
3
|
+
module Mocktail::Matchers
|
4
|
+
# Captors are conceptually complex implementations, but with a simple usage/purpose:
|
5
|
+
# They are values the user can create and hold onto that will return a matcher
|
6
|
+
# and then "capture" the value made by the real call, for later analysis & assertion.
|
7
|
+
#
|
8
|
+
# Unlike other matchers, these don't make any useful sense for stubbing, but are
|
9
|
+
# very useful when asserting complication call verifications
|
10
|
+
#
|
11
|
+
# The fact the user will need the reference outside the verification call is
|
12
|
+
# why this is a top-level method on Mocktail, and not included in the |m| block
|
13
|
+
# arg to stubs/verify
|
14
|
+
#
|
15
|
+
# See Mockito, which is the earliest implementation I know of:
|
16
|
+
# https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/Captor.html
|
17
|
+
class Captor
|
18
|
+
extend T::Sig
|
19
|
+
|
20
|
+
class Capture < Mocktail::Matchers::Base
|
21
|
+
extend T::Sig
|
22
|
+
|
23
|
+
sig { returns(Symbol) }
|
24
|
+
def self.matcher_name
|
25
|
+
:capture
|
26
|
+
end
|
27
|
+
|
28
|
+
sig { returns(T.untyped) }
|
29
|
+
attr_reader :value
|
30
|
+
|
31
|
+
sig { void }
|
32
|
+
def initialize
|
33
|
+
@value = T.let(nil, T.untyped)
|
34
|
+
@captured = T.let(false, T::Boolean)
|
35
|
+
end
|
36
|
+
|
37
|
+
sig { params(actual: T.untyped).returns(TrueClass) }
|
38
|
+
def match?(actual)
|
39
|
+
@value = actual
|
40
|
+
@captured = true
|
41
|
+
true
|
42
|
+
end
|
43
|
+
|
44
|
+
sig { returns(T::Boolean) }
|
45
|
+
def captured?
|
46
|
+
@captured
|
47
|
+
end
|
48
|
+
|
49
|
+
sig { returns(String) }
|
50
|
+
def inspect
|
51
|
+
"capture"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# This T.untyped is intentional. Even though a Capture is surely returned,
|
56
|
+
# in order for a verification demonstration to pass its own type check,
|
57
|
+
# it needs to think it's being returned whatever parameter is expected
|
58
|
+
sig { returns(T.untyped) }
|
59
|
+
attr_reader :capture
|
60
|
+
|
61
|
+
sig { void }
|
62
|
+
def initialize
|
63
|
+
@capture = T.let(Capture.new, Capture)
|
64
|
+
end
|
65
|
+
|
66
|
+
sig { returns(T::Boolean) }
|
67
|
+
def captured?
|
68
|
+
@capture.captured?
|
69
|
+
end
|
70
|
+
|
71
|
+
sig { returns(T.untyped) }
|
72
|
+
def value
|
73
|
+
@capture.value
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,32 @@
|
|
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
|
@@ -0,0 +1,17 @@
|
|
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
|
@@ -0,0 +1,19 @@
|
|
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
|
@@ -0,0 +1,17 @@
|
|
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
|
@@ -0,0 +1,27 @@
|
|
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
|
@@ -0,0 +1,32 @@
|
|
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
|
@@ -0,0 +1,19 @@
|
|
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"
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# typed: strict
|
2
|
+
|
3
|
+
require_relative "share/stringifies_call"
|
4
|
+
require_relative "share/stringifies_method_name"
|
5
|
+
require_relative "share/creates_identifier"
|
6
|
+
|
7
|
+
module Mocktail
|
8
|
+
class RaisesNeatoNoMethodError
|
9
|
+
extend T::Sig
|
10
|
+
|
11
|
+
sig { void }
|
12
|
+
def initialize
|
13
|
+
@stringifies_call = T.let(StringifiesCall.new, StringifiesCall)
|
14
|
+
@stringifies_method_name = T.let(StringifiesMethodName.new, StringifiesMethodName)
|
15
|
+
@creates_identifier = T.let(CreatesIdentifier.new, CreatesIdentifier)
|
16
|
+
end
|
17
|
+
|
18
|
+
sig { params(call: Call).void }
|
19
|
+
def call(call)
|
20
|
+
raise NoMethodError, <<~MSG, caller[1..]
|
21
|
+
No method `#{@stringifies_method_name.stringify(call)}' exists for call:
|
22
|
+
|
23
|
+
#{@stringifies_call.stringify(call, anonymous_blocks: true, always_parens: true)}
|
24
|
+
|
25
|
+
Need to define the method? Here's a sample definition:
|
26
|
+
|
27
|
+
def #{"self." if call.singleton}#{call.method}#{params(call)}
|
28
|
+
end
|
29
|
+
#{corrections(call)}
|
30
|
+
MSG
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
sig { params(call: Call).returns(T.nilable(String)) }
|
36
|
+
def params(call)
|
37
|
+
return if (params_lists = [
|
38
|
+
params_list(call.args),
|
39
|
+
kwparams_list(call.kwargs),
|
40
|
+
block_param(call.block)
|
41
|
+
].compact).empty?
|
42
|
+
|
43
|
+
"(#{params_lists.join(", ")})"
|
44
|
+
end
|
45
|
+
|
46
|
+
sig { params(args: T::Array[T.anything]).returns(T.nilable(String)) }
|
47
|
+
def params_list(args)
|
48
|
+
return if args.empty?
|
49
|
+
|
50
|
+
count_repeats(args.map { |arg|
|
51
|
+
@creates_identifier.create(arg, default: "arg")
|
52
|
+
}).join(", ")
|
53
|
+
end
|
54
|
+
|
55
|
+
sig { params(kwargs: T::Hash[Symbol, T.anything]).returns(T.nilable(String)) }
|
56
|
+
def kwparams_list(kwargs)
|
57
|
+
return if kwargs.empty?
|
58
|
+
|
59
|
+
kwargs.keys.map { |key| "#{key}:" }.join(", ")
|
60
|
+
end
|
61
|
+
|
62
|
+
sig { params(block: T.nilable(Proc)).returns(T.nilable(String)) }
|
63
|
+
def block_param(block)
|
64
|
+
return if block.nil?
|
65
|
+
|
66
|
+
"&blk"
|
67
|
+
end
|
68
|
+
|
69
|
+
sig { params(identifiers: T::Array[String]).returns(T::Array[String]) }
|
70
|
+
def count_repeats(identifiers)
|
71
|
+
identifiers.map.with_index { |id, i|
|
72
|
+
if (preceding_matches = T.must(identifiers[0...i]).count { |other_id| id == other_id }) > 0
|
73
|
+
"#{id}#{preceding_matches + 1}"
|
74
|
+
else
|
75
|
+
id
|
76
|
+
end
|
77
|
+
}
|
78
|
+
end
|
79
|
+
|
80
|
+
sig { params(call: Call).returns(T.nilable(String)) }
|
81
|
+
def corrections(call)
|
82
|
+
return if (corrections = DidYouMean::SpellChecker.new(dictionary: T.must(call.original_type).instance_methods).correct(T.must(call.method))).empty?
|
83
|
+
|
84
|
+
<<~MSG
|
85
|
+
|
86
|
+
There #{(corrections.size == 1) ? "is" : "are"} also #{corrections.size} similar method#{"s" if corrections.size != 1} on #{T.must(call.original_type).name}.
|
87
|
+
|
88
|
+
Did you mean?
|
89
|
+
#{corrections.map { |c| " #{c}" }.join("\n")}
|
90
|
+
MSG
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# typed: strict
|
2
|
+
|
3
|
+
module Mocktail
|
4
|
+
class RecordsDemonstration
|
5
|
+
extend T::Sig
|
6
|
+
|
7
|
+
sig {
|
8
|
+
type_parameters(:T)
|
9
|
+
.params(
|
10
|
+
demonstration: T.proc.params(matchers: Mocktail::MatcherPresentation).returns(T.type_parameter(:T)),
|
11
|
+
demo_config: DemoConfig
|
12
|
+
).returns(Call)
|
13
|
+
}
|
14
|
+
def record(demonstration, demo_config)
|
15
|
+
cabinet = Mocktail.cabinet
|
16
|
+
prior_call_count = Mocktail.cabinet.calls.dup.size
|
17
|
+
|
18
|
+
begin
|
19
|
+
cabinet.demonstration_in_progress = true
|
20
|
+
ValidatesArguments.optional(demo_config.ignore_arity) do
|
21
|
+
demonstration.call(Mocktail.matchers)
|
22
|
+
end
|
23
|
+
ensure
|
24
|
+
cabinet.demonstration_in_progress = false
|
25
|
+
end
|
26
|
+
|
27
|
+
if prior_call_count + 1 == cabinet.calls.size
|
28
|
+
T.must(cabinet.calls.pop)
|
29
|
+
elsif prior_call_count == cabinet.calls.size
|
30
|
+
raise MissingDemonstrationError.new <<~MSG.tr("\n", " ")
|
31
|
+
`stubs` & `verify` expect an invocation of a mocked method by a passed
|
32
|
+
block, but no invocation occurred.
|
33
|
+
MSG
|
34
|
+
else
|
35
|
+
raise AmbiguousDemonstrationError.new <<~MSG.tr("\n", " ")
|
36
|
+
`stubs` & `verify` expect exactly one invocation of a mocked method,
|
37
|
+
but #{cabinet.calls.size - prior_call_count} were detected. As a
|
38
|
+
result, Mocktail doesn't know which invocation to stub or verify.
|
39
|
+
MSG
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|