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
@@ -0,0 +1,65 @@
|
|
1
|
+
# typed: strict
|
2
|
+
|
3
|
+
module Mocktail
|
4
|
+
class RegistersMatcher
|
5
|
+
extend T::Sig
|
6
|
+
|
7
|
+
sig { void }
|
8
|
+
def initialize
|
9
|
+
@grabs_original_method_parameters = T.let(GrabsOriginalMethodParameters.new, GrabsOriginalMethodParameters)
|
10
|
+
end
|
11
|
+
|
12
|
+
sig { params(matcher_type: T.class_of(Matchers::Base)).void }
|
13
|
+
def register(matcher_type)
|
14
|
+
if invalid_type?(matcher_type)
|
15
|
+
raise InvalidMatcherError.new <<~MSG.tr("\n", " ")
|
16
|
+
Matchers must be Ruby classes
|
17
|
+
MSG
|
18
|
+
elsif invalid_name?(matcher_type)
|
19
|
+
raise InvalidMatcherError.new <<~MSG.tr("\n", " ")
|
20
|
+
#{matcher_type.name}.matcher_name must return a valid method name
|
21
|
+
MSG
|
22
|
+
elsif invalid_match?(matcher_type)
|
23
|
+
raise InvalidMatcherError.new <<~MSG.tr("\n", " ")
|
24
|
+
#{matcher_type.name}#match? must be defined as a one-argument method
|
25
|
+
MSG
|
26
|
+
elsif invalid_flag?(matcher_type)
|
27
|
+
raise InvalidMatcherError.new <<~MSG.tr("\n", " ")
|
28
|
+
#{matcher_type.name}#is_mocktail_matcher? must be defined
|
29
|
+
MSG
|
30
|
+
else
|
31
|
+
MatcherRegistry.instance.add(matcher_type)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
sig { params(matcher_type: T.class_of(Matchers::Base)).returns(T::Boolean) }
|
38
|
+
def invalid_type?(matcher_type)
|
39
|
+
!matcher_type.is_a?(Class)
|
40
|
+
end
|
41
|
+
|
42
|
+
sig { params(matcher_type: T.class_of(Matchers::Base)).returns(T::Boolean) }
|
43
|
+
def invalid_name?(matcher_type)
|
44
|
+
return true unless matcher_type.respond_to?(:matcher_name)
|
45
|
+
name = matcher_type.matcher_name
|
46
|
+
|
47
|
+
!name.respond_to?(:to_sym) || name.to_sym.inspect.start_with?(":\"")
|
48
|
+
end
|
49
|
+
|
50
|
+
sig { params(matcher_type: T.class_of(Matchers::Base)).returns(T::Boolean) }
|
51
|
+
def invalid_match?(matcher_type)
|
52
|
+
params = @grabs_original_method_parameters.grab(matcher_type.instance_method(:match?))
|
53
|
+
params.size > 1 || ![:req, :opt].include?(T.unsafe(params).first[0])
|
54
|
+
rescue NameError
|
55
|
+
true
|
56
|
+
end
|
57
|
+
|
58
|
+
sig { params(matcher_type: T.class_of(Matchers::Base)).returns(T::Boolean) }
|
59
|
+
def invalid_flag?(matcher_type)
|
60
|
+
!matcher_type.instance_method(:is_mocktail_matcher?)
|
61
|
+
rescue NameError
|
62
|
+
true
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,31 @@
|
|
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
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# typed: strict
|
2
|
+
|
3
|
+
module Mocktail
|
4
|
+
class ReplacesNext
|
5
|
+
extend T::Sig
|
6
|
+
|
7
|
+
sig { void }
|
8
|
+
def initialize
|
9
|
+
@top_shelf = T.let(TopShelf.instance, TopShelf)
|
10
|
+
@redefines_new = T.let(RedefinesNew.new, RedefinesNew)
|
11
|
+
@imitates_type = T.let(ImitatesType.new, ImitatesType)
|
12
|
+
end
|
13
|
+
|
14
|
+
sig {
|
15
|
+
type_parameters(:T)
|
16
|
+
.params(type: T::Class[T.all(T.type_parameter(:T), Object)])
|
17
|
+
.returns(T.type_parameter(:T))
|
18
|
+
}
|
19
|
+
def replace_once(type)
|
20
|
+
replace(type, 1).fetch(0)
|
21
|
+
end
|
22
|
+
|
23
|
+
sig {
|
24
|
+
type_parameters(:T)
|
25
|
+
.params(type: T::Class[T.all(T.type_parameter(:T), Object)], count: Integer)
|
26
|
+
.returns(T::Array[T.type_parameter(:T)])
|
27
|
+
}
|
28
|
+
def replace(type, count)
|
29
|
+
raise UnsupportedMocktail.new("Mocktail.of_next() only supports classes") unless T.unsafe(type).is_a?(Class)
|
30
|
+
|
31
|
+
mocktails = count.times.map { @imitates_type.imitate(type) }
|
32
|
+
|
33
|
+
@top_shelf.register_of_next_replacement!(type)
|
34
|
+
@redefines_new.redefine(type)
|
35
|
+
mocktails.reverse_each do |mocktail|
|
36
|
+
Mocktail.stubs(
|
37
|
+
ignore_extra_args: true,
|
38
|
+
ignore_block: true,
|
39
|
+
ignore_arity: true,
|
40
|
+
times: 1
|
41
|
+
) {
|
42
|
+
type.new
|
43
|
+
}.with {
|
44
|
+
if mocktail == mocktails.last
|
45
|
+
@top_shelf.unregister_of_next_replacement!(type)
|
46
|
+
end
|
47
|
+
|
48
|
+
mocktail
|
49
|
+
}
|
50
|
+
end
|
51
|
+
|
52
|
+
mocktails
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,32 @@
|
|
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
|
@@ -0,0 +1,80 @@
|
|
1
|
+
# typed: strict
|
2
|
+
|
3
|
+
module Mocktail
|
4
|
+
class RedefinesSingletonMethods
|
5
|
+
extend T::Sig
|
6
|
+
|
7
|
+
sig { void }
|
8
|
+
def initialize
|
9
|
+
@handles_dry_call = T.let(HandlesDryCall.new, HandlesDryCall)
|
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
|
+
return unless type_replacement.replacement_methods.nil?
|
16
|
+
|
17
|
+
type_replacement.original_methods = type.singleton_methods.map { |name|
|
18
|
+
type.method(name)
|
19
|
+
}.reject { |method| sorbet_method_hook?(method) } - [type_replacement.replacement_new]
|
20
|
+
|
21
|
+
declare_singleton_method_missing_errors!(type)
|
22
|
+
handles_dry_call = @handles_dry_call
|
23
|
+
type_replacement.replacement_methods = type_replacement.original_methods&.map { |original_method|
|
24
|
+
type.singleton_class.send(:undef_method, original_method.name)
|
25
|
+
type.define_singleton_method original_method.name, ->(*args, **kwargs, &block) {
|
26
|
+
if TopShelf.instance.singleton_methods_replaced?(type)
|
27
|
+
handles_dry_call.handle(Call.new(
|
28
|
+
singleton: true,
|
29
|
+
double: type,
|
30
|
+
original_type: type,
|
31
|
+
dry_type: type,
|
32
|
+
method: original_method.name,
|
33
|
+
original_method: original_method,
|
34
|
+
args: args,
|
35
|
+
kwargs: kwargs,
|
36
|
+
block: block
|
37
|
+
))
|
38
|
+
else
|
39
|
+
original_method.call(*args, **kwargs, &block)
|
40
|
+
end
|
41
|
+
}
|
42
|
+
type.singleton_method(original_method.name)
|
43
|
+
}
|
44
|
+
end
|
45
|
+
|
46
|
+
sig { params(type: T.any(T::Class[T.anything], Module)).void }
|
47
|
+
def declare_singleton_method_missing_errors!(type)
|
48
|
+
return if type.singleton_methods.include?(:method_missing)
|
49
|
+
|
50
|
+
raises_neato_no_method_error = RaisesNeatoNoMethodError.new
|
51
|
+
type.define_singleton_method :method_missing,
|
52
|
+
->(name, *args, **kwargs, &block) {
|
53
|
+
raises_neato_no_method_error.call(
|
54
|
+
Call.new(
|
55
|
+
singleton: true,
|
56
|
+
double: self,
|
57
|
+
original_type: type,
|
58
|
+
dry_type: self.class,
|
59
|
+
method: name,
|
60
|
+
original_method: nil,
|
61
|
+
args: args,
|
62
|
+
kwargs: kwargs,
|
63
|
+
block: block
|
64
|
+
)
|
65
|
+
)
|
66
|
+
}
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
sig { params(method: Method).returns(T::Boolean) }
|
72
|
+
def sorbet_method_hook?(method)
|
73
|
+
[
|
74
|
+
T::Sig,
|
75
|
+
T::Private::Methods::MethodHooks,
|
76
|
+
T::Private::Methods::SingletonMethodHooks
|
77
|
+
].include?(method.owner)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# typed: strict
|
2
|
+
|
3
|
+
module Mocktail
|
4
|
+
class RunsSorbetSigBlocksBeforeReplacement
|
5
|
+
extend T::Sig
|
6
|
+
|
7
|
+
# This is necessary because when Sorbet runs a sig block of a singleton
|
8
|
+
# method, it has the net effect of unwrapping/redefining the method. If
|
9
|
+
# we try to use Mocktail.replace(Foo) and Foo.bar has a Sorbet sig block,
|
10
|
+
# then we'll end up with three "versions" of the same method and no way
|
11
|
+
# to keep straight which one == which:
|
12
|
+
#
|
13
|
+
# A - Foo.bar, as defined in the original class
|
14
|
+
# B - Foo.bar, as redefined by RedefinesSingletonMethods
|
15
|
+
# C - Foo.bar, as wrapped by sorbet-runtime
|
16
|
+
#
|
17
|
+
# Initially, Foo.method(:bar) would == C, but after the type
|
18
|
+
# replacement, it would == B (with a reference back to C as the original),
|
19
|
+
# but after handling a single dry call, our invocation of
|
20
|
+
# GrabsOriginalMethodParameters.grab(Foo.method(:bar)) would invoke the
|
21
|
+
# Sorbet `sig` block, which has the net effect of redefining the method back
|
22
|
+
# to A.
|
23
|
+
#
|
24
|
+
# It's very fun and confusing and a great time.
|
25
|
+
sig { params(type: T.any(T::Class[T.anything], Module)).void }
|
26
|
+
def run(type)
|
27
|
+
return unless defined?(T::Private::Methods)
|
28
|
+
|
29
|
+
type.singleton_methods.each do |method_name|
|
30
|
+
method = type.method(method_name)
|
31
|
+
|
32
|
+
# Again: calling this for the side effect of running the sig block
|
33
|
+
#
|
34
|
+
# https://github.com/sorbet/sorbet/blob/master/gems/sorbet-runtime/lib/types/private/methods/_methods.rb#L111
|
35
|
+
T::Private::Methods.signature_for_method(method)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,36 @@
|
|
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
|
@@ -0,0 +1,18 @@
|
|
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
|
@@ -0,0 +1,22 @@
|
|
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
|
@@ -0,0 +1,39 @@
|
|
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
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# typed: strict
|
2
|
+
|
3
|
+
require_relative "bind"
|
4
|
+
|
5
|
+
module Mocktail
|
6
|
+
class DeterminesMatchingCalls
|
7
|
+
extend T::Sig
|
8
|
+
|
9
|
+
sig { params(real_call: Call, demo_call: Call, demo_config: DemoConfig).returns(T::Boolean) }
|
10
|
+
def determine(real_call, demo_call, demo_config)
|
11
|
+
T.cast(Bind.call(real_call.double, :==, demo_call.double), T::Boolean) &&
|
12
|
+
real_call.method == demo_call.method &&
|
13
|
+
|
14
|
+
# Matcher implementation will replace this:
|
15
|
+
args_match?(real_call.args, demo_call.args, demo_config.ignore_extra_args) &&
|
16
|
+
kwargs_match?(real_call.kwargs, demo_call.kwargs, demo_config.ignore_extra_args) &&
|
17
|
+
blocks_match?(real_call.block, demo_call.block, demo_config.ignore_block)
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
sig { params(real_args: T::Array[T.untyped], demo_args: T::Array[T.untyped], ignore_extra_args: T::Boolean).returns(T::Boolean) }
|
23
|
+
def args_match?(real_args, demo_args, ignore_extra_args)
|
24
|
+
# Guard clause for performance:
|
25
|
+
return true if ignore_extra_args && demo_args.empty?
|
26
|
+
|
27
|
+
(
|
28
|
+
real_args.size == demo_args.size ||
|
29
|
+
(ignore_extra_args && real_args.size >= demo_args.size)
|
30
|
+
) &&
|
31
|
+
demo_args.each.with_index.all? { |demo_arg, i|
|
32
|
+
match?(real_args[i], demo_arg)
|
33
|
+
}
|
34
|
+
end
|
35
|
+
|
36
|
+
sig { params(real_kwargs: T::Hash[Symbol, T.untyped], demo_kwargs: T::Hash[Symbol, T.untyped], ignore_extra_args: T::Boolean).returns(T::Boolean) }
|
37
|
+
def kwargs_match?(real_kwargs, demo_kwargs, ignore_extra_args)
|
38
|
+
return true if ignore_extra_args && demo_kwargs.empty?
|
39
|
+
|
40
|
+
(
|
41
|
+
real_kwargs.size == demo_kwargs.size ||
|
42
|
+
(ignore_extra_args && real_kwargs.size >= demo_kwargs.size)
|
43
|
+
) &&
|
44
|
+
demo_kwargs.all? { |key, demo_val|
|
45
|
+
real_kwargs.key?(key) && match?(real_kwargs[key], demo_val)
|
46
|
+
}
|
47
|
+
end
|
48
|
+
|
49
|
+
sig { params(real_block: T.nilable(Proc), demo_block: T.nilable(Proc), ignore_block: T::Boolean).returns(T::Boolean) }
|
50
|
+
def blocks_match?(real_block, demo_block, ignore_block)
|
51
|
+
!!(ignore_block ||
|
52
|
+
(real_block.nil? && demo_block.nil?) ||
|
53
|
+
(
|
54
|
+
real_block && demo_block &&
|
55
|
+
(
|
56
|
+
demo_block == real_block ||
|
57
|
+
demo_block.call(real_block)
|
58
|
+
)
|
59
|
+
))
|
60
|
+
end
|
61
|
+
|
62
|
+
sig { params(real_arg: T.untyped, demo_arg: T.untyped).returns(T::Boolean) }
|
63
|
+
def match?(real_arg, demo_arg)
|
64
|
+
if Bind.call(demo_arg, :respond_to?, :is_mocktail_matcher?) &&
|
65
|
+
demo_arg.is_mocktail_matcher?
|
66
|
+
demo_arg.match?(real_arg)
|
67
|
+
else
|
68
|
+
Bind.call(demo_arg, :==, real_arg)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
# typed: strict
|
2
|
+
|
3
|
+
module Mocktail
|
4
|
+
class StringifiesCall
|
5
|
+
extend T::Sig
|
6
|
+
|
7
|
+
sig { params(call: Call, anonymous_blocks: T::Boolean, always_parens: T::Boolean).returns(String) }
|
8
|
+
def stringify(call, anonymous_blocks: false, always_parens: false)
|
9
|
+
"#{call.method}#{args_to_s(call, parens: always_parens)}#{blockify(call.block, anonymous: anonymous_blocks)}"
|
10
|
+
end
|
11
|
+
|
12
|
+
sig { params(calls: T::Array[Call], nonzero_message: String, zero_message: String, anonymous_blocks: T::Boolean, always_parens: T::Boolean).returns(String) }
|
13
|
+
def stringify_multiple(calls, nonzero_message:, zero_message:,
|
14
|
+
anonymous_blocks: false, always_parens: false)
|
15
|
+
|
16
|
+
if calls.empty?
|
17
|
+
"#{zero_message}.\n"
|
18
|
+
else
|
19
|
+
<<~MSG
|
20
|
+
#{nonzero_message}:
|
21
|
+
|
22
|
+
#{calls.map { |call| " " + stringify(call) }.join("\n\n")}
|
23
|
+
MSG
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
sig { params(call: Call, parens: T::Boolean).returns(T.nilable(String)) }
|
30
|
+
def args_to_s(call, parens: true)
|
31
|
+
args_lists = [
|
32
|
+
argify(call.args),
|
33
|
+
kwargify(call.kwargs),
|
34
|
+
lambdafy(call.block)
|
35
|
+
].compact
|
36
|
+
|
37
|
+
if !args_lists.empty?
|
38
|
+
"(#{args_lists.join(", ")})"
|
39
|
+
elsif parens
|
40
|
+
"()"
|
41
|
+
else
|
42
|
+
""
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
sig { params(args: T::Array[Object]).returns(T.nilable(String)) }
|
47
|
+
def argify(args)
|
48
|
+
return unless !args.empty?
|
49
|
+
args.map(&:inspect).join(", ")
|
50
|
+
end
|
51
|
+
|
52
|
+
sig { params(kwargs: T::Hash[Symbol, Object]).returns(T.nilable(String)) }
|
53
|
+
def kwargify(kwargs)
|
54
|
+
return unless !kwargs.empty?
|
55
|
+
kwargs.map { |key, val| "#{key}: #{val.inspect}" }.join(", ")
|
56
|
+
end
|
57
|
+
|
58
|
+
sig { params(block: T.nilable(Proc)).returns(T.nilable(String)) }
|
59
|
+
def lambdafy(block)
|
60
|
+
return unless block&.lambda?
|
61
|
+
"&lambda[#{source_locationify(block)}]"
|
62
|
+
end
|
63
|
+
|
64
|
+
sig { params(block: T.nilable(Proc), anonymous: T::Boolean).returns(T.nilable(String)) }
|
65
|
+
def blockify(block, anonymous:)
|
66
|
+
return unless block && !block.lambda?
|
67
|
+
|
68
|
+
if anonymous
|
69
|
+
" {…}"
|
70
|
+
else
|
71
|
+
" { Proc at #{source_locationify(block)} }"
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
sig { params(block: Proc).returns(String) }
|
76
|
+
def source_locationify(block)
|
77
|
+
"#{strip_pwd(block.source_location[0])}:#{block.source_location[1]}"
|
78
|
+
end
|
79
|
+
|
80
|
+
sig { params(path: String).returns(String) }
|
81
|
+
def strip_pwd(path)
|
82
|
+
path.gsub(Dir.pwd + File::SEPARATOR, "")
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,16 @@
|
|
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
|
@@ -0,0 +1,27 @@
|
|
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
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# typed: strict
|
2
|
+
|
3
|
+
module Mocktail
|
4
|
+
class RecreatesMessage
|
5
|
+
extend T::Sig
|
6
|
+
|
7
|
+
sig { params(signature: Signature).returns(String) }
|
8
|
+
def recreate(signature)
|
9
|
+
req_args = signature.positional_params.required.size
|
10
|
+
allowed_args = signature.positional_params.allowed.size
|
11
|
+
rest_args = signature.positional_params.rest?
|
12
|
+
req_kwargs = signature.keyword_params.required
|
13
|
+
|
14
|
+
if signature.positional_args.size < req_args || (!rest_args && signature.positional_args.size > allowed_args)
|
15
|
+
expected_desc = if rest_args
|
16
|
+
"#{req_args}+"
|
17
|
+
elsif allowed_args != req_args
|
18
|
+
"#{req_args}..#{allowed_args}"
|
19
|
+
else
|
20
|
+
req_args.to_s
|
21
|
+
end
|
22
|
+
|
23
|
+
"wrong number of arguments (given #{signature.positional_args.size}, expected #{expected_desc}#{"; required keyword#{"s" if req_kwargs.size > 1}: #{req_kwargs.join(", ")}" unless req_kwargs.empty?})"
|
24
|
+
|
25
|
+
elsif !(missing_kwargs = req_kwargs.reject { |name| signature.keyword_args.key?(name) }).empty?
|
26
|
+
"missing keyword#{"s" if missing_kwargs.size > 1}: #{missing_kwargs.map { |name| name.inspect }.join(", ")}"
|
27
|
+
elsif !(unknown_kwargs = signature.keyword_args.keys.reject { |name| signature.keyword_params.all.include?(name) }).empty?
|
28
|
+
"unknown keyword#{"s" if unknown_kwargs.size > 1}: #{unknown_kwargs.map { |name| name.inspect }.join(", ")}"
|
29
|
+
else
|
30
|
+
"unknown cause (this is probably a bug in Mocktail)"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|