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.
Files changed (183) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/main.yml +6 -5
  3. data/.gitignore +3 -0
  4. data/.standard.yml +8 -0
  5. data/CHANGELOG.md +14 -0
  6. data/Gemfile +6 -1
  7. data/Gemfile.lock +98 -25
  8. data/README.md +18 -922
  9. data/Rakefile +0 -1
  10. data/bin/console +1 -2
  11. data/bin/tapioca +29 -0
  12. data/lib/mocktail/collects_calls.rb +2 -0
  13. data/lib/mocktail/debug.rb +13 -10
  14. data/lib/mocktail/dsl.rb +2 -0
  15. data/lib/mocktail/errors.rb +2 -0
  16. data/lib/mocktail/explains_nils.rb +2 -0
  17. data/lib/mocktail/explains_thing.rb +7 -4
  18. data/lib/mocktail/grabs_original_method_parameters.rb +30 -0
  19. data/lib/mocktail/handles_dry_call/fulfills_stubbing/describes_unsatisfied_stubbing.rb +3 -1
  20. data/lib/mocktail/handles_dry_call/fulfills_stubbing/finds_satisfaction.rb +5 -1
  21. data/lib/mocktail/handles_dry_call/fulfills_stubbing.rb +2 -0
  22. data/lib/mocktail/handles_dry_call/logs_call.rb +2 -0
  23. data/lib/mocktail/handles_dry_call/validates_arguments.rb +6 -4
  24. data/lib/mocktail/handles_dry_call.rb +2 -0
  25. data/lib/mocktail/handles_dry_new_call.rb +2 -0
  26. data/lib/mocktail/imitates_type/ensures_imitation_support.rb +2 -0
  27. data/lib/mocktail/imitates_type/makes_double/declares_dry_class/reconstructs_call.rb +4 -1
  28. data/lib/mocktail/imitates_type/makes_double/declares_dry_class.rb +32 -20
  29. data/lib/mocktail/imitates_type/makes_double/gathers_fakeable_instance_methods.rb +2 -0
  30. data/lib/mocktail/imitates_type/makes_double.rb +3 -0
  31. data/lib/mocktail/imitates_type.rb +3 -1
  32. data/lib/mocktail/initialize_based_on_type_system_mode_switching.rb +9 -0
  33. data/lib/mocktail/initializes_mocktail.rb +5 -0
  34. data/lib/mocktail/matcher_presentation.rb +4 -2
  35. data/lib/mocktail/matchers/any.rb +4 -3
  36. data/lib/mocktail/matchers/base.rb +10 -2
  37. data/lib/mocktail/matchers/captor.rb +9 -0
  38. data/lib/mocktail/matchers/includes.rb +2 -0
  39. data/lib/mocktail/matchers/includes_hash.rb +9 -0
  40. data/lib/mocktail/matchers/includes_key.rb +9 -0
  41. data/lib/mocktail/matchers/includes_string.rb +9 -0
  42. data/lib/mocktail/matchers/is_a.rb +2 -0
  43. data/lib/mocktail/matchers/matches.rb +2 -0
  44. data/lib/mocktail/matchers/not.rb +2 -0
  45. data/lib/mocktail/matchers/numeric.rb +5 -4
  46. data/lib/mocktail/matchers/that.rb +2 -0
  47. data/lib/mocktail/matchers.rb +3 -0
  48. data/lib/mocktail/raises_neato_no_method_error.rb +2 -0
  49. data/lib/mocktail/records_demonstration.rb +2 -0
  50. data/lib/mocktail/registers_matcher.rb +8 -3
  51. data/lib/mocktail/registers_stubbing.rb +2 -0
  52. data/lib/mocktail/replaces_next.rb +7 -1
  53. data/lib/mocktail/replaces_type/redefines_new.rb +3 -1
  54. data/lib/mocktail/replaces_type/redefines_singleton_methods.rb +14 -2
  55. data/lib/mocktail/replaces_type/runs_sorbet_sig_blocks_before_replacement.rb +37 -0
  56. data/lib/mocktail/replaces_type.rb +6 -0
  57. data/lib/mocktail/resets_state.rb +2 -0
  58. data/lib/mocktail/share/bind.rb +7 -5
  59. data/lib/mocktail/share/cleans_backtrace.rb +3 -5
  60. data/lib/mocktail/share/creates_identifier.rb +16 -9
  61. data/lib/mocktail/share/determines_matching_calls.rb +4 -2
  62. data/lib/mocktail/share/stringifies_call.rb +6 -2
  63. data/lib/mocktail/share/stringifies_method_name.rb +3 -1
  64. data/lib/mocktail/simulates_argument_error/reconciles_args_with_params.rb +2 -0
  65. data/lib/mocktail/simulates_argument_error/recreates_message.rb +2 -0
  66. data/lib/mocktail/simulates_argument_error/transforms_params.rb +15 -8
  67. data/lib/mocktail/simulates_argument_error.rb +2 -0
  68. data/lib/mocktail/sorbet/mocktail/collects_calls.rb +18 -0
  69. data/lib/mocktail/sorbet/mocktail/debug.rb +54 -0
  70. data/lib/mocktail/sorbet/mocktail/dsl.rb +46 -0
  71. data/lib/mocktail/sorbet/mocktail/errors.rb +19 -0
  72. data/lib/mocktail/sorbet/mocktail/explains_nils.rb +41 -0
  73. data/lib/mocktail/sorbet/mocktail/explains_thing.rb +137 -0
  74. data/lib/mocktail/sorbet/mocktail/grabs_original_method_parameters.rb +33 -0
  75. data/lib/mocktail/sorbet/mocktail/handles_dry_call/fulfills_stubbing/describes_unsatisfied_stubbing.rb +27 -0
  76. data/lib/mocktail/sorbet/mocktail/handles_dry_call/fulfills_stubbing/finds_satisfaction.rb +24 -0
  77. data/lib/mocktail/sorbet/mocktail/handles_dry_call/fulfills_stubbing.rb +45 -0
  78. data/lib/mocktail/sorbet/mocktail/handles_dry_call/logs_call.rb +12 -0
  79. data/lib/mocktail/sorbet/mocktail/handles_dry_call/validates_arguments.rb +45 -0
  80. data/lib/mocktail/sorbet/mocktail/handles_dry_call.rb +25 -0
  81. data/lib/mocktail/sorbet/mocktail/handles_dry_new_call.rb +42 -0
  82. data/lib/mocktail/sorbet/mocktail/imitates_type/ensures_imitation_support.rb +16 -0
  83. data/lib/mocktail/sorbet/mocktail/imitates_type/makes_double/declares_dry_class/reconstructs_call.rb +73 -0
  84. data/lib/mocktail/sorbet/mocktail/imitates_type/makes_double/declares_dry_class.rb +136 -0
  85. data/lib/mocktail/sorbet/mocktail/imitates_type/makes_double/gathers_fakeable_instance_methods.rb +28 -0
  86. data/lib/mocktail/sorbet/mocktail/imitates_type/makes_double.rb +29 -0
  87. data/lib/mocktail/sorbet/mocktail/imitates_type.rb +29 -0
  88. data/lib/mocktail/sorbet/mocktail/initialize_based_on_type_system_mode_switching.rb +11 -0
  89. data/lib/mocktail/sorbet/mocktail/initializes_mocktail.rb +25 -0
  90. data/lib/mocktail/sorbet/mocktail/matcher_presentation.rb +21 -0
  91. data/lib/mocktail/sorbet/mocktail/matchers/any.rb +27 -0
  92. data/lib/mocktail/sorbet/mocktail/matchers/base.rb +39 -0
  93. data/lib/mocktail/sorbet/mocktail/matchers/captor.rb +76 -0
  94. data/lib/mocktail/sorbet/mocktail/matchers/includes.rb +32 -0
  95. data/lib/mocktail/sorbet/mocktail/matchers/includes_hash.rb +12 -0
  96. data/lib/mocktail/sorbet/mocktail/matchers/includes_key.rb +12 -0
  97. data/lib/mocktail/sorbet/mocktail/matchers/includes_string.rb +12 -0
  98. data/lib/mocktail/sorbet/mocktail/matchers/is_a.rb +17 -0
  99. data/lib/mocktail/sorbet/mocktail/matchers/matches.rb +19 -0
  100. data/lib/mocktail/sorbet/mocktail/matchers/not.rb +17 -0
  101. data/lib/mocktail/sorbet/mocktail/matchers/numeric.rb +27 -0
  102. data/lib/mocktail/sorbet/mocktail/matchers/that.rb +32 -0
  103. data/lib/mocktail/sorbet/mocktail/matchers.rb +19 -0
  104. data/lib/mocktail/sorbet/mocktail/raises_neato_no_method_error.rb +93 -0
  105. data/lib/mocktail/sorbet/mocktail/records_demonstration.rb +43 -0
  106. data/lib/mocktail/sorbet/mocktail/registers_matcher.rb +65 -0
  107. data/lib/mocktail/sorbet/mocktail/registers_stubbing.rb +31 -0
  108. data/lib/mocktail/sorbet/mocktail/replaces_next.rb +55 -0
  109. data/lib/mocktail/sorbet/mocktail/replaces_type/redefines_new.rb +32 -0
  110. data/lib/mocktail/sorbet/mocktail/replaces_type/redefines_singleton_methods.rb +80 -0
  111. data/lib/mocktail/sorbet/mocktail/replaces_type/runs_sorbet_sig_blocks_before_replacement.rb +39 -0
  112. data/lib/mocktail/sorbet/mocktail/replaces_type.rb +36 -0
  113. data/lib/mocktail/sorbet/mocktail/resets_state.rb +14 -0
  114. data/lib/mocktail/sorbet/mocktail/share/bind.rb +18 -0
  115. data/lib/mocktail/sorbet/mocktail/share/cleans_backtrace.rb +22 -0
  116. data/lib/mocktail/sorbet/mocktail/share/creates_identifier.rb +39 -0
  117. data/lib/mocktail/sorbet/mocktail/share/determines_matching_calls.rb +72 -0
  118. data/lib/mocktail/sorbet/mocktail/share/stringifies_call.rb +85 -0
  119. data/lib/mocktail/sorbet/mocktail/share/stringifies_method_name.rb +16 -0
  120. data/lib/mocktail/sorbet/mocktail/simulates_argument_error/reconciles_args_with_params.rb +27 -0
  121. data/lib/mocktail/sorbet/mocktail/simulates_argument_error/recreates_message.rb +34 -0
  122. data/lib/mocktail/sorbet/mocktail/simulates_argument_error/transforms_params.rb +58 -0
  123. data/lib/mocktail/sorbet/mocktail/simulates_argument_error.rb +36 -0
  124. data/lib/mocktail/sorbet/mocktail/sorbet.rb +3 -0
  125. data/lib/mocktail/sorbet/mocktail/stringifies_method_signature.rb +53 -0
  126. data/lib/mocktail/sorbet/mocktail/typed.rb +5 -0
  127. data/lib/mocktail/sorbet/mocktail/value/cabinet.rb +91 -0
  128. data/lib/mocktail/sorbet/mocktail/value/call.rb +51 -0
  129. data/lib/mocktail/sorbet/mocktail/value/demo_config.rb +10 -0
  130. data/lib/mocktail/sorbet/mocktail/value/double.rb +10 -0
  131. data/lib/mocktail/sorbet/mocktail/value/double_data.rb +15 -0
  132. data/lib/mocktail/sorbet/mocktail/value/explanation.rb +68 -0
  133. data/lib/mocktail/sorbet/mocktail/value/explanation_data.rb +19 -0
  134. data/lib/mocktail/sorbet/mocktail/value/fake_method_data.rb +11 -0
  135. data/lib/mocktail/sorbet/mocktail/value/matcher_registry.rb +27 -0
  136. data/lib/mocktail/sorbet/mocktail/value/no_explanation_data.rb +20 -0
  137. data/lib/mocktail/sorbet/mocktail/value/signature.rb +35 -0
  138. data/lib/mocktail/sorbet/mocktail/value/stubbing.rb +26 -0
  139. data/lib/mocktail/sorbet/mocktail/value/top_shelf.rb +79 -0
  140. data/lib/mocktail/sorbet/mocktail/value/type_replacement.rb +11 -0
  141. data/lib/mocktail/sorbet/mocktail/value/type_replacement_data.rb +19 -0
  142. data/lib/mocktail/sorbet/mocktail/value/unsatisfying_call.rb +9 -0
  143. data/lib/mocktail/sorbet/mocktail/value/unsatisfying_call_explanation.rb +24 -0
  144. data/lib/mocktail/sorbet/mocktail/value.rb +19 -0
  145. data/lib/mocktail/sorbet/mocktail/verifies_call/finds_verifiable_calls.rb +21 -0
  146. data/lib/mocktail/sorbet/mocktail/verifies_call/raises_verification_error/gathers_calls_of_method.rb +15 -0
  147. data/lib/mocktail/sorbet/mocktail/verifies_call/raises_verification_error.rb +74 -0
  148. data/lib/mocktail/sorbet/mocktail/verifies_call.rb +37 -0
  149. data/lib/mocktail/sorbet/mocktail/version.rb +12 -0
  150. data/lib/mocktail/sorbet/mocktail.rb +154 -0
  151. data/lib/mocktail/sorbet.rb +1 -0
  152. data/lib/mocktail/stringifies_method_signature.rb +2 -0
  153. data/lib/mocktail/typed.rb +3 -0
  154. data/lib/mocktail/value/cabinet.rb +8 -1
  155. data/lib/mocktail/value/call.rb +44 -12
  156. data/lib/mocktail/value/demo_config.rb +6 -7
  157. data/lib/mocktail/value/double.rb +6 -7
  158. data/lib/mocktail/value/double_data.rb +11 -7
  159. data/lib/mocktail/value/explanation.rb +28 -3
  160. data/lib/mocktail/value/explanation_data.rb +14 -0
  161. data/lib/mocktail/value/fake_method_data.rb +7 -6
  162. data/lib/mocktail/value/matcher_registry.rb +2 -0
  163. data/lib/mocktail/value/no_explanation_data.rb +16 -0
  164. data/lib/mocktail/value/signature.rb +19 -27
  165. data/lib/mocktail/value/stubbing.rb +11 -12
  166. data/lib/mocktail/value/top_shelf.rb +5 -0
  167. data/lib/mocktail/value/type_replacement.rb +7 -8
  168. data/lib/mocktail/value/type_replacement_data.rb +10 -7
  169. data/lib/mocktail/value/unsatisfying_call.rb +5 -6
  170. data/lib/mocktail/value/unsatisfying_call_explanation.rb +18 -0
  171. data/lib/mocktail/value.rb +5 -2
  172. data/lib/mocktail/verifies_call/finds_verifiable_calls.rb +2 -0
  173. data/lib/mocktail/verifies_call/raises_verification_error/gathers_calls_of_method.rb +2 -0
  174. data/lib/mocktail/verifies_call/raises_verification_error.rb +2 -0
  175. data/lib/mocktail/verifies_call.rb +3 -0
  176. data/lib/mocktail/version.rb +8 -1
  177. data/lib/mocktail.rb +46 -5
  178. data/mocktail.gemspec +8 -4
  179. data/rbi/mocktail-pregenerated.rbi +1865 -0
  180. data/rbi/mocktail.rbi +77 -0
  181. data/rbi/sorbet-runtime.rbi +29 -0
  182. data/spoom_report.html +1248 -0
  183. metadata +130 -3
@@ -0,0 +1,41 @@
1
+ # typed: strict
2
+
3
+ require_relative "share/stringifies_method_name"
4
+ require_relative "share/stringifies_call"
5
+
6
+ module Mocktail
7
+ class ExplainsNils
8
+ extend T::Sig
9
+
10
+ sig { void }
11
+ def initialize
12
+ @stringifies_method_name = T.let(StringifiesMethodName.new, StringifiesMethodName)
13
+ @stringifies_call = T.let(StringifiesCall.new, StringifiesCall)
14
+ end
15
+
16
+ sig { returns(T::Array[UnsatisfyingCallExplanation]) }
17
+ def explain
18
+ Mocktail.cabinet.unsatisfying_calls.map { |unsatisfying_call|
19
+ dry_call = unsatisfying_call.call
20
+ other_stubbings = unsatisfying_call.other_stubbings
21
+
22
+ UnsatisfyingCallExplanation.new(unsatisfying_call, <<~MSG)
23
+ `nil' was returned by a mocked `#{@stringifies_method_name.stringify(dry_call)}' method
24
+ because none of its configured stubbings were satisfied.
25
+
26
+ The actual call:
27
+
28
+ #{@stringifies_call.stringify(dry_call, always_parens: true)}
29
+
30
+ The call site:
31
+
32
+ #{unsatisfying_call.backtrace.first}
33
+
34
+ #{@stringifies_call.stringify_multiple(other_stubbings.map(&:recording),
35
+ nonzero_message: "Stubbings configured prior to this call but not satisfied by it",
36
+ zero_message: "No stubbings were configured on this method")}
37
+ MSG
38
+ }
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,137 @@
1
+ # typed: strict
2
+
3
+ require_relative "share/stringifies_method_name"
4
+ require_relative "share/stringifies_call"
5
+
6
+ module Mocktail
7
+ class ExplainsThing
8
+ extend T::Sig
9
+
10
+ sig { void }
11
+ def initialize
12
+ @stringifies_method_name = T.let(StringifiesMethodName.new, StringifiesMethodName)
13
+ @stringifies_call = T.let(StringifiesCall.new, StringifiesCall)
14
+ end
15
+
16
+ sig { params(thing: Object).returns(Explanation) }
17
+ def explain(thing)
18
+ if (double = Mocktail.cabinet.double_for_instance(thing))
19
+ double_explanation(double)
20
+ elsif (thing.is_a?(Module) || thing.is_a?(Class)) &&
21
+ (type_replacement = TopShelf.instance.type_replacement_if_exists_for(thing))
22
+ replaced_type_explanation(type_replacement)
23
+ elsif (fake_method_explanation = fake_method_explanation_for(thing))
24
+ fake_method_explanation
25
+ else
26
+ no_explanation(thing)
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ sig { params(thing: Object).returns(T.nilable(FakeMethodExplanation)) }
33
+ def fake_method_explanation_for(thing)
34
+ return unless thing.is_a?(Method)
35
+ method = thing
36
+ receiver = thing.receiver
37
+
38
+ receiver_data = if (double = Mocktail.cabinet.double_for_instance(receiver))
39
+ data_for_double(double)
40
+ elsif (type_replacement = TopShelf.instance.type_replacement_if_exists_for(receiver))
41
+ data_for_type_replacement(type_replacement)
42
+ end
43
+
44
+ if receiver_data
45
+ FakeMethodExplanation.new(FakeMethodData.new(
46
+ receiver: receiver,
47
+ calls: receiver_data.calls,
48
+ stubbings: receiver_data.stubbings
49
+ ), describe_dry_method(receiver_data, method.name))
50
+ end
51
+ end
52
+
53
+ sig { params(double: Double).returns(DoubleData) }
54
+ def data_for_double(double)
55
+ DoubleData.new(
56
+ type: double.original_type,
57
+ double: double.dry_instance,
58
+ calls: Mocktail.cabinet.calls_for_double(double),
59
+ stubbings: Mocktail.cabinet.stubbings_for_double(double)
60
+ )
61
+ end
62
+
63
+ sig { params(double: Double).returns(DoubleExplanation) }
64
+ def double_explanation(double)
65
+ double_data = data_for_double(double)
66
+
67
+ DoubleExplanation.new(double_data, <<~MSG)
68
+ This is a fake `#{double.original_type.name}' instance.
69
+
70
+ It has these mocked methods:
71
+ #{double.dry_methods.sort.map { |method| " - #{method}" }.join("\n")}
72
+
73
+ #{double.dry_methods.sort.map { |method| describe_dry_method(double_data, method) }.join("\n")}
74
+ MSG
75
+ end
76
+
77
+ sig { params(type_replacement: TypeReplacement).returns(TypeReplacementData) }
78
+ def data_for_type_replacement(type_replacement)
79
+ TypeReplacementData.new(
80
+ type: type_replacement.type,
81
+ replaced_method_names: type_replacement.replacement_methods&.map(&:name)&.sort || [],
82
+ calls: Mocktail.cabinet.calls.select { |call|
83
+ call.double == type_replacement.type
84
+ },
85
+ stubbings: Mocktail.cabinet.stubbings.select { |stubbing|
86
+ stubbing.recording.double == type_replacement.type
87
+ }
88
+ )
89
+ end
90
+
91
+ sig { params(type_replacement: TypeReplacement).returns(ReplacedTypeExplanation) }
92
+ def replaced_type_explanation(type_replacement)
93
+ type_replacement_data = data_for_type_replacement(type_replacement)
94
+
95
+ ReplacedTypeExplanation.new(type_replacement_data, <<~MSG)
96
+ `#{type_replacement.type}' is a #{type_replacement.type.class.to_s.downcase} that has had its methods faked.
97
+
98
+ It has these mocked methods:
99
+ #{type_replacement_data.replaced_method_names.map { |method| " - #{method}" }.join("\n")}
100
+
101
+ #{type_replacement_data.replaced_method_names.map { |method| describe_dry_method(type_replacement_data, method) }.join("\n")}
102
+ MSG
103
+ end
104
+
105
+ sig { params(double_data: T.any(DoubleData, TypeReplacementData), method: Symbol).returns(String) }
106
+ def describe_dry_method(double_data, method)
107
+ method_name = @stringifies_method_name.stringify(Call.new(
108
+ original_type: double_data.type,
109
+ singleton: double_data.type == double_data.double,
110
+ method: method
111
+ ))
112
+
113
+ [
114
+ @stringifies_call.stringify_multiple(
115
+ double_data.stubbings.map(&:recording).select { |call|
116
+ call.method == method
117
+ },
118
+ nonzero_message: "`#{method_name}' stubbings",
119
+ zero_message: "`#{method_name}' has no stubbings"
120
+ ),
121
+ @stringifies_call.stringify_multiple(
122
+ double_data.calls.select { |call|
123
+ call.method == method
124
+ },
125
+ nonzero_message: "`#{method_name}' calls",
126
+ zero_message: "`#{method_name}' has no calls"
127
+ )
128
+ ].join("\n")
129
+ end
130
+
131
+ sig { params(thing: Object).returns(NoExplanation) }
132
+ def no_explanation(thing)
133
+ NoExplanation.new(NoExplanationData.new(thing: thing),
134
+ "Unfortunately, Mocktail doesn't know what this thing is: #{thing.inspect}")
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,33 @@
1
+ # typed: strict
2
+
3
+ module Mocktail
4
+ class GrabsOriginalMethodParameters
5
+ extend T::Sig
6
+
7
+ # Sorbet wraps the original method in a sig wrapper, so we need to unwrap it.
8
+ # The value returned from `owner.instance_method(method_name)` does not have
9
+ # the real parameters values available, as they'll have been erased
10
+ #
11
+ # If the method isn't wrapped by Sorbet, this will return the #instance_method,
12
+ # per usual
13
+ sig { params(method: T.nilable(T.any(UnboundMethod, Method))).returns(T::Array[T::Array[Symbol]]) }
14
+ def grab(method)
15
+ return [] unless method
16
+
17
+ if (wrapped_method = sorbet_wrapped_method(method))
18
+ wrapped_method.parameters
19
+ else
20
+ method.parameters
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ sig { params(method: T.any(UnboundMethod, Method)).returns(T.nilable(T::Private::Methods::Signature)) }
27
+ def sorbet_wrapped_method(method)
28
+ return unless defined?(::T::Private::Methods)
29
+
30
+ T::Private::Methods.signature_for_method(method)
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,27 @@
1
+ # typed: strict
2
+
3
+ require_relative "../../share/cleans_backtrace"
4
+ require_relative "../../share/bind"
5
+
6
+ module Mocktail
7
+ class DescribesUnsatisfiedStubbing
8
+ extend T::Sig
9
+
10
+ sig { void }
11
+ def initialize
12
+ @cleans_backtrace = T.let(CleansBacktrace.new, Mocktail::CleansBacktrace)
13
+ end
14
+
15
+ sig { params(dry_call: Mocktail::Call).returns(Mocktail::UnsatisfyingCall) }
16
+ def describe(dry_call)
17
+ UnsatisfyingCall.new(
18
+ call: dry_call,
19
+ other_stubbings: Mocktail.cabinet.stubbings.select { |stubbing|
20
+ Bind.call(dry_call.double, :==, stubbing.recording.double) &&
21
+ dry_call.method == stubbing.recording.method
22
+ },
23
+ backtrace: @cleans_backtrace.clean(Error.new).backtrace || []
24
+ )
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,24 @@
1
+ # typed: strict
2
+
3
+ require_relative "../../share/determines_matching_calls"
4
+
5
+ module Mocktail
6
+ class FindsSatisfaction
7
+ extend T::Sig
8
+
9
+ sig { void }
10
+ def initialize
11
+ @determines_matching_calls = T.let(DeterminesMatchingCalls.new, Mocktail::DeterminesMatchingCalls)
12
+ end
13
+
14
+ sig { params(dry_call: Call).returns(T.nilable(Stubbing[T.anything])) }
15
+ def find(dry_call)
16
+ Mocktail.cabinet.stubbings.reverse.find { |stubbing|
17
+ demo_config_times = stubbing.demo_config.times
18
+
19
+ @determines_matching_calls.determine(dry_call, stubbing.recording, stubbing.demo_config) &&
20
+ (demo_config_times.nil? || demo_config_times > stubbing.satisfaction_count)
21
+ }
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,45 @@
1
+ # typed: strict
2
+
3
+ require_relative "fulfills_stubbing/finds_satisfaction"
4
+ require_relative "fulfills_stubbing/describes_unsatisfied_stubbing"
5
+
6
+ module Mocktail
7
+ class FulfillsStubbing
8
+ extend T::Sig
9
+
10
+ sig { void }
11
+ def initialize
12
+ @finds_satisfaction = T.let(FindsSatisfaction.new, Mocktail::FindsSatisfaction)
13
+ @describes_unsatisfied_stubbing = T.let(DescribesUnsatisfiedStubbing.new, Mocktail::DescribesUnsatisfiedStubbing)
14
+ end
15
+
16
+ sig { params(dry_call: Call).returns(T.anything) }
17
+ def fulfill(dry_call)
18
+ if (stubbing = satisfaction(dry_call))
19
+ stubbing.satisfied!
20
+ stubbing.effect&.call(dry_call)
21
+ else
22
+ store_unsatisfying_call!(dry_call)
23
+ nil
24
+ end
25
+ end
26
+
27
+ sig { params(dry_call: Call).returns(T.nilable(Stubbing[T.anything])) }
28
+ def satisfaction(dry_call)
29
+ return if Mocktail.cabinet.demonstration_in_progress?
30
+
31
+ @finds_satisfaction.find(dry_call)
32
+ end
33
+
34
+ private
35
+
36
+ sig { params(dry_call: Call).void }
37
+ def store_unsatisfying_call!(dry_call)
38
+ return if Mocktail.cabinet.demonstration_in_progress?
39
+
40
+ Mocktail.cabinet.store_unsatisfying_call(
41
+ @describes_unsatisfied_stubbing.describe(dry_call)
42
+ )
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,12 @@
1
+ # typed: strict
2
+
3
+ module Mocktail
4
+ class LogsCall
5
+ extend T::Sig
6
+
7
+ sig { params(dry_call: Call).void }
8
+ def log(dry_call)
9
+ Mocktail.cabinet.store_call(dry_call)
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,45 @@
1
+ # typed: strict
2
+
3
+ module Mocktail
4
+ class ValidatesArguments
5
+ extend T::Sig
6
+ sig { void }
7
+ def self.disable!
8
+ Thread.current[:mocktail_arity_validation_disabled] = true
9
+ end
10
+
11
+ sig { void }
12
+ def self.enable!
13
+ Thread.current[:mocktail_arity_validation_disabled] = false
14
+ end
15
+
16
+ sig { returns(T::Boolean) }
17
+ def self.disabled?
18
+ !!Thread.current[:mocktail_arity_validation_disabled]
19
+ end
20
+
21
+ sig { params(disable: T.nilable(T::Boolean), blk: T.proc.returns(T.anything)).void }
22
+ def self.optional(disable, &blk)
23
+ return blk.call unless disable
24
+
25
+ disable!
26
+ ret = blk.call
27
+ enable!
28
+ ret
29
+ end
30
+
31
+ sig { void }
32
+ def initialize
33
+ @simulates_argument_error = T.let(SimulatesArgumentError.new, Mocktail::SimulatesArgumentError)
34
+ end
35
+
36
+ sig { params(dry_call: Call).returns(NilClass) }
37
+ def validate(dry_call)
38
+ return if self.class.disabled?
39
+
40
+ if (error = @simulates_argument_error.simulate(dry_call))
41
+ raise error
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,25 @@
1
+ # typed: strict
2
+
3
+ require_relative "handles_dry_call/fulfills_stubbing"
4
+ require_relative "handles_dry_call/logs_call"
5
+ require_relative "handles_dry_call/validates_arguments"
6
+
7
+ module Mocktail
8
+ class HandlesDryCall
9
+ extend T::Sig
10
+
11
+ sig { void }
12
+ def initialize
13
+ @validates_arguments = T.let(ValidatesArguments.new, ValidatesArguments)
14
+ @logs_call = T.let(LogsCall.new, LogsCall)
15
+ @fulfills_stubbing = T.let(FulfillsStubbing.new, FulfillsStubbing)
16
+ end
17
+
18
+ sig { params(dry_call: Call).returns(T.anything) }
19
+ def handle(dry_call)
20
+ @validates_arguments.validate(dry_call)
21
+ @logs_call.log(dry_call)
22
+ @fulfills_stubbing.fulfill(dry_call)
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,42 @@
1
+ # typed: strict
2
+
3
+ module Mocktail
4
+ class HandlesDryNewCall
5
+ extend T::Sig
6
+
7
+ sig { void }
8
+ def initialize
9
+ @validates_arguments = T.let(ValidatesArguments.new, ValidatesArguments)
10
+ @logs_call = T.let(LogsCall.new, LogsCall)
11
+ @fulfills_stubbing = T.let(FulfillsStubbing.new, FulfillsStubbing)
12
+ @imitates_type = T.let(ImitatesType.new, ImitatesType)
13
+ end
14
+
15
+ sig { params(type: T::Class[T.all(T, Object)], args: T::Array[T.anything], kwargs: T::Hash[Symbol, T.anything], block: T.nilable(Proc)).returns(T.anything) }
16
+ def handle(type, args, kwargs, block)
17
+ @validates_arguments.validate(Call.new(
18
+ original_method: type.instance_method(:initialize),
19
+ args: args,
20
+ kwargs: kwargs,
21
+ block: block
22
+ ))
23
+
24
+ new_call = Call.new(
25
+ singleton: true,
26
+ double: type,
27
+ original_type: type,
28
+ dry_type: type,
29
+ method: :new,
30
+ args: args,
31
+ kwargs: kwargs,
32
+ block: block
33
+ )
34
+ @logs_call.log(new_call)
35
+ if @fulfills_stubbing.satisfaction(new_call)
36
+ @fulfills_stubbing.fulfill(new_call)
37
+ else
38
+ @imitates_type.imitate(type)
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,16 @@
1
+ # typed: strict
2
+
3
+ module Mocktail
4
+ class EnsuresImitationSupport
5
+ extend T::Sig
6
+
7
+ sig { params(type: T.any(T::Class[T.anything], Module)).void }
8
+ def ensure(type)
9
+ unless type.is_a?(Class) || type.is_a?(Module)
10
+ raise UnsupportedMocktail.new <<~MSG.tr("\n", " ")
11
+ Mocktail.of() can only mix mocktail instances of modules and classes.
12
+ MSG
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,73 @@
1
+ # typed: strict
2
+
3
+ module Mocktail
4
+ class ReconstructsCall
5
+ extend T::Sig
6
+
7
+ sig {
8
+ params(
9
+ double: Object,
10
+ call_binding: Binding,
11
+ default_args: T.nilable(T::Hash[Symbol, T.anything]),
12
+ dry_class: T::Class[Object],
13
+ type: T.any(Module, T::Class[T.anything]),
14
+ method: Symbol,
15
+ original_method: T.any(UnboundMethod, Method),
16
+ signature: Signature
17
+ ).returns(Call)
18
+ }
19
+ def reconstruct(double:, call_binding:, default_args:, dry_class:, type:, method:, original_method:, signature:)
20
+ Call.new(
21
+ singleton: false,
22
+ double: double,
23
+ original_type: type,
24
+ dry_type: dry_class,
25
+ method: method,
26
+ original_method: original_method,
27
+ args: args_for(signature, call_binding, default_args),
28
+ kwargs: kwargs_for(signature, call_binding, default_args),
29
+ block: call_binding.local_variable_get(signature.block_param || ::Mocktail::Signature::DEFAULT_BLOCK_PARAM)
30
+ )
31
+ end
32
+
33
+ private
34
+
35
+ sig {
36
+ params(signature: Signature, call_binding: Binding, default_args: T.nilable(T::Hash[Symbol, T.anything]))
37
+ .returns(T::Array[T.anything])
38
+ }
39
+ def args_for(signature, call_binding, default_args)
40
+ arg_names, rest_name = non_default_args(signature.positional_params, default_args)
41
+
42
+ arg_values = arg_names.map { |p| call_binding.local_variable_get(p) }
43
+ rest_value = call_binding.local_variable_get(rest_name) if rest_name
44
+
45
+ arg_values + (rest_value || [])
46
+ end
47
+
48
+ sig {
49
+ params(signature: Signature, call_binding: Binding, default_args: T.nilable(T::Hash[Symbol, T.anything]))
50
+ .returns(T::Hash[Symbol, T.anything])
51
+ }
52
+ def kwargs_for(signature, call_binding, default_args)
53
+ kwarg_names, kwrest_name = non_default_args(signature.keyword_params, default_args)
54
+
55
+ kwarg_values = kwarg_names.to_h { |p| [p, call_binding.local_variable_get(p)] }
56
+ kwrest_value = call_binding.local_variable_get(kwrest_name) if kwrest_name
57
+
58
+ kwarg_values.merge(kwrest_value || {})
59
+ end
60
+
61
+ sig { params(params: Params, default_args: T.nilable(T::Hash[Symbol, T.anything])).returns([T::Array[Symbol], T.nilable(Symbol)]) }
62
+ def non_default_args(params, default_args)
63
+ named_args = params.allowed
64
+ .reject { |p| default_args&.key?(p) }
65
+ rest_param = params.rest
66
+ rest_arg = if rest_param && !default_args&.key?(rest_param)
67
+ params.rest
68
+ end
69
+
70
+ [named_args, rest_arg]
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,136 @@
1
+ # typed: strict
2
+
3
+ require_relative "declares_dry_class/reconstructs_call"
4
+
5
+ module Mocktail
6
+ class DeclaresDryClass
7
+ extend T::Sig
8
+
9
+ DEFAULT_ANCESTORS = T.let(T.must(Class.new(Object).ancestors[1..]), T::Array[T.any(T::Class[T.anything], Module)])
10
+
11
+ sig { void }
12
+ def initialize
13
+ @raises_neato_no_method_error = T.let(RaisesNeatoNoMethodError.new, RaisesNeatoNoMethodError)
14
+ @transforms_params = T.let(TransformsParams.new, TransformsParams)
15
+ @stringifies_method_signature = T.let(StringifiesMethodSignature.new, StringifiesMethodSignature)
16
+ @grabs_original_method_parameters = T.let(GrabsOriginalMethodParameters.new, GrabsOriginalMethodParameters)
17
+ end
18
+
19
+ sig {
20
+ type_parameters(:T)
21
+ .params(type: T.all(T.type_parameter(:T), T::Class[Object]), instance_methods: T::Array[Symbol]).returns(T.type_parameter(:T))
22
+ }
23
+ def declare(type, instance_methods)
24
+ dry_class = Class.new(Object) {
25
+ include type if T.unsafe(type).is_a?(Module) && !T.unsafe(type).is_a?(Class)
26
+
27
+ define_method :initialize do |*args, **kwargs, &blk|
28
+ end
29
+
30
+ [:is_a?, :kind_of?].each do |method_name|
31
+ define_method method_name, ->(thing) {
32
+ # Mocktails extend from Object, so share the same ancestors, plus the passed type
33
+ [type, *DEFAULT_ANCESTORS].include?(thing)
34
+ }
35
+ end
36
+
37
+ if T.unsafe(type).is_a?(Class)
38
+ define_method :instance_of?, ->(thing) {
39
+ type == thing
40
+ }
41
+ end
42
+ }
43
+
44
+ add_more_methods!(dry_class, type, instance_methods)
45
+
46
+ T.unsafe(dry_class) # This is all fake! That's the whole point—it's not a real Foo, it's just some new class that quacks like a Foo
47
+ end
48
+
49
+ private
50
+
51
+ # These have special implementations, but if the user defines
52
+ # any of them on the object itself, then they'll be replaced with normal
53
+ # mocked methods. YMMV
54
+ sig { params(dry_class: T::Class[Object], type: T.any(T::Class[T.anything], Module), instance_methods: T::Array[Symbol]).void }
55
+ def add_more_methods!(dry_class, type, instance_methods)
56
+ add_stringify_methods!(dry_class, :to_s, type, instance_methods)
57
+ add_stringify_methods!(dry_class, :inspect, type, instance_methods)
58
+ define_method_missing_errors!(dry_class, type, instance_methods)
59
+
60
+ define_double_methods!(dry_class, type, instance_methods)
61
+ end
62
+
63
+ sig { params(dry_class: T::Class[Object], type: T.any(T::Class[T.anything], Module), instance_methods: T::Array[Symbol]).void }
64
+ def define_double_methods!(dry_class, type, instance_methods)
65
+ instance_methods.each do |method_name|
66
+ dry_class.undef_method(method_name) if dry_class.method_defined?(method_name)
67
+ parameters = @grabs_original_method_parameters.grab(type.instance_method(method_name))
68
+ signature = @transforms_params.transform(Call.new, params: parameters)
69
+ method_signature = @stringifies_method_signature.stringify(signature)
70
+ __mocktail_closure = {
71
+ dry_class: dry_class,
72
+ type: type,
73
+ method: method_name,
74
+ original_method: type.instance_method(method_name),
75
+ signature: signature
76
+ }
77
+
78
+ dry_class.define_method method_name,
79
+ eval(<<-RUBBY, binding, __FILE__, __LINE__ + 1) # standard:disable Security/Eval
80
+ ->#{method_signature} do
81
+ ::Mocktail::Debug.guard_against_mocktail_accidentally_calling_mocks_if_debugging!
82
+ ::Mocktail::HandlesDryCall.new.handle(::Mocktail::ReconstructsCall.new.reconstruct(
83
+ double: self,
84
+ call_binding: __send__(:binding),
85
+ default_args: (__send__(:binding).local_variable_defined?(:__mocktail_default_args) ? __send__(:binding).local_variable_get(:__mocktail_default_args) : {}),
86
+ **__mocktail_closure
87
+ ))
88
+ end
89
+ RUBBY
90
+ end
91
+ end
92
+
93
+ sig { params(dry_class: T::Class[Object], method_name: Symbol, type: T.any(T::Class[T.anything], Module), instance_methods: T::Array[Symbol]).void }
94
+ def add_stringify_methods!(dry_class, method_name, type, instance_methods)
95
+ dry_class.define_singleton_method method_name, -> {
96
+ if (id_matches = super().match(/:([0-9a-fx]+)>$/))
97
+ "#<Class #{"including module " if type.instance_of?(Module)}for mocktail of #{type.name}:#{id_matches[1]}>"
98
+ else
99
+ super()
100
+ end
101
+ }
102
+
103
+ unless instance_methods.include?(method_name)
104
+ dry_class.define_method method_name, -> {
105
+ if (id_matches = super().match(/:([0-9a-fx]+)>$/))
106
+ "#<Mocktail of #{type.name}:#{id_matches[1]}>"
107
+ else
108
+ super()
109
+ end
110
+ }
111
+ end
112
+ end
113
+
114
+ sig { params(dry_class: T::Class[Object], type: T.any(T::Class[T.anything], Module), instance_methods: T::Array[Symbol]).void }
115
+ def define_method_missing_errors!(dry_class, type, instance_methods)
116
+ return if instance_methods.include?(:method_missing)
117
+
118
+ raises_neato_no_method_error = @raises_neato_no_method_error
119
+ dry_class.define_method :method_missing, ->(name, *args, **kwargs, &block) {
120
+ raises_neato_no_method_error.call(
121
+ Call.new(
122
+ singleton: false,
123
+ double: self,
124
+ original_type: type,
125
+ dry_type: self.class,
126
+ method: name,
127
+ original_method: nil,
128
+ args: args,
129
+ kwargs: kwargs,
130
+ block: block
131
+ )
132
+ )
133
+ }
134
+ end
135
+ end
136
+ end