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,39 @@
1
+ # typed: strict
2
+
3
+ module Mocktail
4
+ class RunsSorbetSigBlocksBeforeReplacement
5
+ extend T::Sig
6
+
7
+ # This is necessary because when Sorbet runs a sig block of a singleton
8
+ # method, it has the net effect of unwrapping/redefining the method. If
9
+ # we try to use Mocktail.replace(Foo) and Foo.bar has a Sorbet sig block,
10
+ # then we'll end up with three "versions" of the same method and no way
11
+ # to keep straight which one == which:
12
+ #
13
+ # A - Foo.bar, as defined in the original class
14
+ # B - Foo.bar, as redefined by RedefinesSingletonMethods
15
+ # C - Foo.bar, as wrapped by sorbet-runtime
16
+ #
17
+ # Initially, Foo.method(:bar) would == C, but after the type
18
+ # replacement, it would == B (with a reference back to C as the original),
19
+ # but after handling a single dry call, our invocation of
20
+ # GrabsOriginalMethodParameters.grab(Foo.method(:bar)) would invoke the
21
+ # Sorbet `sig` block, which has the net effect of redefining the method back
22
+ # to A.
23
+ #
24
+ # It's very fun and confusing and a great time.
25
+ sig { params(type: T.any(T::Class[T.anything], Module)).void }
26
+ def run(type)
27
+ return unless defined?(T::Private::Methods)
28
+
29
+ type.singleton_methods.each do |method_name|
30
+ method = type.method(method_name)
31
+
32
+ # Again: calling this for the side effect of running the sig block
33
+ #
34
+ # https://github.com/sorbet/sorbet/blob/master/gems/sorbet-runtime/lib/types/private/methods/_methods.rb#L111
35
+ T::Private::Methods.signature_for_method(method)
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,36 @@
1
+ # typed: strict
2
+
3
+ require_relative "replaces_type/redefines_new"
4
+ require_relative "replaces_type/redefines_singleton_methods"
5
+ require_relative "replaces_type/runs_sorbet_sig_blocks_before_replacement"
6
+
7
+ module Mocktail
8
+ class ReplacesType
9
+ extend T::Sig
10
+
11
+ sig { void }
12
+ def initialize
13
+ @top_shelf = T.let(TopShelf.instance, TopShelf)
14
+ @runs_sorbet_sig_blocks_before_replacement = T.let(RunsSorbetSigBlocksBeforeReplacement.new, RunsSorbetSigBlocksBeforeReplacement)
15
+ @redefines_new = T.let(RedefinesNew.new, RedefinesNew)
16
+ @redefines_singleton_methods = T.let(RedefinesSingletonMethods.new, RedefinesSingletonMethods)
17
+ end
18
+
19
+ sig { params(type: T.any(T::Class[T.anything], Module)).void }
20
+ def replace(type)
21
+ unless T.unsafe(type).is_a?(Class) || T.unsafe(type).is_a?(Module)
22
+ raise UnsupportedMocktail.new("Mocktail.replace() only supports classes and modules")
23
+ end
24
+
25
+ @runs_sorbet_sig_blocks_before_replacement.run(type)
26
+
27
+ if type.is_a?(Class)
28
+ @top_shelf.register_new_replacement!(type)
29
+ @redefines_new.redefine(type)
30
+ end
31
+
32
+ @top_shelf.register_singleton_method_replacement!(type)
33
+ @redefines_singleton_methods.redefine(type)
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,14 @@
1
+ # typed: strict
2
+
3
+ module Mocktail
4
+ class ResetsState
5
+ extend T::Sig
6
+
7
+ sig { void }
8
+ def reset
9
+ TopShelf.instance.reset_current_thread!
10
+ Mocktail.cabinet.reset!
11
+ ValidatesArguments.enable!
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,18 @@
1
+ # typed: true
2
+
3
+ module Mocktail
4
+ module Bind
5
+ # sig intentionally omitted, because the wrapper will cause infinite recursion if certain methods are mocked
6
+ def self.call(mock, method_name, *args, **kwargs, &blk) # standard:disable Style/ArgumentsForwarding
7
+ if Mocktail.cabinet.double_for_instance(mock)
8
+ T.unsafe(Object.instance_method(method_name)).bind_call(mock, *args, **kwargs, &blk) # standard:disable Style/ArgumentsForwarding
9
+ elsif (mock.is_a?(Module) || mock.is_a?(Class)) &&
10
+ (type_replacement = TopShelf.instance.type_replacement_if_exists_for(mock)) &&
11
+ (og_method = type_replacement.original_methods&.find { |m| m.name == method_name })
12
+ T.unsafe(og_method).call(*args, **kwargs, &blk) # standard:disable Style/ArgumentsForwarding
13
+ else
14
+ T.unsafe(mock).__send__(method_name, *args, **kwargs, &blk) # standard:disable Style/ArgumentsForwarding
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,22 @@
1
+ # typed: strict
2
+
3
+ module Mocktail
4
+ class CleansBacktrace
5
+ extend T::Sig
6
+
7
+ sig {
8
+ type_parameters(:T)
9
+ .params(error: T.all(T.type_parameter(:T), StandardError))
10
+ .returns(T.type_parameter(:T))
11
+ }
12
+ def clean(error)
13
+ raise error
14
+ rescue error.class => e
15
+ T.cast(e, T.all(T.type_parameter(:T), StandardError)).tap do |e|
16
+ e.set_backtrace(e.backtrace.drop_while { |frame|
17
+ frame.start_with?(BASE_PATH, BASE_PATH) || frame.match?(/[\\|\/]sorbet-runtime.*[\\|\/]lib[\\|\/]types[\\|\/]private/)
18
+ })
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,39 @@
1
+ # typed: strict
2
+
3
+ module Mocktail
4
+ class CreatesIdentifier
5
+ extend T::Sig
6
+
7
+ KEYWORDS = T.let(%w[__FILE__ __LINE__ alias and begin BEGIN break case class def defined? do else elsif end END ensure false for if in module next nil not or redo rescue retry return self super then true undef unless until when while yield], T::Array[String])
8
+
9
+ sig { params(s: T.anything, default: String, max_length: Integer).returns(String) }
10
+ def create(s, default: "identifier", max_length: 24)
11
+ case s
12
+ when Kernel
13
+ id = (s.to_s.downcase
14
+ .gsub(/:0x[0-9a-f]+/, "") # Lazy attempt to wipe any Object:0x802beef identifiers
15
+ .gsub(/[^\w\s]/, "")
16
+ .gsub(/^\d+/, "")[0...max_length] || "")
17
+ .strip
18
+ .gsub(/\s+/, "_") # snake_case
19
+
20
+ if id.empty?
21
+ default
22
+ else
23
+ unreserved(id, default)
24
+ end
25
+ else
26
+ default
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ sig { params(id: String, default: String).returns(String) }
33
+ def unreserved(id, default)
34
+ return id unless KEYWORDS.include?(id)
35
+
36
+ "#{id}_#{default}"
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,72 @@
1
+ # typed: strict
2
+
3
+ require_relative "bind"
4
+
5
+ module Mocktail
6
+ class DeterminesMatchingCalls
7
+ extend T::Sig
8
+
9
+ sig { params(real_call: Call, demo_call: Call, demo_config: DemoConfig).returns(T::Boolean) }
10
+ def determine(real_call, demo_call, demo_config)
11
+ T.cast(Bind.call(real_call.double, :==, demo_call.double), T::Boolean) &&
12
+ real_call.method == demo_call.method &&
13
+
14
+ # Matcher implementation will replace this:
15
+ args_match?(real_call.args, demo_call.args, demo_config.ignore_extra_args) &&
16
+ kwargs_match?(real_call.kwargs, demo_call.kwargs, demo_config.ignore_extra_args) &&
17
+ blocks_match?(real_call.block, demo_call.block, demo_config.ignore_block)
18
+ end
19
+
20
+ private
21
+
22
+ sig { params(real_args: T::Array[T.untyped], demo_args: T::Array[T.untyped], ignore_extra_args: T::Boolean).returns(T::Boolean) }
23
+ def args_match?(real_args, demo_args, ignore_extra_args)
24
+ # Guard clause for performance:
25
+ return true if ignore_extra_args && demo_args.empty?
26
+
27
+ (
28
+ real_args.size == demo_args.size ||
29
+ (ignore_extra_args && real_args.size >= demo_args.size)
30
+ ) &&
31
+ demo_args.each.with_index.all? { |demo_arg, i|
32
+ match?(real_args[i], demo_arg)
33
+ }
34
+ end
35
+
36
+ sig { params(real_kwargs: T::Hash[Symbol, T.untyped], demo_kwargs: T::Hash[Symbol, T.untyped], ignore_extra_args: T::Boolean).returns(T::Boolean) }
37
+ def kwargs_match?(real_kwargs, demo_kwargs, ignore_extra_args)
38
+ return true if ignore_extra_args && demo_kwargs.empty?
39
+
40
+ (
41
+ real_kwargs.size == demo_kwargs.size ||
42
+ (ignore_extra_args && real_kwargs.size >= demo_kwargs.size)
43
+ ) &&
44
+ demo_kwargs.all? { |key, demo_val|
45
+ real_kwargs.key?(key) && match?(real_kwargs[key], demo_val)
46
+ }
47
+ end
48
+
49
+ sig { params(real_block: T.nilable(Proc), demo_block: T.nilable(Proc), ignore_block: T::Boolean).returns(T::Boolean) }
50
+ def blocks_match?(real_block, demo_block, ignore_block)
51
+ !!(ignore_block ||
52
+ (real_block.nil? && demo_block.nil?) ||
53
+ (
54
+ real_block && demo_block &&
55
+ (
56
+ demo_block == real_block ||
57
+ demo_block.call(real_block)
58
+ )
59
+ ))
60
+ end
61
+
62
+ sig { params(real_arg: T.untyped, demo_arg: T.untyped).returns(T::Boolean) }
63
+ def match?(real_arg, demo_arg)
64
+ if Bind.call(demo_arg, :respond_to?, :is_mocktail_matcher?) &&
65
+ demo_arg.is_mocktail_matcher?
66
+ demo_arg.match?(real_arg)
67
+ else
68
+ Bind.call(demo_arg, :==, real_arg)
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,84 @@
1
+ # typed: strict
2
+
3
+ module Mocktail
4
+ class StringifiesCall
5
+ extend T::Sig
6
+
7
+ sig { params(call: Call, anonymous_blocks: T::Boolean, always_parens: T::Boolean).returns(String) }
8
+ def stringify(call, anonymous_blocks: false, always_parens: false)
9
+ "#{call.method}#{args_to_s(call, parens: always_parens)}#{blockify(call.block, anonymous: anonymous_blocks)}"
10
+ end
11
+
12
+ sig { params(calls: T::Array[Call], nonzero_message: String, zero_message: String, anonymous_blocks: T::Boolean, always_parens: T::Boolean).returns(String) }
13
+ def stringify_multiple(calls, nonzero_message:, zero_message:,
14
+ anonymous_blocks: false, always_parens: false)
15
+ if calls.empty?
16
+ "#{zero_message}.\n"
17
+ else
18
+ <<~MSG
19
+ #{nonzero_message}:
20
+
21
+ #{calls.map { |call| " " + stringify(call) }.join("\n\n")}
22
+ MSG
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ sig { params(call: Call, parens: T::Boolean).returns(T.nilable(String)) }
29
+ def args_to_s(call, parens: true)
30
+ args_lists = [
31
+ argify(call.args),
32
+ kwargify(call.kwargs),
33
+ lambdafy(call.block)
34
+ ].compact
35
+
36
+ if !args_lists.empty?
37
+ "(#{args_lists.join(", ")})"
38
+ elsif parens
39
+ "()"
40
+ else
41
+ ""
42
+ end
43
+ end
44
+
45
+ sig { params(args: T::Array[Object]).returns(T.nilable(String)) }
46
+ def argify(args)
47
+ return unless !args.empty?
48
+ args.map(&:inspect).join(", ")
49
+ end
50
+
51
+ sig { params(kwargs: T::Hash[Symbol, Object]).returns(T.nilable(String)) }
52
+ def kwargify(kwargs)
53
+ return unless !kwargs.empty?
54
+ kwargs.map { |key, val| "#{key}: #{val.inspect}" }.join(", ")
55
+ end
56
+
57
+ sig { params(block: T.nilable(Proc)).returns(T.nilable(String)) }
58
+ def lambdafy(block)
59
+ return unless block&.lambda?
60
+ "&lambda[#{source_locationify(block)}]"
61
+ end
62
+
63
+ sig { params(block: T.nilable(Proc), anonymous: T::Boolean).returns(T.nilable(String)) }
64
+ def blockify(block, anonymous:)
65
+ return unless block && !block.lambda?
66
+
67
+ if anonymous
68
+ " {…}"
69
+ else
70
+ " { Proc at #{source_locationify(block)} }"
71
+ end
72
+ end
73
+
74
+ sig { params(block: Proc).returns(String) }
75
+ def source_locationify(block)
76
+ "#{strip_pwd(block.source_location[0])}:#{block.source_location[1]}"
77
+ end
78
+
79
+ sig { params(path: String).returns(String) }
80
+ def strip_pwd(path)
81
+ path.gsub(Dir.pwd + File::SEPARATOR, "")
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,16 @@
1
+ # typed: strict
2
+
3
+ module Mocktail
4
+ class StringifiesMethodName
5
+ extend T::Sig
6
+
7
+ sig { params(call: Call).returns(String) }
8
+ def stringify(call)
9
+ [
10
+ call.original_type&.name,
11
+ call.singleton ? "." : "#",
12
+ call.method
13
+ ].join
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,27 @@
1
+ # typed: strict
2
+
3
+ module Mocktail
4
+ class ReconcilesArgsWithParams
5
+ extend T::Sig
6
+
7
+ sig { params(signature: Signature).returns(T::Boolean) }
8
+ def reconcile(signature)
9
+ args_match?(signature.positional_params, signature.positional_args) &&
10
+ kwargs_match?(signature.keyword_params, signature.keyword_args)
11
+ end
12
+
13
+ private
14
+
15
+ sig { params(arg_params: Params, args: T::Array[T.untyped]).returns(T::Boolean) }
16
+ def args_match?(arg_params, args)
17
+ args.size >= arg_params.required.size &&
18
+ (arg_params.rest? || args.size <= arg_params.allowed.size)
19
+ end
20
+
21
+ sig { params(kwarg_params: Params, kwargs: T::Hash[Symbol, T.untyped]).returns(T::Boolean) }
22
+ def kwargs_match?(kwarg_params, kwargs)
23
+ kwarg_params.required.all? { |name| kwargs.key?(name) } &&
24
+ (kwarg_params.rest? || kwargs.keys.all? { |name| kwarg_params.allowed.include?(name) })
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,34 @@
1
+ # typed: strict
2
+
3
+ module Mocktail
4
+ class RecreatesMessage
5
+ extend T::Sig
6
+
7
+ sig { params(signature: Signature).returns(String) }
8
+ def recreate(signature)
9
+ req_args = signature.positional_params.required.size
10
+ allowed_args = signature.positional_params.allowed.size
11
+ rest_args = signature.positional_params.rest?
12
+ req_kwargs = signature.keyword_params.required
13
+
14
+ if signature.positional_args.size < req_args || (!rest_args && signature.positional_args.size > allowed_args)
15
+ expected_desc = if rest_args
16
+ "#{req_args}+"
17
+ elsif allowed_args != req_args
18
+ "#{req_args}..#{allowed_args}"
19
+ else
20
+ req_args.to_s
21
+ end
22
+
23
+ "wrong number of arguments (given #{signature.positional_args.size}, expected #{expected_desc}#{"; required keyword#{"s" if req_kwargs.size > 1}: #{req_kwargs.join(", ")}" unless req_kwargs.empty?})"
24
+
25
+ elsif !(missing_kwargs = req_kwargs.reject { |name| signature.keyword_args.key?(name) }).empty?
26
+ "missing keyword#{"s" if missing_kwargs.size > 1}: #{missing_kwargs.map { |name| name.inspect }.join(", ")}"
27
+ elsif !(unknown_kwargs = signature.keyword_args.keys.reject { |name| signature.keyword_params.all.include?(name) }).empty?
28
+ "unknown keyword#{"s" if unknown_kwargs.size > 1}: #{unknown_kwargs.map { |name| name.inspect }.join(", ")}"
29
+ else
30
+ "unknown cause (this is probably a bug in Mocktail)"
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,58 @@
1
+ # typed: strict
2
+
3
+ require_relative "../share/bind"
4
+
5
+ module Mocktail
6
+ class TransformsParams
7
+ extend T::Sig
8
+
9
+ sig { void }
10
+ def initialize
11
+ @grabs_original_method_parameters = T.let(GrabsOriginalMethodParameters.new, GrabsOriginalMethodParameters)
12
+ end
13
+
14
+ sig { params(dry_call: Call, params: T.nilable(T::Array[T::Array[Symbol]])).returns(Signature) }
15
+ def transform(dry_call, params: nil)
16
+ params ||= @grabs_original_method_parameters.grab(dry_call.original_method)
17
+ params = name_unnamed_params(params)
18
+
19
+ Signature.new(
20
+ positional_params: Params.new(
21
+ all: params.select { |t, _|
22
+ [:req, :opt, :rest].any? { |param_type| Bind.call(t, :==, param_type) }
23
+ }.map { |pair| pair.fetch(1) },
24
+ required: params.select { |t, _| Bind.call(t, :==, :req) }.map { |pair| pair.fetch(1) },
25
+ optional: params.select { |t, _| Bind.call(t, :==, :opt) }.map { |pair| pair.fetch(1) },
26
+ rest: params.find { |t, _| Bind.call(t, :==, :rest) }&.last
27
+ ),
28
+ positional_args: dry_call.args,
29
+
30
+ keyword_params: Params.new(
31
+ all: params.select { |type, _| # standard:disable Style/HashSlice
32
+ [:keyreq, :key, :keyrest].include?(type)
33
+ }.map { |pair| pair.fetch(1) },
34
+ required: params.select { |t, _| Bind.call(t, :==, :keyreq) }.map { |pair| pair.fetch(1) },
35
+ optional: params.select { |t, _| Bind.call(t, :==, :key) }.map { |pair| pair.fetch(1) },
36
+ rest: params.find { |t, _| Bind.call(t, :==, :keyrest) }&.last
37
+ ),
38
+ keyword_args: dry_call.kwargs,
39
+
40
+ block_param: params.find { |t, _| Bind.call(t, :==, :block) }&.last,
41
+ block_arg: dry_call.block
42
+ )
43
+ end
44
+
45
+ private
46
+
47
+ sig { params(params: T::Array[T::Array[Symbol]]).returns(T::Array[T::Array[Symbol]]) }
48
+ def name_unnamed_params(params)
49
+ params.map.with_index { |param, i|
50
+ if param.size == 1
51
+ param + [:"unnamed_arg_#{i + 1}"]
52
+ else
53
+ param
54
+ end
55
+ }
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,36 @@
1
+ # typed: strict
2
+
3
+ require_relative "simulates_argument_error/transforms_params"
4
+ require_relative "simulates_argument_error/reconciles_args_with_params"
5
+ require_relative "simulates_argument_error/recreates_message"
6
+ require_relative "share/cleans_backtrace"
7
+ require_relative "share/stringifies_call"
8
+
9
+ module Mocktail
10
+ class SimulatesArgumentError
11
+ extend T::Sig
12
+
13
+ sig { void }
14
+ def initialize
15
+ @transforms_params = T.let(TransformsParams.new, TransformsParams)
16
+ @reconciles_args_with_params = T.let(ReconcilesArgsWithParams.new, ReconcilesArgsWithParams)
17
+ @recreates_message = T.let(RecreatesMessage.new, RecreatesMessage)
18
+ @cleans_backtrace = T.let(CleansBacktrace.new, CleansBacktrace)
19
+ @stringifies_call = T.let(StringifiesCall.new, StringifiesCall)
20
+ end
21
+
22
+ sig { params(dry_call: Call).returns(T.nilable(ArgumentError)) }
23
+ def simulate(dry_call)
24
+ signature = @transforms_params.transform(dry_call)
25
+
26
+ unless @reconciles_args_with_params.reconcile(signature)
27
+ @cleans_backtrace.clean(
28
+ ArgumentError.new([
29
+ @recreates_message.recreate(signature),
30
+ "[Mocktail call: `#{@stringifies_call.stringify(dry_call)}']"
31
+ ].join(" "))
32
+ )
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,3 @@
1
+ # typed: strict
2
+
3
+ require_relative "sorbet/mocktail"
@@ -0,0 +1,53 @@
1
+ # typed: strict
2
+
3
+ module Mocktail
4
+ class StringifiesMethodSignature
5
+ extend T::Sig
6
+
7
+ sig { params(signature: Signature).returns(String) }
8
+ def stringify(signature)
9
+ positional_params = positional(signature)
10
+ keyword_params = keyword(signature)
11
+ block_param = block(signature)
12
+
13
+ "(#{[positional_params, keyword_params, block_param].compact.join(", ")})"
14
+ end
15
+
16
+ private
17
+
18
+ sig { params(signature: Signature).returns(T.nilable(String)) }
19
+ def positional(signature)
20
+ params = signature.positional_params.all.map do |name|
21
+ if signature.positional_params.allowed.include?(name)
22
+ "#{name} = ((__mocktail_default_args ||= {})[:#{name}] = nil)"
23
+ elsif signature.positional_params.rest == name
24
+ "*#{(name == :*) ? Signature::DEFAULT_REST_ARGS : name}"
25
+ end
26
+ end.compact
27
+
28
+ params.join(", ") if params.any?
29
+ end
30
+
31
+ sig { params(signature: Signature).returns(T.nilable(String)) }
32
+ def keyword(signature)
33
+ params = signature.keyword_params.all.map do |name|
34
+ if signature.keyword_params.allowed.include?(name)
35
+ "#{name}: ((__mocktail_default_args ||= {})[:#{name}] = nil)"
36
+ elsif signature.keyword_params.rest == name
37
+ "**#{(name == :**) ? Signature::DEFAULT_REST_KWARGS : name}"
38
+ end
39
+ end.compact
40
+
41
+ params.join(", ") if params.any?
42
+ end
43
+
44
+ sig { params(signature: Signature).returns(String) }
45
+ def block(signature)
46
+ if signature.block_param && signature.block_param != :&
47
+ "&#{signature.block_param}"
48
+ else
49
+ "&#{Signature::DEFAULT_BLOCK_PARAM}"
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,5 @@
1
+ # typed: strict
2
+
3
+ module Mocktail
4
+ TYPED = true
5
+ end
@@ -0,0 +1,91 @@
1
+ # typed: strict
2
+
3
+ require_relative "../share/bind"
4
+
5
+ # The Cabinet stores all thread-local state, so anything that goes here
6
+ # is guaranteed by Mocktail to be local to the currently-running thread
7
+ module Mocktail
8
+ class Cabinet
9
+ extend T::Sig
10
+
11
+ sig { params(demonstration_in_progress: T::Boolean).void }
12
+ attr_writer :demonstration_in_progress
13
+
14
+ sig { returns(T::Array[Call]) }
15
+ attr_reader :calls
16
+
17
+ sig { returns(T::Array[Stubbing[T.anything]]) }
18
+ attr_reader :stubbings
19
+
20
+ sig { returns(T::Array[UnsatisfyingCall]) }
21
+ attr_reader :unsatisfying_calls
22
+
23
+ sig { void }
24
+ def initialize
25
+ @doubles = T.let([], T::Array[Double])
26
+ @calls = T.let([], T::Array[Call])
27
+ @stubbings = T.let([], T::Array[Stubbing[T.anything]])
28
+ @unsatisfying_calls = T.let([], T::Array[UnsatisfyingCall])
29
+ @demonstration_in_progress = T.let(false, T::Boolean)
30
+ end
31
+
32
+ sig { void }
33
+ def reset!
34
+ @calls = []
35
+ @stubbings = []
36
+ @unsatisfying_calls = []
37
+ # Could cause an exception or prevent pollution—you decide!
38
+ @demonstration_in_progress = false
39
+ # note we don't reset doubles as they don't carry any
40
+ # user-meaningful state on them, and clearing them on reset could result
41
+ # in valid mocks being broken and stop working
42
+ end
43
+
44
+ sig { params(double: Double).void }
45
+ def store_double(double)
46
+ @doubles << double
47
+ end
48
+
49
+ sig { params(call: Call).void }
50
+ def store_call(call)
51
+ @calls << call
52
+ end
53
+
54
+ sig { params(stubbing: Stubbing[T.anything]).void }
55
+ def store_stubbing(stubbing)
56
+ @stubbings << stubbing
57
+ end
58
+
59
+ sig { params(unsatisfying_call: UnsatisfyingCall).void }
60
+ def store_unsatisfying_call(unsatisfying_call)
61
+ @unsatisfying_calls << unsatisfying_call
62
+ end
63
+
64
+ sig { returns(T::Boolean) }
65
+ def demonstration_in_progress?
66
+ @demonstration_in_progress
67
+ end
68
+
69
+ sig { params(thing: T.anything).returns(T.nilable(Double)) }
70
+ def double_for_instance(thing)
71
+ @doubles.find { |double|
72
+ # Intentionally calling directly to avoid an infinite recursion in Bind.call
73
+ Object.instance_method(:==).bind_call(double.dry_instance, thing)
74
+ }
75
+ end
76
+
77
+ sig { params(double: Double).returns(T::Array[Stubbing[T.anything]]) }
78
+ def stubbings_for_double(double)
79
+ @stubbings.select { |stubbing|
80
+ Bind.call(stubbing.recording.double, :==, double.dry_instance)
81
+ }
82
+ end
83
+
84
+ sig { params(double: Double).returns(T::Array[Call]) }
85
+ def calls_for_double(double)
86
+ @calls.select { |call|
87
+ Bind.call(call.double, :==, double.dry_instance)
88
+ }
89
+ end
90
+ end
91
+ end