mocktail 2.0.0 → 3.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 (279) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/main.yml +2 -2
  3. data/CHANGELOG.md +5 -0
  4. data/Gemfile +3 -2
  5. data/Gemfile.lock +76 -82
  6. data/README.md +2 -0
  7. data/docs/code_of_conduct.md +8 -0
  8. data/docs/configuring_minitest.md +20 -0
  9. data/docs/configuring_rspec.md +19 -0
  10. data/docs/example_test.md +594 -0
  11. data/docs/faq/existing_tests.md +47 -0
  12. data/docs/faq/mocking_http.md +151 -0
  13. data/docs/faq/mocking_the_subject.md +161 -0
  14. data/docs/faq/mocking_time.md +34 -0
  15. data/docs/faq/partial_mocks.md +34 -0
  16. data/docs/faq/verifying_real_interactions.md +52 -0
  17. data/docs/img/delegator_tree.png +0 -0
  18. data/docs/img/example_test.png +0 -0
  19. data/docs/img/extract_transform_load.png +0 -0
  20. data/docs/img/mocktail_sorbet.jpg +0 -0
  21. data/docs/img/mocktail_untyped.jpg +0 -0
  22. data/docs/img/spacer.png +0 -0
  23. data/docs/installation_sorbet.md +99 -0
  24. data/docs/installation_untyped.md +29 -0
  25. data/docs/other_uses.md +32 -0
  26. data/docs/stubbing_and_verifying.md +515 -0
  27. data/docs/support/api.md +819 -0
  28. data/docs/support/example_test.rb +94 -0
  29. data/docs/support/glossary.md +426 -0
  30. data/docs/tdd/class_methods.md +44 -0
  31. data/docs/tdd/poro/dependency_inception.md +136 -0
  32. data/docs/tdd/poro/dependency_injection.md +82 -0
  33. data/docs/tdd/poro.md +15 -0
  34. data/docs/tdd/third_party.md +220 -0
  35. data/docs/tdd.md +15 -0
  36. data/lib/mocktail/debug.rb +4 -3
  37. data/lib/mocktail/share/stringifies_call.rb +0 -1
  38. data/lib/mocktail/simulates_argument_error/transforms_params.rb +2 -2
  39. data/lib/mocktail/sorbet/mocktail/debug.rb +4 -3
  40. data/lib/mocktail/sorbet/mocktail/share/stringifies_call.rb +0 -1
  41. data/lib/mocktail/sorbet/mocktail/simulates_argument_error/transforms_params.rb +2 -2
  42. data/lib/mocktail/sorbet/mocktail/value/call.rb +1 -1
  43. data/lib/mocktail/sorbet/mocktail/version.rb +1 -1
  44. data/lib/mocktail/value/call.rb +1 -1
  45. data/lib/mocktail/version.rb +1 -1
  46. data/mocktail.gemspec +2 -2
  47. data/rbi/mocktail-pregenerated.rbi +58 -0
  48. data/script/build +47 -0
  49. data/script/setup +17 -0
  50. data/script/spoom_me +7 -0
  51. data/script/strip_sigils +17 -0
  52. data/script/test +33 -0
  53. data/script/test_double_require_warnings +46 -0
  54. data/script/update +21 -0
  55. data/sorbet/config +12 -0
  56. data/sorbet/rbi/annotations/rainbow.rbi +269 -0
  57. data/sorbet/rbi/gems/ast@2.4.2.rbi +584 -0
  58. data/sorbet/rbi/gems/diff-lcs@1.5.0.rbi +1083 -0
  59. data/sorbet/rbi/gems/docile@1.4.0.rbi +376 -0
  60. data/sorbet/rbi/gems/json@2.6.3.rbi +1533 -0
  61. data/sorbet/rbi/gems/language_server-protocol@3.17.0.3.rbi +14237 -0
  62. data/sorbet/rbi/gems/lint_roller@1.0.0.rbi +230 -0
  63. data/sorbet/rbi/gems/m@1.6.1.rbi +257 -0
  64. data/sorbet/rbi/gems/method_source@1.0.0.rbi +272 -0
  65. data/sorbet/rbi/gems/minitest@5.18.0.rbi +1491 -0
  66. data/sorbet/rbi/gems/netrc@0.11.0.rbi +158 -0
  67. data/sorbet/rbi/gems/parallel@1.23.0.rbi +273 -0
  68. data/sorbet/rbi/gems/parser@3.2.2.1.rbi +7253 -0
  69. data/sorbet/rbi/gems/rainbow@3.1.1.rbi +402 -0
  70. data/sorbet/rbi/gems/rake@13.0.6.rbi +3024 -0
  71. data/sorbet/rbi/gems/rbi@0.0.16.rbi +3049 -0
  72. data/sorbet/rbi/gems/regexp_parser@2.8.0.rbi +3748 -0
  73. data/sorbet/rbi/gems/rexml@3.2.5.rbi +4717 -0
  74. data/sorbet/rbi/gems/rubocop-ast@1.29.0.rbi +7003 -0
  75. data/sorbet/rbi/gems/rubocop-performance@1.18.0.rbi +8 -0
  76. data/sorbet/rbi/gems/rubocop-sorbet@0.7.0.rbi +1043 -0
  77. data/sorbet/rbi/gems/rubocop@1.52.0.rbi +55500 -0
  78. data/sorbet/rbi/gems/ruby-progressbar@1.13.0.rbi +1317 -0
  79. data/sorbet/rbi/gems/simplecov-html@0.12.3.rbi +216 -0
  80. data/sorbet/rbi/gems/simplecov@0.22.0.rbi +2148 -0
  81. data/sorbet/rbi/gems/simplecov_json_formatter@0.1.4.rbi +8 -0
  82. data/sorbet/rbi/gems/spoom@1.2.1.rbi +2536 -0
  83. data/sorbet/rbi/gems/standard-custom@1.0.1.rbi +63 -0
  84. data/sorbet/rbi/gems/standard-performance@1.1.0.rbi +8 -0
  85. data/sorbet/rbi/gems/standard@1.29.0.rbi +850 -0
  86. data/sorbet/rbi/gems/tapioca@0.11.6.rbi +3301 -0
  87. data/sorbet/rbi/gems/thor@1.2.2.rbi +3965 -0
  88. data/sorbet/rbi/gems/unicode-display_width@2.4.2.rbi +65 -0
  89. data/sorbet/rbi/gems/unparser@0.6.7.rbi +4524 -0
  90. data/sorbet/rbi/gems/yard-sorbet@0.8.1.rbi +428 -0
  91. data/sorbet/rbi/gems/yard@0.9.34.rbi +18219 -0
  92. data/sorbet/tapioca/config.yml +13 -0
  93. data/sorbet/tapioca/require.rb +4 -0
  94. data/spoom_data/05f0c49.json +1 -0
  95. data/spoom_data/1657012.json +1 -0
  96. data/spoom_data/167434b.json +1 -0
  97. data/spoom_data/1d0ba5b.json +1 -0
  98. data/spoom_data/1ffa724.json +1 -0
  99. data/spoom_data/2129b3d.json +1 -0
  100. data/spoom_data/24b1c92.json +1 -0
  101. data/spoom_data/2c46aee.json +1 -0
  102. data/spoom_data/305ec0b.json +1 -0
  103. data/spoom_data/30e9528.json +1 -0
  104. data/spoom_data/4638cd5.json +1 -0
  105. data/spoom_data/47c7dad.json +1 -0
  106. data/spoom_data/4b1edef.json +1 -0
  107. data/spoom_data/4de157f.json +1 -0
  108. data/spoom_data/526e7db.json +1 -0
  109. data/spoom_data/5d093b9.json +1 -0
  110. data/spoom_data/5db3b43.json +1 -0
  111. data/spoom_data/5fe2a65.json +1 -0
  112. data/spoom_data/6891312.json +1 -0
  113. data/spoom_data/6b0fef4.json +1 -0
  114. data/spoom_data/6b83d12.json +1 -0
  115. data/spoom_data/74c83c2.json +1 -0
  116. data/spoom_data/7644ff4.json +1 -0
  117. data/spoom_data/79054db.json +1 -0
  118. data/spoom_data/814e515.json +1 -0
  119. data/spoom_data/88c3b60.json +1 -0
  120. data/spoom_data/8bd4b6e.json +1 -0
  121. data/spoom_data/93f8153.json +1 -0
  122. data/spoom_data/95242fe.json +1 -0
  123. data/spoom_data/97f4c09.json +1 -0
  124. data/spoom_data/a13d150.json +1 -0
  125. data/spoom_data/a17f215.json +1 -0
  126. data/spoom_data/b705a9d.json +1 -0
  127. data/spoom_data/ba19195.json +1 -0
  128. data/spoom_data/bef51ca.json +1 -0
  129. data/spoom_data/dbb595a.json +1 -0
  130. data/spoom_data/e8fab92.json +1 -0
  131. data/spoom_data/f166c87.json +1 -0
  132. data/spoom_data/f57992d.json +1 -0
  133. data/spoom_data/f5a1e40.json +1 -0
  134. data/spoom_data/f6d6431.json +1 -0
  135. data/spoom_data/f72b67c.json +1 -0
  136. data/spoom_data/fc2f231.json +1 -0
  137. data/src/mocktail/collects_calls.rb +18 -0
  138. data/src/mocktail/debug.rb +55 -0
  139. data/src/mocktail/dsl.rb +46 -0
  140. data/src/mocktail/errors.rb +19 -0
  141. data/src/mocktail/explains_nils.rb +41 -0
  142. data/src/mocktail/explains_thing.rb +137 -0
  143. data/src/mocktail/grabs_original_method_parameters.rb +33 -0
  144. data/src/mocktail/handles_dry_call/fulfills_stubbing/describes_unsatisfied_stubbing.rb +27 -0
  145. data/src/mocktail/handles_dry_call/fulfills_stubbing/finds_satisfaction.rb +24 -0
  146. data/src/mocktail/handles_dry_call/fulfills_stubbing.rb +45 -0
  147. data/src/mocktail/handles_dry_call/logs_call.rb +12 -0
  148. data/src/mocktail/handles_dry_call/validates_arguments.rb +45 -0
  149. data/src/mocktail/handles_dry_call.rb +25 -0
  150. data/src/mocktail/handles_dry_new_call.rb +42 -0
  151. data/src/mocktail/imitates_type/ensures_imitation_support.rb +16 -0
  152. data/src/mocktail/imitates_type/makes_double/declares_dry_class/reconstructs_call.rb +73 -0
  153. data/src/mocktail/imitates_type/makes_double/declares_dry_class.rb +136 -0
  154. data/src/mocktail/imitates_type/makes_double/gathers_fakeable_instance_methods.rb +28 -0
  155. data/src/mocktail/imitates_type/makes_double.rb +29 -0
  156. data/src/mocktail/imitates_type.rb +29 -0
  157. data/src/mocktail/initialize_based_on_type_system_mode_switching.rb +11 -0
  158. data/src/mocktail/initializes_mocktail.rb +25 -0
  159. data/src/mocktail/matcher_presentation.rb +21 -0
  160. data/src/mocktail/matchers/any.rb +27 -0
  161. data/src/mocktail/matchers/base.rb +39 -0
  162. data/src/mocktail/matchers/captor.rb +76 -0
  163. data/src/mocktail/matchers/includes.rb +32 -0
  164. data/src/mocktail/matchers/includes_hash.rb +12 -0
  165. data/src/mocktail/matchers/includes_key.rb +12 -0
  166. data/src/mocktail/matchers/includes_string.rb +12 -0
  167. data/src/mocktail/matchers/is_a.rb +17 -0
  168. data/src/mocktail/matchers/matches.rb +19 -0
  169. data/src/mocktail/matchers/not.rb +17 -0
  170. data/src/mocktail/matchers/numeric.rb +27 -0
  171. data/src/mocktail/matchers/that.rb +32 -0
  172. data/src/mocktail/matchers.rb +19 -0
  173. data/src/mocktail/raises_neato_no_method_error.rb +93 -0
  174. data/src/mocktail/records_demonstration.rb +43 -0
  175. data/src/mocktail/registers_matcher.rb +65 -0
  176. data/src/mocktail/registers_stubbing.rb +31 -0
  177. data/src/mocktail/replaces_next.rb +55 -0
  178. data/src/mocktail/replaces_type/redefines_new.rb +32 -0
  179. data/src/mocktail/replaces_type/redefines_singleton_methods.rb +80 -0
  180. data/src/mocktail/replaces_type/runs_sorbet_sig_blocks_before_replacement.rb +39 -0
  181. data/src/mocktail/replaces_type.rb +36 -0
  182. data/src/mocktail/resets_state.rb +14 -0
  183. data/src/mocktail/share/bind.rb +18 -0
  184. data/src/mocktail/share/cleans_backtrace.rb +22 -0
  185. data/src/mocktail/share/creates_identifier.rb +39 -0
  186. data/src/mocktail/share/determines_matching_calls.rb +72 -0
  187. data/src/mocktail/share/stringifies_call.rb +84 -0
  188. data/src/mocktail/share/stringifies_method_name.rb +16 -0
  189. data/src/mocktail/simulates_argument_error/reconciles_args_with_params.rb +27 -0
  190. data/src/mocktail/simulates_argument_error/recreates_message.rb +34 -0
  191. data/src/mocktail/simulates_argument_error/transforms_params.rb +58 -0
  192. data/src/mocktail/simulates_argument_error.rb +36 -0
  193. data/src/mocktail/sorbet.rb +3 -0
  194. data/src/mocktail/stringifies_method_signature.rb +53 -0
  195. data/src/mocktail/typed.rb +5 -0
  196. data/src/mocktail/value/cabinet.rb +91 -0
  197. data/src/mocktail/value/call.rb +51 -0
  198. data/src/mocktail/value/demo_config.rb +10 -0
  199. data/src/mocktail/value/double.rb +10 -0
  200. data/src/mocktail/value/double_data.rb +15 -0
  201. data/src/mocktail/value/explanation.rb +68 -0
  202. data/src/mocktail/value/explanation_data.rb +19 -0
  203. data/src/mocktail/value/fake_method_data.rb +11 -0
  204. data/src/mocktail/value/matcher_registry.rb +27 -0
  205. data/src/mocktail/value/no_explanation_data.rb +20 -0
  206. data/src/mocktail/value/signature.rb +35 -0
  207. data/src/mocktail/value/stubbing.rb +26 -0
  208. data/src/mocktail/value/top_shelf.rb +79 -0
  209. data/src/mocktail/value/type_replacement.rb +11 -0
  210. data/src/mocktail/value/type_replacement_data.rb +19 -0
  211. data/src/mocktail/value/unsatisfying_call.rb +9 -0
  212. data/src/mocktail/value/unsatisfying_call_explanation.rb +24 -0
  213. data/src/mocktail/value.rb +19 -0
  214. data/src/mocktail/verifies_call/finds_verifiable_calls.rb +21 -0
  215. data/src/mocktail/verifies_call/raises_verification_error/gathers_calls_of_method.rb +15 -0
  216. data/src/mocktail/verifies_call/raises_verification_error.rb +74 -0
  217. data/src/mocktail/verifies_call.rb +37 -0
  218. data/src/mocktail/version.rb +12 -0
  219. data/src/mocktail.rb +154 -0
  220. data/sub_projects/rbi_generator/Gemfile +4 -0
  221. data/sub_projects/rbi_generator/Gemfile.lock +63 -0
  222. data/sub_projects/rbi_generator/sorbet/rbi/gems/.gitattributes +1 -0
  223. data/sub_projects/sorbet_user/Gemfile +13 -0
  224. data/sub_projects/sorbet_user/Gemfile.lock +126 -0
  225. data/sub_projects/sorbet_user/Rakefile +10 -0
  226. data/sub_projects/sorbet_user/bin/tapioca +27 -0
  227. data/sub_projects/sorbet_user/rbi/mocktail.rbi +9 -0
  228. data/sub_projects/sorbet_user/script/test +13 -0
  229. data/sub_projects/sorbet_user/sorbet/config +4 -0
  230. data/sub_projects/sorbet_user/sorbet/rbi/gems/.gitattributes +1 -0
  231. data/sub_projects/sorbet_user/sorbet/rbi/gems/ast@2.4.3.rbi +585 -0
  232. data/sub_projects/sorbet_user/sorbet/rbi/gems/benchmark@0.4.0.rbi +618 -0
  233. data/sub_projects/sorbet_user/sorbet/rbi/gems/erubi@1.13.1.rbi +155 -0
  234. data/sub_projects/sorbet_user/sorbet/rbi/gems/json@2.21.2.rbi +2140 -0
  235. data/sub_projects/sorbet_user/sorbet/rbi/gems/language_server-protocol@3.17.0.4.rbi +14244 -0
  236. data/sub_projects/sorbet_user/sorbet/rbi/gems/lint_roller@1.1.0.rbi +239 -0
  237. data/sub_projects/sorbet_user/sorbet/rbi/gems/logger@1.7.0.rbi +963 -0
  238. data/sub_projects/sorbet_user/sorbet/rbi/gems/m@1.6.2.rbi +258 -0
  239. data/sub_projects/sorbet_user/sorbet/rbi/gems/method_source@1.1.0.rbi +9 -0
  240. data/sub_projects/sorbet_user/sorbet/rbi/gems/minitest@5.27.0.rbi +1549 -0
  241. data/sub_projects/sorbet_user/sorbet/rbi/gems/mocktail@3.0.0.rbi +1923 -0
  242. data/sub_projects/sorbet_user/sorbet/rbi/gems/netrc@0.11.0.rbi +158 -0
  243. data/sub_projects/sorbet_user/sorbet/rbi/gems/parallel@1.26.3.rbi +291 -0
  244. data/sub_projects/sorbet_user/sorbet/rbi/gems/parser@3.3.7.4.rbi +5451 -0
  245. data/sub_projects/sorbet_user/sorbet/rbi/gems/prism@1.4.0.rbi +41732 -0
  246. data/sub_projects/sorbet_user/sorbet/rbi/gems/racc@1.8.1.rbi +164 -0
  247. data/sub_projects/sorbet_user/sorbet/rbi/gems/rainbow@3.1.1.rbi +402 -0
  248. data/sub_projects/sorbet_user/sorbet/rbi/gems/rake@13.2.1.rbi +3028 -0
  249. data/sub_projects/sorbet_user/sorbet/rbi/gems/rbi@0.3.1.rbi +6599 -0
  250. data/sub_projects/sorbet_user/sorbet/rbi/gems/rbs@3.9.2.rbi +6976 -0
  251. data/sub_projects/sorbet_user/sorbet/rbi/gems/regexp_parser@2.10.0.rbi +3795 -0
  252. data/sub_projects/sorbet_user/sorbet/rbi/gems/rubocop-ast@1.43.0.rbi +7717 -0
  253. data/sub_projects/sorbet_user/sorbet/rbi/gems/rubocop-performance@1.24.0.rbi +9 -0
  254. data/sub_projects/sorbet_user/sorbet/rbi/gems/rubocop-sorbet@0.9.0.rbi +9 -0
  255. data/sub_projects/sorbet_user/sorbet/rbi/gems/rubocop@1.73.2.rbi +61306 -0
  256. data/sub_projects/sorbet_user/sorbet/rbi/gems/ruby-progressbar@1.13.0.rbi +1317 -0
  257. data/sub_projects/sorbet_user/sorbet/rbi/gems/sorbet-eraser@0.3.1.rbi +8 -0
  258. data/sub_projects/sorbet_user/sorbet/rbi/gems/spoom@1.6.1.rbi +7274 -0
  259. data/sub_projects/sorbet_user/sorbet/rbi/gems/standard-custom@1.0.2.rbi +8 -0
  260. data/sub_projects/sorbet_user/sorbet/rbi/gems/standard-performance@1.7.0.rbi +9 -0
  261. data/sub_projects/sorbet_user/sorbet/rbi/gems/standard-sorbet@0.0.3.rbi +53 -0
  262. data/sub_projects/sorbet_user/sorbet/rbi/gems/standard@1.47.0.rbi +935 -0
  263. data/sub_projects/sorbet_user/sorbet/rbi/gems/tapioca@0.16.11.rbi +3628 -0
  264. data/sub_projects/sorbet_user/sorbet/rbi/gems/thor@1.3.2.rbi +4378 -0
  265. data/sub_projects/sorbet_user/sorbet/rbi/gems/unicode-display_width@3.1.4.rbi +132 -0
  266. data/sub_projects/sorbet_user/sorbet/rbi/gems/unicode-emoji@4.2.0.rbi +254 -0
  267. data/sub_projects/sorbet_user/sorbet/rbi/gems/yard-sorbet@0.9.0.rbi +435 -0
  268. data/sub_projects/sorbet_user/sorbet/rbi/gems/yard@0.9.45.rbi +18916 -0
  269. data/sub_projects/sorbet_user/sorbet/tapioca/config.yml +13 -0
  270. data/sub_projects/sorbet_user/sorbet/tapioca/require.rb +4 -0
  271. data/sub_projects/sorbet_user/test/ensure_type_safety_test.rb +21 -0
  272. data/sub_projects/sorbet_user/test/paint_by_number_test.rb +113 -0
  273. data/sub_projects/sorbet_user/test/sorbet_test.rb +289 -0
  274. data/sub_projects/sorbet_user/test/test_helper.rb +35 -0
  275. data/sub_projects/untyped_user/Gemfile +4 -0
  276. data/sub_projects/untyped_user/Gemfile.lock +25 -0
  277. data/sub_projects/untyped_user/antitype_test.rb +54 -0
  278. data/sub_projects/untyped_user/script/test +5 -0
  279. metadata +264 -8
@@ -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
@@ -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