mocktail 1.2.3 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
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,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,14 @@
1
+ # typed: strict
2
+
3
+ module Mocktail
4
+ class ResetsState
5
+ extend T::Sig
6
+
7
+ sig { void }
8
+ def reset
9
+ TopShelf.instance.reset_current_thread!
10
+ Mocktail.cabinet.reset!
11
+ ValidatesArguments.enable!
12
+ end
13
+ end
14
+ end
@@ -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