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
@@ -1,5 +1,7 @@
1
1
  module Mocktail::Matchers
2
2
  class Not < Base
3
+ extend T::Sig
4
+
3
5
  def self.matcher_name
4
6
  :not
5
7
  end
@@ -1,16 +1,17 @@
1
1
  module Mocktail::Matchers
2
2
  class Numeric < Base
3
+ extend T::Sig
4
+
3
5
  def self.matcher_name
4
6
  :numeric
5
7
  end
6
8
 
7
- # Change this comment to a descriptive one once this is merged:
8
- # https://github.com/rubocop/rubocop/pull/10551
9
- def initialize # standard:disable Style/RedundantInitialize
9
+ def initialize
10
+ # Empty initialize is necessary b/c Base default expects an argument
10
11
  end
11
12
 
12
13
  def match?(actual)
13
- [Integer, Float, (BigDecimal if defined?(BigDecimal))].include?(actual.class)
14
+ actual.is_a?(::Numeric)
14
15
  end
15
16
 
16
17
  def inspect
@@ -1,5 +1,7 @@
1
1
  module Mocktail::Matchers
2
2
  class That < Base
3
+ extend T::Sig
4
+
3
5
  def self.matcher_name
4
6
  :that
5
7
  end
@@ -7,6 +7,9 @@ require_relative "matchers/base"
7
7
  require_relative "matchers/any"
8
8
  require_relative "matchers/captor"
9
9
  require_relative "matchers/includes"
10
+ require_relative "matchers/includes_string"
11
+ require_relative "matchers/includes_hash"
12
+ require_relative "matchers/includes_key"
10
13
  require_relative "matchers/is_a"
11
14
  require_relative "matchers/matches"
12
15
  require_relative "matchers/not"
@@ -4,6 +4,8 @@ require_relative "share/creates_identifier"
4
4
 
5
5
  module Mocktail
6
6
  class RaisesNeatoNoMethodError
7
+ extend T::Sig
8
+
7
9
  def initialize
8
10
  @stringifies_call = StringifiesCall.new
9
11
  @stringifies_method_name = StringifiesMethodName.new
@@ -1,5 +1,7 @@
1
1
  module Mocktail
2
2
  class RecordsDemonstration
3
+ extend T::Sig
4
+
3
5
  def record(demonstration, demo_config)
4
6
  cabinet = Mocktail.cabinet
5
7
  prior_call_count = Mocktail.cabinet.calls.dup.size
@@ -1,5 +1,11 @@
1
1
  module Mocktail
2
2
  class RegistersMatcher
3
+ extend T::Sig
4
+
5
+ def initialize
6
+ @grabs_original_method_parameters = GrabsOriginalMethodParameters.new
7
+ end
8
+
3
9
  def register(matcher_type)
4
10
  if invalid_type?(matcher_type)
5
11
  raise InvalidMatcherError.new <<~MSG.tr("\n", " ")
@@ -32,12 +38,11 @@ module Mocktail
32
38
  return true unless matcher_type.respond_to?(:matcher_name)
33
39
  name = matcher_type.matcher_name
34
40
 
35
- !(name.is_a?(String) || name.is_a?(Symbol)) ||
36
- name.to_sym.inspect.start_with?(":\"")
41
+ !name.respond_to?(:to_sym) || name.to_sym.inspect.start_with?(":\"")
37
42
  end
38
43
 
39
44
  def invalid_match?(matcher_type)
40
- params = matcher_type.instance_method(:match?).parameters
45
+ params = @grabs_original_method_parameters.grab(matcher_type.instance_method(:match?))
41
46
  params.size > 1 || ![:req, :opt].include?(params.first[0])
42
47
  rescue NameError
43
48
  true
@@ -2,6 +2,8 @@ require_relative "records_demonstration"
2
2
 
3
3
  module Mocktail
4
4
  class RegistersStubbing
5
+ extend T::Sig
6
+
5
7
  def initialize
6
8
  @records_demonstration = RecordsDemonstration.new
7
9
  end
@@ -1,11 +1,17 @@
1
1
  module Mocktail
2
2
  class ReplacesNext
3
+ extend T::Sig
4
+
3
5
  def initialize
4
6
  @top_shelf = TopShelf.instance
5
7
  @redefines_new = RedefinesNew.new
6
8
  @imitates_type = ImitatesType.new
7
9
  end
8
10
 
11
+ def replace_once(type)
12
+ replace(type, 1).fetch(0)
13
+ end
14
+
9
15
  def replace(type, count)
10
16
  raise UnsupportedMocktail.new("Mocktail.of_next() only supports classes") unless type.is_a?(Class)
11
17
 
@@ -30,7 +36,7 @@ module Mocktail
30
36
  }
31
37
  end
32
38
 
33
- (mocktails.size == 1) ? mocktails.first : mocktails
39
+ mocktails
34
40
  end
35
41
  end
36
42
  end
@@ -1,5 +1,7 @@
1
1
  module Mocktail
2
2
  class RedefinesNew
3
+ extend T::Sig
4
+
3
5
  def initialize
4
6
  @handles_dry_new_call = HandlesDryNewCall.new
5
7
  end
@@ -13,7 +15,7 @@ module Mocktail
13
15
  handles_dry_new_call = @handles_dry_new_call
14
16
  type.define_singleton_method :new, ->(*args, **kwargs, &block) {
15
17
  if TopShelf.instance.new_replaced?(type) ||
16
- TopShelf.instance.of_next_registered?(type)
18
+ (type.is_a?(Class) && TopShelf.instance.of_next_registered?(type))
17
19
  handles_dry_new_call.handle(type, args, kwargs, block)
18
20
  else
19
21
  type_replacement.original_new.call(*args, **kwargs, &block)
@@ -1,5 +1,7 @@
1
1
  module Mocktail
2
2
  class RedefinesSingletonMethods
3
+ extend T::Sig
4
+
3
5
  def initialize
4
6
  @handles_dry_call = HandlesDryCall.new
5
7
  end
@@ -10,11 +12,11 @@ module Mocktail
10
12
 
11
13
  type_replacement.original_methods = type.singleton_methods.map { |name|
12
14
  type.method(name)
13
- } - [type_replacement.replacement_new]
15
+ }.reject { |method| sorbet_method_hook?(method) } - [type_replacement.replacement_new]
14
16
 
15
17
  declare_singleton_method_missing_errors!(type)
16
18
  handles_dry_call = @handles_dry_call
17
- type_replacement.replacement_methods = type_replacement.original_methods.map { |original_method|
19
+ type_replacement.replacement_methods = type_replacement.original_methods&.map { |original_method|
18
20
  type.singleton_class.send(:undef_method, original_method.name)
19
21
  type.define_singleton_method original_method.name, ->(*args, **kwargs, &block) {
20
22
  if TopShelf.instance.singleton_methods_replaced?(type)
@@ -58,5 +60,15 @@ module Mocktail
58
60
  )
59
61
  }
60
62
  end
63
+
64
+ private
65
+
66
+ def sorbet_method_hook?(method)
67
+ [
68
+ T::Sig,
69
+ T::Private::Methods::MethodHooks,
70
+ T::Private::Methods::SingletonMethodHooks
71
+ ].include?(method.owner)
72
+ end
61
73
  end
62
74
  end
@@ -0,0 +1,37 @@
1
+ module Mocktail
2
+ class RunsSorbetSigBlocksBeforeReplacement
3
+ extend T::Sig
4
+
5
+ # This is necessary because when Sorbet runs a sig block of a singleton
6
+ # method, it has the net effect of unwrapping/redefining the method. If
7
+ # we try to use Mocktail.replace(Foo) and Foo.bar has a Sorbet sig block,
8
+ # then we'll end up with three "versions" of the same method and no way
9
+ # to keep straight which one == which:
10
+ #
11
+ # A - Foo.bar, as defined in the original class
12
+ # B - Foo.bar, as redefined by RedefinesSingletonMethods
13
+ # C - Foo.bar, as wrapped by sorbet-runtime
14
+ #
15
+ # Initially, Foo.method(:bar) would == C, but after the type
16
+ # replacement, it would == B (with a reference back to C as the original),
17
+ # but after handling a single dry call, our invocation of
18
+ # GrabsOriginalMethodParameters.grab(Foo.method(:bar)) would invoke the
19
+ # Sorbet `sig` block, which has the net effect of redefining the method back
20
+ # to A.
21
+ #
22
+ # It's very fun and confusing and a great time.
23
+
24
+ def run(type)
25
+ return unless defined?(T::Private::Methods)
26
+
27
+ type.singleton_methods.each do |method_name|
28
+ method = type.method(method_name)
29
+
30
+ # Again: calling this for the side effect of running the sig block
31
+ #
32
+ # https://github.com/sorbet/sorbet/blob/master/gems/sorbet-runtime/lib/types/private/methods/_methods.rb#L111
33
+ T::Private::Methods.signature_for_method(method)
34
+ end
35
+ end
36
+ end
37
+ end
@@ -1,10 +1,14 @@
1
1
  require_relative "replaces_type/redefines_new"
2
2
  require_relative "replaces_type/redefines_singleton_methods"
3
+ require_relative "replaces_type/runs_sorbet_sig_blocks_before_replacement"
3
4
 
4
5
  module Mocktail
5
6
  class ReplacesType
7
+ extend T::Sig
8
+
6
9
  def initialize
7
10
  @top_shelf = TopShelf.instance
11
+ @runs_sorbet_sig_blocks_before_replacement = RunsSorbetSigBlocksBeforeReplacement.new
8
12
  @redefines_new = RedefinesNew.new
9
13
  @redefines_singleton_methods = RedefinesSingletonMethods.new
10
14
  end
@@ -14,6 +18,8 @@ module Mocktail
14
18
  raise UnsupportedMocktail.new("Mocktail.replace() only supports classes and modules")
15
19
  end
16
20
 
21
+ @runs_sorbet_sig_blocks_before_replacement.run(type)
22
+
17
23
  if type.is_a?(Class)
18
24
  @top_shelf.register_new_replacement!(type)
19
25
  @redefines_new.redefine(type)
@@ -1,5 +1,7 @@
1
1
  module Mocktail
2
2
  class ResetsState
3
+ extend T::Sig
4
+
3
5
  def reset
4
6
  TopShelf.instance.reset_current_thread!
5
7
  Mocktail.cabinet.reset!
@@ -1,13 +1,15 @@
1
1
  module Mocktail
2
2
  module Bind
3
- def self.call(mock, method_name, *args, **kwargs, &blk)
3
+ # sig intentionally omitted, because the wrapper will cause infinite recursion if certain methods are mocked
4
+ def self.call(mock, method_name, *args, **kwargs, &blk) # standard:disable Style/ArgumentsForwarding
4
5
  if Mocktail.cabinet.double_for_instance(mock)
5
- Object.instance_method(method_name).bind_call(mock, *args, **kwargs, &blk)
6
- elsif (type_replacement = TopShelf.instance.type_replacement_if_exists_for(mock)) &&
6
+ Object.instance_method(method_name).bind_call(mock, *args, **kwargs, &blk) # standard:disable Style/ArgumentsForwarding
7
+ elsif (mock.is_a?(Module) || mock.is_a?(Class)) &&
8
+ (type_replacement = TopShelf.instance.type_replacement_if_exists_for(mock)) &&
7
9
  (og_method = type_replacement.original_methods&.find { |m| m.name == method_name })
8
- og_method.call(*args, **kwargs, &blk)
10
+ og_method.call(*args, **kwargs, &blk) # standard:disable Style/ArgumentsForwarding
9
11
  else
10
- mock.__send__(method_name, *args, **kwargs, &blk)
12
+ mock.__send__(method_name, *args, **kwargs, &blk) # standard:disable Style/ArgumentsForwarding
11
13
  end
12
14
  end
13
15
  end
@@ -1,15 +1,13 @@
1
- require "pathname"
2
-
3
1
  module Mocktail
4
2
  class CleansBacktrace
5
- BASE_PATH = (Pathname.new(__FILE__) + "../../..").to_s
3
+ extend T::Sig
6
4
 
7
5
  def clean(error)
8
6
  raise error
9
- rescue => e
7
+ rescue error.class => e
10
8
  e.tap do |e|
11
9
  e.set_backtrace(e.backtrace.drop_while { |frame|
12
- frame.start_with?(BASE_PATH)
10
+ frame.start_with?(BASE_PATH, BASE_PATH) || frame.match?(/[\\|\/]sorbet-runtime.*[\\|\/]lib[\\|\/]types[\\|\/]private/)
13
11
  })
14
12
  end
15
13
  end
@@ -1,19 +1,26 @@
1
1
  module Mocktail
2
2
  class CreatesIdentifier
3
+ extend T::Sig
4
+
3
5
  KEYWORDS = %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]
4
6
 
5
7
  def create(s, default: "identifier", max_length: 24)
6
- id = s.to_s.downcase
7
- .gsub(/:0x[0-9a-f]+/, "") # Lazy attempt to wipe any Object:0x802beef identifiers
8
- .gsub(/[^\w\s]/, "")
9
- .gsub(/^\d+/, "")[0...max_length]
10
- .strip
11
- .gsub(/\s+/, "_") # snake_case
8
+ case s
9
+ when Kernel
10
+ id = (s.to_s.downcase
11
+ .gsub(/:0x[0-9a-f]+/, "") # Lazy attempt to wipe any Object:0x802beef identifiers
12
+ .gsub(/[^\w\s]/, "")
13
+ .gsub(/^\d+/, "")[0...max_length] || "")
14
+ .strip
15
+ .gsub(/\s+/, "_") # snake_case
12
16
 
13
- if id.empty?
14
- default
17
+ if id.empty?
18
+ default
19
+ else
20
+ unreserved(id, default)
21
+ end
15
22
  else
16
- unreserved(id, default)
23
+ default
17
24
  end
18
25
  end
19
26
 
@@ -2,6 +2,8 @@ require_relative "bind"
2
2
 
3
3
  module Mocktail
4
4
  class DeterminesMatchingCalls
5
+ extend T::Sig
6
+
5
7
  def determine(real_call, demo_call, demo_config)
6
8
  Bind.call(real_call.double, :==, demo_call.double) &&
7
9
  real_call.method == demo_call.method &&
@@ -40,7 +42,7 @@ module Mocktail
40
42
  end
41
43
 
42
44
  def blocks_match?(real_block, demo_block, ignore_block)
43
- ignore_block ||
45
+ !!(ignore_block ||
44
46
  (real_block.nil? && demo_block.nil?) ||
45
47
  (
46
48
  real_block && demo_block &&
@@ -48,7 +50,7 @@ module Mocktail
48
50
  demo_block == real_block ||
49
51
  demo_block.call(real_block)
50
52
  )
51
- )
53
+ ))
52
54
  end
53
55
 
54
56
  def match?(real_arg, demo_arg)
@@ -1,5 +1,7 @@
1
1
  module Mocktail
2
2
  class StringifiesCall
3
+ extend T::Sig
4
+
3
5
  def stringify(call, anonymous_blocks: false, always_parens: false)
4
6
  "#{call.method}#{args_to_s(call, parens: always_parens)}#{blockify(call.block, anonymous: anonymous_blocks)}"
5
7
  end
@@ -31,16 +33,18 @@ module Mocktail
31
33
  "(#{args_lists.join(", ")})"
32
34
  elsif parens
33
35
  "()"
36
+ else
37
+ ""
34
38
  end
35
39
  end
36
40
 
37
41
  def argify(args)
38
- return unless args && !args.empty?
42
+ return unless !args.empty?
39
43
  args.map(&:inspect).join(", ")
40
44
  end
41
45
 
42
46
  def kwargify(kwargs)
43
- return unless kwargs && !kwargs.empty?
47
+ return unless !kwargs.empty?
44
48
  kwargs.map { |key, val| "#{key}: #{val.inspect}" }.join(", ")
45
49
  end
46
50
 
@@ -1,8 +1,10 @@
1
1
  module Mocktail
2
2
  class StringifiesMethodName
3
+ extend T::Sig
4
+
3
5
  def stringify(call)
4
6
  [
5
- call.original_type.name,
7
+ call.original_type&.name,
6
8
  call.singleton ? "." : "#",
7
9
  call.method
8
10
  ].join
@@ -1,5 +1,7 @@
1
1
  module Mocktail
2
2
  class ReconcilesArgsWithParams
3
+ extend T::Sig
4
+
3
5
  def reconcile(signature)
4
6
  args_match?(signature.positional_params, signature.positional_args) &&
5
7
  kwargs_match?(signature.keyword_params, signature.keyword_args)
@@ -1,5 +1,7 @@
1
1
  module Mocktail
2
2
  class RecreatesMessage
3
+ extend T::Sig
4
+
3
5
  def recreate(signature)
4
6
  req_args = signature.positional_params.required.size
5
7
  allowed_args = signature.positional_params.allowed.size
@@ -2,16 +2,23 @@ require_relative "../share/bind"
2
2
 
3
3
  module Mocktail
4
4
  class TransformsParams
5
- def transform(dry_call, params: dry_call.original_method.parameters)
5
+ extend T::Sig
6
+
7
+ def initialize
8
+ @grabs_original_method_parameters = GrabsOriginalMethodParameters.new
9
+ end
10
+
11
+ def transform(dry_call, params: nil)
12
+ params ||= @grabs_original_method_parameters.grab(dry_call.original_method)
6
13
  params = name_unnamed_params(params)
7
14
 
8
15
  Signature.new(
9
16
  positional_params: Params.new(
10
17
  all: params.select { |t, _|
11
18
  [:req, :opt, :rest].any? { |param_type| Bind.call(t, :==, param_type) }
12
- }.map { |_, name| name },
13
- required: params.select { |t, _| Bind.call(t, :==, :req) }.map { |_, n| n },
14
- optional: params.select { |t, _| Bind.call(t, :==, :opt) }.map { |_, n| n },
19
+ }.map { |pair| pair.fetch(1) },
20
+ required: params.select { |t, _| Bind.call(t, :==, :req) }.map { |pair| pair.fetch(1) },
21
+ optional: params.select { |t, _| Bind.call(t, :==, :opt) }.map { |pair| pair.fetch(1) },
15
22
  rest: params.find { |t, _| Bind.call(t, :==, :rest) }&.last
16
23
  ),
17
24
  positional_args: dry_call.args,
@@ -19,9 +26,9 @@ module Mocktail
19
26
  keyword_params: Params.new(
20
27
  all: params.select { |type, _|
21
28
  [:keyreq, :key, :keyrest].include?(type)
22
- }.map { |_, name| name },
23
- required: params.select { |t, _| Bind.call(t, :==, :keyreq) }.map { |_, n| n },
24
- optional: params.select { |t, _| Bind.call(t, :==, :key) }.map { |_, n| n },
29
+ }.map { |pair| pair.fetch(1) },
30
+ required: params.select { |t, _| Bind.call(t, :==, :keyreq) }.map { |pair| pair.fetch(1) },
31
+ optional: params.select { |t, _| Bind.call(t, :==, :key) }.map { |pair| pair.fetch(1) },
25
32
  rest: params.find { |t, _| Bind.call(t, :==, :keyrest) }&.last
26
33
  ),
27
34
  keyword_args: dry_call.kwargs,
@@ -36,7 +43,7 @@ module Mocktail
36
43
  def name_unnamed_params(params)
37
44
  params.map.with_index { |param, i|
38
45
  if param.size == 1
39
- param + ["unnamed_arg_#{i + 1}"]
46
+ param + ["unnamed_arg_#{i + 1}".to_sym]
40
47
  else
41
48
  param
42
49
  end
@@ -6,6 +6,8 @@ require_relative "share/stringifies_call"
6
6
 
7
7
  module Mocktail
8
8
  class SimulatesArgumentError
9
+ extend T::Sig
10
+
9
11
  def initialize
10
12
  @transforms_params = TransformsParams.new
11
13
  @reconciles_args_with_params = ReconcilesArgsWithParams.new
@@ -0,0 +1,18 @@
1
+ # typed: strict
2
+
3
+ module Mocktail
4
+ class CollectsCalls
5
+ extend T::Sig
6
+
7
+ sig { params(double: Object, method_name: T.nilable(Symbol)).returns(T::Array[Call]) }
8
+ def collect(double, method_name)
9
+ calls = ExplainsThing.new.explain(double).reference.calls
10
+
11
+ if method_name.nil?
12
+ calls
13
+ else
14
+ calls.select { |call| call.method.to_s == method_name.to_s }
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,54 @@
1
+ # typed: strict
2
+
3
+ module Mocktail
4
+ module Debug
5
+ extend T::Sig
6
+
7
+ # It would be easy and bad for the mocktail lib to call something like
8
+ #
9
+ # double == other_double
10
+ #
11
+ # But if it's a double, that means anyone who stubs that method could change
12
+ # the internal behavior of the library in unexpected ways (as happened here:
13
+ # https://github.com/testdouble/mocktail/issues/7 )
14
+ #
15
+ # For that reason when we run our tests, we also want to blow up if this
16
+ # happens unintentionally. This works in conjunction with the test
17
+ # MockingMethodfulClassesTest, because it mocks every defined method on the
18
+ # mocked BasicObject
19
+ sig { void }
20
+ def self.guard_against_mocktail_accidentally_calling_mocks_if_debugging!
21
+ return unless ENV["MOCKTAIL_DEBUG_ACCIDENTAL_INTERNAL_MOCK_CALLS"]
22
+ raise Mocktail::Error
23
+ rescue Mocktail::Error => e
24
+ base_path = Pathname.new(__FILE__).dirname.to_s
25
+ backtrace_minus_this_and_whoever_called_this = e.backtrace&.[](2..)
26
+ internal_call_sites = backtrace_minus_this_and_whoever_called_this&.take_while { |call_site|
27
+ # the "in `block" is very confusing but necessary to include lines after
28
+ # a stubs { blah.foo }.with { … } call, since that's when most of the
29
+ # good stuff happens
30
+ call_site.start_with?(base_path) || call_site.include?("in `block")
31
+ }&.reject { |call_site| call_site.include?("in `block") } || []
32
+
33
+ approved_call_sites = [
34
+ /fulfills_stubbing.rb:(16|20)/,
35
+ /validates_arguments.rb:(18|23)/,
36
+ /validates_arguments.rb:(21|26)/
37
+ ]
38
+ if internal_call_sites.any? && approved_call_sites.none? { |approved_call_site|
39
+ internal_call_sites.first&.match?(approved_call_site)
40
+ }
41
+ raise Error.new <<~MSG
42
+ Unauthorized internal call of a mock internally by Mocktail itself:
43
+
44
+ #{internal_call_sites.first}
45
+
46
+ Offending call's complete stack trace:
47
+
48
+ #{backtrace_minus_this_and_whoever_called_this&.join("\n")}
49
+ ==END OFFENDING TRACE==
50
+ MSG
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,46 @@
1
+ # typed: strict
2
+
3
+ module Mocktail
4
+ module DSL
5
+ extend T::Sig
6
+
7
+ sig {
8
+ type_parameters(:T)
9
+ .params(
10
+ ignore_block: T::Boolean,
11
+ ignore_extra_args: T::Boolean,
12
+ ignore_arity: T::Boolean,
13
+ times: T.nilable(Integer),
14
+ demo: T.proc.params(matchers: Mocktail::MatcherPresentation).returns(T.type_parameter(:T))
15
+ )
16
+ .returns(Mocktail::Stubbing[T.type_parameter(:T)])
17
+ }
18
+ def stubs(ignore_block: false, ignore_extra_args: false, ignore_arity: false, times: nil, &demo)
19
+ RegistersStubbing.new.register(demo, DemoConfig.new(
20
+ ignore_block: ignore_block,
21
+ ignore_extra_args: ignore_extra_args,
22
+ ignore_arity: ignore_arity,
23
+ times: times
24
+ ))
25
+ end
26
+
27
+ sig {
28
+ type_parameters(:T)
29
+ .params(
30
+ ignore_block: T::Boolean,
31
+ ignore_extra_args: T::Boolean,
32
+ ignore_arity: T::Boolean,
33
+ times: T.nilable(Integer),
34
+ demo: T.proc.params(matchers: Mocktail::MatcherPresentation).void
35
+ ).void
36
+ }
37
+ def verify(ignore_block: false, ignore_extra_args: false, ignore_arity: false, times: nil, &demo)
38
+ VerifiesCall.new.verify(demo, DemoConfig.new(
39
+ ignore_block: ignore_block,
40
+ ignore_extra_args: ignore_extra_args,
41
+ ignore_arity: ignore_arity,
42
+ times: times
43
+ ))
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,19 @@
1
+ # typed: strict
2
+
3
+ module Mocktail
4
+ class Error < StandardError; end
5
+
6
+ class UnexpectedError < Error; end
7
+
8
+ class UnsupportedMocktail < Error; end
9
+
10
+ class MissingDemonstrationError < Error; end
11
+
12
+ class AmbiguousDemonstrationError < Error; end
13
+
14
+ class InvalidMatcherError < Error; end
15
+
16
+ class VerificationError < Error; end
17
+
18
+ class TypeCheckingError < Error; end
19
+ end