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