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,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