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,28 @@
1
+ # typed: strict
2
+
3
+ module Mocktail
4
+ class GathersFakeableInstanceMethods
5
+ extend T::Sig
6
+
7
+ sig { params(type: T.any(T::Class[T.anything], Module)).returns(T::Array[Symbol]) }
8
+ def gather(type)
9
+ methods = type.instance_methods + [
10
+ (:respond_to_missing? if type.private_method_defined?(:respond_to_missing?))
11
+ ].compact
12
+
13
+ methods.reject { |m|
14
+ ignore?(type, m)
15
+ }
16
+ end
17
+
18
+ sig { params(type: T.any(T::Class[T.anything], Module), method_name: Symbol).returns(T::Boolean) }
19
+ def ignore?(type, method_name)
20
+ ignored_ancestors.include?(type.instance_method(method_name).owner)
21
+ end
22
+
23
+ sig { returns(T::Array[Module]) }
24
+ def ignored_ancestors
25
+ Object.ancestors
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,29 @@
1
+ # typed: strict
2
+
3
+ require_relative "makes_double/declares_dry_class"
4
+ require_relative "makes_double/gathers_fakeable_instance_methods"
5
+
6
+ module Mocktail
7
+ class MakesDouble
8
+ extend T::Sig
9
+
10
+ sig { void }
11
+ def initialize
12
+ @declares_dry_class = T.let(DeclaresDryClass.new, DeclaresDryClass)
13
+ @gathers_fakeable_instance_methods = T.let(GathersFakeableInstanceMethods.new, GathersFakeableInstanceMethods)
14
+ end
15
+
16
+ sig { params(type: T::Class[Object]).returns(Double) }
17
+ def make(type)
18
+ dry_methods = @gathers_fakeable_instance_methods.gather(type)
19
+ dry_type = @declares_dry_class.declare(type, dry_methods)
20
+
21
+ Double.new(
22
+ original_type: type,
23
+ dry_type: dry_type,
24
+ dry_instance: dry_type.new,
25
+ dry_methods: dry_methods
26
+ )
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,29 @@
1
+ # typed: strict
2
+
3
+ require_relative "imitates_type/ensures_imitation_support"
4
+ require_relative "imitates_type/makes_double"
5
+
6
+ module Mocktail
7
+ class ImitatesType
8
+ extend T::Sig
9
+ extend T::Generic
10
+
11
+ sig { void }
12
+ def initialize
13
+ @ensures_imitation_support = T.let(EnsuresImitationSupport.new, EnsuresImitationSupport)
14
+ @makes_double = T.let(MakesDouble.new, MakesDouble)
15
+ end
16
+
17
+ sig {
18
+ type_parameters(:T)
19
+ .params(type: T::Class[T.all(T.type_parameter(:T), Object)])
20
+ .returns(T.all(T.type_parameter(:T), Object))
21
+ }
22
+ def imitate(type)
23
+ @ensures_imitation_support.ensure(type)
24
+ T.cast(@makes_double.make(type).tap do |double|
25
+ Mocktail.cabinet.store_double(double)
26
+ end.dry_instance, T.all(T.type_parameter(:T), Object))
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,11 @@
1
+ # typed: false
2
+
3
+ require_relative "typed"
4
+
5
+ # Constant boolean, so won't statically type-check, but `T.unsafe` can't be used
6
+ # because we haven't required sorbet-runtime yet
7
+ if eval("Mocktail::TYPED", binding, __FILE__, __LINE__)
8
+ require "sorbet-runtime"
9
+ else
10
+ require "#{Gem.loaded_specs["sorbet-eraser"].gem_dir}/lib/t"
11
+ end
@@ -0,0 +1,25 @@
1
+ # typed: strict
2
+
3
+ module Mocktail
4
+ class InitializesMocktail
5
+ extend T::Sig
6
+
7
+ sig { void }
8
+ def init
9
+ [
10
+ Mocktail::Matchers::Any,
11
+ Mocktail::Matchers::Includes,
12
+ Mocktail::Matchers::IncludesString,
13
+ Mocktail::Matchers::IncludesKey,
14
+ Mocktail::Matchers::IncludesHash,
15
+ Mocktail::Matchers::IsA,
16
+ Mocktail::Matchers::Matches,
17
+ Mocktail::Matchers::Not,
18
+ Mocktail::Matchers::Numeric,
19
+ Mocktail::Matchers::That
20
+ ].each do |matcher_type|
21
+ Mocktail.register_matcher(matcher_type)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,21 @@
1
+ # typed: strict
2
+
3
+ module Mocktail
4
+ class MatcherPresentation
5
+ extend T::Sig
6
+
7
+ sig { params(name: Symbol, include_private: T::Boolean).returns(T::Boolean) }
8
+ def respond_to_missing?(name, include_private = false)
9
+ !!MatcherRegistry.instance.get(name) || super
10
+ end
11
+
12
+ sig { params(name: Symbol, args: T.anything, kwargs: T.anything, blk: T.nilable(Proc)).returns(T.anything) }
13
+ def method_missing(name, *args, **kwargs, &blk) # standard:disable Style/ArgumentsForwarding
14
+ if (matcher = MatcherRegistry.instance.get(name))
15
+ T.unsafe(matcher).new(*args, **kwargs, &blk) # standard:disable Style/ArgumentsForwarding
16
+ else
17
+ super
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,27 @@
1
+ # typed: strict
2
+
3
+ module Mocktail::Matchers
4
+ class Any < Base
5
+ extend T::Sig
6
+
7
+ sig { returns(Symbol) }
8
+ def self.matcher_name
9
+ :any
10
+ end
11
+
12
+ sig { void }
13
+ def initialize
14
+ # Empty initialize is necessary b/c Base default expects an argument
15
+ end
16
+
17
+ sig { params(actual: T.anything).returns(T::Boolean) }
18
+ def match?(actual)
19
+ true
20
+ end
21
+
22
+ sig { returns(String) }
23
+ def inspect
24
+ "any"
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,39 @@
1
+ # typed: strict
2
+
3
+ module Mocktail::Matchers
4
+ class Base
5
+ extend T::Sig
6
+ extend T::Helpers
7
+
8
+ if T.unsafe(Mocktail::TYPED) && T::Private::RuntimeLevels.default_checked_level != :never
9
+ abstract!
10
+ end
11
+
12
+ # Custom matchers can receive any args, kwargs, or block they want. Usually
13
+ # single-argument, though, so that's defaulted here and in #insepct
14
+ sig { params(expected: BasicObject).void }
15
+ def initialize(expected)
16
+ @expected = expected
17
+ end
18
+
19
+ sig { returns(Symbol) }
20
+ def self.matcher_name
21
+ raise Mocktail::InvalidMatcherError.new("The `matcher_name` class method must return a valid method name")
22
+ end
23
+
24
+ sig { params(actual: T.untyped).returns(T::Boolean) }
25
+ def match?(actual)
26
+ raise Mocktail::InvalidMatcherError.new("Matchers must implement `match?(argument)`")
27
+ end
28
+
29
+ sig { returns(String) }
30
+ def inspect
31
+ "#{self.class.matcher_name}(#{T.cast(@expected, Object).inspect})"
32
+ end
33
+
34
+ sig { returns(TrueClass) }
35
+ def is_mocktail_matcher?
36
+ true
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,76 @@
1
+ # typed: strict
2
+
3
+ module Mocktail::Matchers
4
+ # Captors are conceptually complex implementations, but with a simple usage/purpose:
5
+ # They are values the user can create and hold onto that will return a matcher
6
+ # and then "capture" the value made by the real call, for later analysis & assertion.
7
+ #
8
+ # Unlike other matchers, these don't make any useful sense for stubbing, but are
9
+ # very useful when asserting complication call verifications
10
+ #
11
+ # The fact the user will need the reference outside the verification call is
12
+ # why this is a top-level method on Mocktail, and not included in the |m| block
13
+ # arg to stubs/verify
14
+ #
15
+ # See Mockito, which is the earliest implementation I know of:
16
+ # https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/Captor.html
17
+ class Captor
18
+ extend T::Sig
19
+
20
+ class Capture < Mocktail::Matchers::Base
21
+ extend T::Sig
22
+
23
+ sig { returns(Symbol) }
24
+ def self.matcher_name
25
+ :capture
26
+ end
27
+
28
+ sig { returns(T.untyped) }
29
+ attr_reader :value
30
+
31
+ sig { void }
32
+ def initialize
33
+ @value = T.let(nil, T.untyped)
34
+ @captured = T.let(false, T::Boolean)
35
+ end
36
+
37
+ sig { params(actual: T.untyped).returns(TrueClass) }
38
+ def match?(actual)
39
+ @value = actual
40
+ @captured = true
41
+ true
42
+ end
43
+
44
+ sig { returns(T::Boolean) }
45
+ def captured?
46
+ @captured
47
+ end
48
+
49
+ sig { returns(String) }
50
+ def inspect
51
+ "capture"
52
+ end
53
+ end
54
+
55
+ # This T.untyped is intentional. Even though a Capture is surely returned,
56
+ # in order for a verification demonstration to pass its own type check,
57
+ # it needs to think it's being returned whatever parameter is expected
58
+ sig { returns(T.untyped) }
59
+ attr_reader :capture
60
+
61
+ sig { void }
62
+ def initialize
63
+ @capture = T.let(Capture.new, Capture)
64
+ end
65
+
66
+ sig { returns(T::Boolean) }
67
+ def captured?
68
+ @capture.captured?
69
+ end
70
+
71
+ sig { returns(T.untyped) }
72
+ def value
73
+ @capture.value
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,32 @@
1
+ # typed: strict
2
+
3
+ module Mocktail::Matchers
4
+ class Includes < Base
5
+ extend T::Sig
6
+
7
+ sig { returns(Symbol) }
8
+ def self.matcher_name
9
+ :includes
10
+ end
11
+
12
+ sig { params(expecteds: T.untyped).void }
13
+ def initialize(*expecteds)
14
+ @expecteds = T.let(expecteds, T::Array[T.untyped])
15
+ end
16
+
17
+ sig { params(actual: T.untyped).returns(T::Boolean) }
18
+ def match?(actual)
19
+ @expecteds.all? { |expected|
20
+ (actual.respond_to?(:include?) && actual.include?(expected)) ||
21
+ (actual.is_a?(Hash) && expected.is_a?(Hash) && expected.all? { |k, v| actual[k] == v })
22
+ }
23
+ rescue
24
+ false
25
+ end
26
+
27
+ sig { returns(String) }
28
+ def inspect
29
+ "#{self.class.matcher_name}(#{@expecteds.map(&:inspect).join(", ")})"
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,12 @@
1
+ # typed: strict
2
+
3
+ module Mocktail::Matchers
4
+ class IncludesHash < Includes
5
+ extend T::Sig
6
+
7
+ sig { returns(Symbol) }
8
+ def self.matcher_name
9
+ :includes_hash
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ # typed: strict
2
+
3
+ module Mocktail::Matchers
4
+ class IncludesKey < Includes
5
+ extend T::Sig
6
+
7
+ sig { returns(Symbol) }
8
+ def self.matcher_name
9
+ :includes_key
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ # typed: strict
2
+
3
+ module Mocktail::Matchers
4
+ class IncludesString < Includes
5
+ extend T::Sig
6
+
7
+ sig { returns(Symbol) }
8
+ def self.matcher_name
9
+ :includes_string
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,17 @@
1
+ # typed: strict
2
+
3
+ module Mocktail::Matchers
4
+ class IsA < Base
5
+ extend T::Sig
6
+
7
+ sig { returns(Symbol) }
8
+ def self.matcher_name
9
+ :is_a
10
+ end
11
+
12
+ sig { params(actual: T.untyped).returns(T::Boolean) }
13
+ def match?(actual)
14
+ actual.is_a?(@expected)
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,19 @@
1
+ # typed: strict
2
+
3
+ module Mocktail::Matchers
4
+ class Matches < Base
5
+ extend T::Sig
6
+
7
+ sig { returns(Symbol) }
8
+ def self.matcher_name
9
+ :matches
10
+ end
11
+
12
+ sig { params(actual: T.untyped).returns(T::Boolean) }
13
+ def match?(actual)
14
+ actual.respond_to?(:match?) && actual.match?(@expected)
15
+ rescue
16
+ false
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,17 @@
1
+ # typed: strict
2
+
3
+ module Mocktail::Matchers
4
+ class Not < Base
5
+ extend T::Sig
6
+
7
+ sig { returns(Symbol) }
8
+ def self.matcher_name
9
+ :not
10
+ end
11
+
12
+ sig { params(actual: T.untyped).returns(T::Boolean) }
13
+ def match?(actual)
14
+ @expected != actual
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,27 @@
1
+ # typed: strict
2
+
3
+ module Mocktail::Matchers
4
+ class Numeric < Base
5
+ extend T::Sig
6
+
7
+ sig { returns(Symbol) }
8
+ def self.matcher_name
9
+ :numeric
10
+ end
11
+
12
+ sig { void }
13
+ def initialize
14
+ # Empty initialize is necessary b/c Base default expects an argument
15
+ end
16
+
17
+ sig { params(actual: T.untyped).returns(T::Boolean) }
18
+ def match?(actual)
19
+ actual.is_a?(::Numeric)
20
+ end
21
+
22
+ sig { returns(String) }
23
+ def inspect
24
+ "numeric"
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,32 @@
1
+ # typed: strict
2
+
3
+ module Mocktail::Matchers
4
+ class That < Base
5
+ extend T::Sig
6
+
7
+ sig { returns(Symbol) }
8
+ def self.matcher_name
9
+ :that
10
+ end
11
+
12
+ sig { params(blk: T.nilable(T.proc.params(actual: T.untyped).returns(T.untyped))).void }
13
+ def initialize(&blk)
14
+ if blk.nil?
15
+ raise ArgumentError.new("The `that` matcher must be passed a block (e.g. `that { |arg| … }`)")
16
+ end
17
+ @blk = T.let(blk, T.proc.params(actual: T.untyped).returns(T.untyped))
18
+ end
19
+
20
+ sig { params(actual: T.untyped).returns(T::Boolean) }
21
+ def match?(actual)
22
+ @blk.call(actual)
23
+ rescue
24
+ false
25
+ end
26
+
27
+ sig { returns(String) }
28
+ def inspect
29
+ "that {…}"
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,19 @@
1
+ # typed: strict
2
+
3
+ module Mocktail
4
+ module Matchers
5
+ end
6
+ end
7
+
8
+ require_relative "matchers/base"
9
+ require_relative "matchers/any"
10
+ require_relative "matchers/captor"
11
+ require_relative "matchers/includes"
12
+ require_relative "matchers/includes_string"
13
+ require_relative "matchers/includes_hash"
14
+ require_relative "matchers/includes_key"
15
+ require_relative "matchers/is_a"
16
+ require_relative "matchers/matches"
17
+ require_relative "matchers/not"
18
+ require_relative "matchers/numeric"
19
+ require_relative "matchers/that"
@@ -0,0 +1,93 @@
1
+ # typed: strict
2
+
3
+ require_relative "share/stringifies_call"
4
+ require_relative "share/stringifies_method_name"
5
+ require_relative "share/creates_identifier"
6
+
7
+ module Mocktail
8
+ class RaisesNeatoNoMethodError
9
+ extend T::Sig
10
+
11
+ sig { void }
12
+ def initialize
13
+ @stringifies_call = T.let(StringifiesCall.new, StringifiesCall)
14
+ @stringifies_method_name = T.let(StringifiesMethodName.new, StringifiesMethodName)
15
+ @creates_identifier = T.let(CreatesIdentifier.new, CreatesIdentifier)
16
+ end
17
+
18
+ sig { params(call: Call).void }
19
+ def call(call)
20
+ raise NoMethodError, <<~MSG, caller[1..]
21
+ No method `#{@stringifies_method_name.stringify(call)}' exists for call:
22
+
23
+ #{@stringifies_call.stringify(call, anonymous_blocks: true, always_parens: true)}
24
+
25
+ Need to define the method? Here's a sample definition:
26
+
27
+ def #{"self." if call.singleton}#{call.method}#{params(call)}
28
+ end
29
+ #{corrections(call)}
30
+ MSG
31
+ end
32
+
33
+ private
34
+
35
+ sig { params(call: Call).returns(T.nilable(String)) }
36
+ def params(call)
37
+ return if (params_lists = [
38
+ params_list(call.args),
39
+ kwparams_list(call.kwargs),
40
+ block_param(call.block)
41
+ ].compact).empty?
42
+
43
+ "(#{params_lists.join(", ")})"
44
+ end
45
+
46
+ sig { params(args: T::Array[T.anything]).returns(T.nilable(String)) }
47
+ def params_list(args)
48
+ return if args.empty?
49
+
50
+ count_repeats(args.map { |arg|
51
+ @creates_identifier.create(arg, default: "arg")
52
+ }).join(", ")
53
+ end
54
+
55
+ sig { params(kwargs: T::Hash[Symbol, T.anything]).returns(T.nilable(String)) }
56
+ def kwparams_list(kwargs)
57
+ return if kwargs.empty?
58
+
59
+ kwargs.keys.map { |key| "#{key}:" }.join(", ")
60
+ end
61
+
62
+ sig { params(block: T.nilable(Proc)).returns(T.nilable(String)) }
63
+ def block_param(block)
64
+ return if block.nil?
65
+
66
+ "&blk"
67
+ end
68
+
69
+ sig { params(identifiers: T::Array[String]).returns(T::Array[String]) }
70
+ def count_repeats(identifiers)
71
+ identifiers.map.with_index { |id, i|
72
+ if (preceding_matches = T.must(identifiers[0...i]).count { |other_id| id == other_id }) > 0
73
+ "#{id}#{preceding_matches + 1}"
74
+ else
75
+ id
76
+ end
77
+ }
78
+ end
79
+
80
+ sig { params(call: Call).returns(T.nilable(String)) }
81
+ def corrections(call)
82
+ return if (corrections = DidYouMean::SpellChecker.new(dictionary: T.must(call.original_type).instance_methods).correct(T.must(call.method))).empty?
83
+
84
+ <<~MSG
85
+
86
+ There #{(corrections.size == 1) ? "is" : "are"} also #{corrections.size} similar method#{"s" if corrections.size != 1} on #{T.must(call.original_type).name}.
87
+
88
+ Did you mean?
89
+ #{corrections.map { |c| " #{c}" }.join("\n")}
90
+ MSG
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,43 @@
1
+ # typed: strict
2
+
3
+ module Mocktail
4
+ class RecordsDemonstration
5
+ extend T::Sig
6
+
7
+ sig {
8
+ type_parameters(:T)
9
+ .params(
10
+ demonstration: T.proc.params(matchers: Mocktail::MatcherPresentation).returns(T.type_parameter(:T)),
11
+ demo_config: DemoConfig
12
+ ).returns(Call)
13
+ }
14
+ def record(demonstration, demo_config)
15
+ cabinet = Mocktail.cabinet
16
+ prior_call_count = Mocktail.cabinet.calls.dup.size
17
+
18
+ begin
19
+ cabinet.demonstration_in_progress = true
20
+ ValidatesArguments.optional(demo_config.ignore_arity) do
21
+ demonstration.call(Mocktail.matchers)
22
+ end
23
+ ensure
24
+ cabinet.demonstration_in_progress = false
25
+ end
26
+
27
+ if prior_call_count + 1 == cabinet.calls.size
28
+ T.must(cabinet.calls.pop)
29
+ elsif prior_call_count == cabinet.calls.size
30
+ raise MissingDemonstrationError.new <<~MSG.tr("\n", " ")
31
+ `stubs` & `verify` expect an invocation of a mocked method by a passed
32
+ block, but no invocation occurred.
33
+ MSG
34
+ else
35
+ raise AmbiguousDemonstrationError.new <<~MSG.tr("\n", " ")
36
+ `stubs` & `verify` expect exactly one invocation of a mocked method,
37
+ but #{cabinet.calls.size - prior_call_count} were detected. As a
38
+ result, Mocktail doesn't know which invocation to stub or verify.
39
+ MSG
40
+ end
41
+ end
42
+ end
43
+ end