retrovim 0.0.3

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 (218) hide show
  1. package/.conda/retrovim.yaml +259 -0
  2. package/.conda/terax.icns +0 -0
  3. package/.conda/terax.ico +0 -0
  4. package/.conda/terax.png +0 -0
  5. package/.github/workflows/release.yml +95 -0
  6. package/.gitmodules +3 -0
  7. package/.npm/kanata_touchcursor.js +28 -0
  8. package/.npm/nvim.js +26 -0
  9. package/.npm/yazi.js +26 -0
  10. package/.npm/zsh.js +17 -0
  11. package/.pip/kanata_touchcursor/__init__.py +28 -0
  12. package/.pip/nvim/__init__.py +31 -0
  13. package/.pip/yazi/__init__.py +31 -0
  14. package/.pip/zsh/__init__.py +22 -0
  15. package/CHANGELOG.md +26 -0
  16. package/LICENSE +674 -0
  17. package/README.md +336 -0
  18. package/kanata/touchcursor.kbd +164 -0
  19. package/nvim/init.lua +813 -0
  20. package/nvim/plugins/mini.nvim/.github/DISCUSSION_TEMPLATE/q-a.yml +72 -0
  21. package/nvim/plugins/mini.nvim/.github/ISSUE_TEMPLATE/bug-report.yml +136 -0
  22. package/nvim/plugins/mini.nvim/.github/ISSUE_TEMPLATE/config.yml +5 -0
  23. package/nvim/plugins/mini.nvim/.github/ISSUE_TEMPLATE/feature-request.yml +75 -0
  24. package/nvim/plugins/mini.nvim/.github/PULL_REQUEST_TEMPLATE.md +2 -0
  25. package/nvim/plugins/mini.nvim/.github/workflows/quality-control.yml +150 -0
  26. package/nvim/plugins/mini.nvim/.pre-commit-config.yaml +20 -0
  27. package/nvim/plugins/mini.nvim/.stylua.toml +7 -0
  28. package/nvim/plugins/mini.nvim/.styluaignore +1 -0
  29. package/nvim/plugins/mini.nvim/CHANGELOG.md +1894 -0
  30. package/nvim/plugins/mini.nvim/CODE_OF_CONDUCT.md +132 -0
  31. package/nvim/plugins/mini.nvim/CONTRIBUTING.md +331 -0
  32. package/nvim/plugins/mini.nvim/LICENSE +21 -0
  33. package/nvim/plugins/mini.nvim/MAINTAINING.md +330 -0
  34. package/nvim/plugins/mini.nvim/Makefile +51 -0
  35. package/nvim/plugins/mini.nvim/README.md +261 -0
  36. package/nvim/plugins/mini.nvim/TESTING.md +1011 -0
  37. package/nvim/plugins/mini.nvim/benchmarks/starter/README.md +35 -0
  38. package/nvim/plugins/mini.nvim/benchmarks/starter/benchmark.sh +56 -0
  39. package/nvim/plugins/mini.nvim/benchmarks/starter/init-files/init_dashboard-alpha.lua +21 -0
  40. package/nvim/plugins/mini.nvim/benchmarks/starter/init-files/init_dashboard-original.lua +17 -0
  41. package/nvim/plugins/mini.nvim/benchmarks/starter/init-files/init_dashboard-starter.lua +18 -0
  42. package/nvim/plugins/mini.nvim/benchmarks/starter/init-files/init_empty.lua +4 -0
  43. package/nvim/plugins/mini.nvim/benchmarks/starter/init-files/init_starter-default.lua +7 -0
  44. package/nvim/plugins/mini.nvim/benchmarks/starter/init-files/init_startify-alpha.lua +10 -0
  45. package/nvim/plugins/mini.nvim/benchmarks/starter/init-files/init_startify-original.lua +7 -0
  46. package/nvim/plugins/mini.nvim/benchmarks/starter/init-files/init_startify-starter.lua +20 -0
  47. package/nvim/plugins/mini.nvim/benchmarks/starter/install.sh +10 -0
  48. package/nvim/plugins/mini.nvim/benchmarks/starter/make_summary.py +68 -0
  49. package/nvim/plugins/mini.nvim/benchmarks/starter/startup-summary.md +10 -0
  50. package/nvim/plugins/mini.nvim/colors/miniautumn.lua +55 -0
  51. package/nvim/plugins/mini.nvim/colors/minicyan.lua +95 -0
  52. package/nvim/plugins/mini.nvim/colors/minischeme.lua +95 -0
  53. package/nvim/plugins/mini.nvim/colors/minispring.lua +53 -0
  54. package/nvim/plugins/mini.nvim/colors/minisummer.lua +49 -0
  55. package/nvim/plugins/mini.nvim/colors/miniwinter.lua +51 -0
  56. package/nvim/plugins/mini.nvim/colors/randomhue.lua +16 -0
  57. package/nvim/plugins/mini.nvim/doc/mini-ai.txt +889 -0
  58. package/nvim/plugins/mini.nvim/doc/mini-align.txt +923 -0
  59. package/nvim/plugins/mini.nvim/doc/mini-animate.txt +937 -0
  60. package/nvim/plugins/mini.nvim/doc/mini-base16.txt +285 -0
  61. package/nvim/plugins/mini.nvim/doc/mini-basics.txt +359 -0
  62. package/nvim/plugins/mini.nvim/doc/mini-bracketed.txt +622 -0
  63. package/nvim/plugins/mini.nvim/doc/mini-bufremove.txt +123 -0
  64. package/nvim/plugins/mini.nvim/doc/mini-clue.txt +760 -0
  65. package/nvim/plugins/mini.nvim/doc/mini-cmdline.txt +392 -0
  66. package/nvim/plugins/mini.nvim/doc/mini-colors.txt +880 -0
  67. package/nvim/plugins/mini.nvim/doc/mini-comment.txt +230 -0
  68. package/nvim/plugins/mini.nvim/doc/mini-completion.txt +477 -0
  69. package/nvim/plugins/mini.nvim/doc/mini-cursorword.txt +105 -0
  70. package/nvim/plugins/mini.nvim/doc/mini-deps.txt +590 -0
  71. package/nvim/plugins/mini.nvim/doc/mini-diff.txt +661 -0
  72. package/nvim/plugins/mini.nvim/doc/mini-doc.txt +434 -0
  73. package/nvim/plugins/mini.nvim/doc/mini-extra.txt +856 -0
  74. package/nvim/plugins/mini.nvim/doc/mini-files.txt +1018 -0
  75. package/nvim/plugins/mini.nvim/doc/mini-fuzzy.txt +160 -0
  76. package/nvim/plugins/mini.nvim/doc/mini-git.txt +425 -0
  77. package/nvim/plugins/mini.nvim/doc/mini-hipatterns.txt +489 -0
  78. package/nvim/plugins/mini.nvim/doc/mini-hues.txt +420 -0
  79. package/nvim/plugins/mini.nvim/doc/mini-icons.txt +508 -0
  80. package/nvim/plugins/mini.nvim/doc/mini-indentscope.txt +485 -0
  81. package/nvim/plugins/mini.nvim/doc/mini-input.txt +932 -0
  82. package/nvim/plugins/mini.nvim/doc/mini-jump.txt +202 -0
  83. package/nvim/plugins/mini.nvim/doc/mini-jump2d.txt +525 -0
  84. package/nvim/plugins/mini.nvim/doc/mini-keymap.txt +449 -0
  85. package/nvim/plugins/mini.nvim/doc/mini-map.txt +717 -0
  86. package/nvim/plugins/mini.nvim/doc/mini-misc.txt +431 -0
  87. package/nvim/plugins/mini.nvim/doc/mini-move.txt +181 -0
  88. package/nvim/plugins/mini.nvim/doc/mini-notify.txt +434 -0
  89. package/nvim/plugins/mini.nvim/doc/mini-nvim.txt +431 -0
  90. package/nvim/plugins/mini.nvim/doc/mini-operators.txt +472 -0
  91. package/nvim/plugins/mini.nvim/doc/mini-pairs.txt +346 -0
  92. package/nvim/plugins/mini.nvim/doc/mini-pick.txt +1535 -0
  93. package/nvim/plugins/mini.nvim/doc/mini-sessions.txt +239 -0
  94. package/nvim/plugins/mini.nvim/doc/mini-snippets.txt +1291 -0
  95. package/nvim/plugins/mini.nvim/doc/mini-splitjoin.txt +536 -0
  96. package/nvim/plugins/mini.nvim/doc/mini-starter.txt +618 -0
  97. package/nvim/plugins/mini.nvim/doc/mini-statusline.txt +361 -0
  98. package/nvim/plugins/mini.nvim/doc/mini-surround.txt +863 -0
  99. package/nvim/plugins/mini.nvim/doc/mini-tabline.txt +151 -0
  100. package/nvim/plugins/mini.nvim/doc/mini-test.txt +966 -0
  101. package/nvim/plugins/mini.nvim/doc/mini-trailspace.txt +91 -0
  102. package/nvim/plugins/mini.nvim/doc/mini-visits.txt +937 -0
  103. package/nvim/plugins/mini.nvim/logo.png +0 -0
  104. package/nvim/plugins/mini.nvim/lua/mini/ai.lua +2244 -0
  105. package/nvim/plugins/mini.nvim/lua/mini/align.lua +2076 -0
  106. package/nvim/plugins/mini.nvim/lua/mini/animate.lua +2127 -0
  107. package/nvim/plugins/mini.nvim/lua/mini/base16.lua +1752 -0
  108. package/nvim/plugins/mini.nvim/lua/mini/basics.lua +772 -0
  109. package/nvim/plugins/mini.nvim/lua/mini/bracketed.lua +2013 -0
  110. package/nvim/plugins/mini.nvim/lua/mini/bufremove.lua +283 -0
  111. package/nvim/plugins/mini.nvim/lua/mini/clue.lua +2096 -0
  112. package/nvim/plugins/mini.nvim/lua/mini/cmdline.lua +1182 -0
  113. package/nvim/plugins/mini.nvim/lua/mini/colors.lua +2483 -0
  114. package/nvim/plugins/mini.nvim/lua/mini/comment.lua +586 -0
  115. package/nvim/plugins/mini.nvim/lua/mini/completion.lua +2094 -0
  116. package/nvim/plugins/mini.nvim/lua/mini/cursorword.lua +311 -0
  117. package/nvim/plugins/mini.nvim/lua/mini/deps.lua +1660 -0
  118. package/nvim/plugins/mini.nvim/lua/mini/diff.lua +1938 -0
  119. package/nvim/plugins/mini.nvim/lua/mini/doc.lua +1371 -0
  120. package/nvim/plugins/mini.nvim/lua/mini/extra.lua +2290 -0
  121. package/nvim/plugins/mini.nvim/lua/mini/files.lua +3145 -0
  122. package/nvim/plugins/mini.nvim/lua/mini/fuzzy.lua +356 -0
  123. package/nvim/plugins/mini.nvim/lua/mini/git.lua +1737 -0
  124. package/nvim/plugins/mini.nvim/lua/mini/hipatterns.lua +1042 -0
  125. package/nvim/plugins/mini.nvim/lua/mini/hues.lua +2088 -0
  126. package/nvim/plugins/mini.nvim/lua/mini/icons.lua +2254 -0
  127. package/nvim/plugins/mini.nvim/lua/mini/indentscope.lua +1158 -0
  128. package/nvim/plugins/mini.nvim/lua/mini/init.lua +427 -0
  129. package/nvim/plugins/mini.nvim/lua/mini/input.lua +2183 -0
  130. package/nvim/plugins/mini.nvim/lua/mini/jump.lua +609 -0
  131. package/nvim/plugins/mini.nvim/lua/mini/jump2d.lua +1244 -0
  132. package/nvim/plugins/mini.nvim/lua/mini/keymap.lua +880 -0
  133. package/nvim/plugins/mini.nvim/lua/mini/map.lua +1709 -0
  134. package/nvim/plugins/mini.nvim/lua/mini/misc.lua +969 -0
  135. package/nvim/plugins/mini.nvim/lua/mini/move.lua +509 -0
  136. package/nvim/plugins/mini.nvim/lua/mini/notify.lua +938 -0
  137. package/nvim/plugins/mini.nvim/lua/mini/operators.lua +1349 -0
  138. package/nvim/plugins/mini.nvim/lua/mini/pairs.lua +657 -0
  139. package/nvim/plugins/mini.nvim/lua/mini/pick.lua +3876 -0
  140. package/nvim/plugins/mini.nvim/lua/mini/sessions.lua +689 -0
  141. package/nvim/plugins/mini.nvim/lua/mini/snippets.lua +2828 -0
  142. package/nvim/plugins/mini.nvim/lua/mini/splitjoin.lua +1132 -0
  143. package/nvim/plugins/mini.nvim/lua/mini/starter.lua +1599 -0
  144. package/nvim/plugins/mini.nvim/lua/mini/statusline.lua +686 -0
  145. package/nvim/plugins/mini.nvim/lua/mini/surround.lua +2388 -0
  146. package/nvim/plugins/mini.nvim/lua/mini/tabline.lua +564 -0
  147. package/nvim/plugins/mini.nvim/lua/mini/test.lua +2516 -0
  148. package/nvim/plugins/mini.nvim/lua/mini/trailspace.lua +215 -0
  149. package/nvim/plugins/mini.nvim/lua/mini/visits.lua +1577 -0
  150. package/nvim/plugins/mini.nvim/readmes/mini-ai.md +190 -0
  151. package/nvim/plugins/mini.nvim/readmes/mini-align.md +203 -0
  152. package/nvim/plugins/mini.nvim/readmes/mini-animate.md +214 -0
  153. package/nvim/plugins/mini.nvim/readmes/mini-base16.md +198 -0
  154. package/nvim/plugins/mini.nvim/readmes/mini-basics.md +184 -0
  155. package/nvim/plugins/mini.nvim/readmes/mini-bracketed.md +193 -0
  156. package/nvim/plugins/mini.nvim/readmes/mini-bufremove.md +139 -0
  157. package/nvim/plugins/mini.nvim/readmes/mini-clue.md +260 -0
  158. package/nvim/plugins/mini.nvim/readmes/mini-cmdline.md +183 -0
  159. package/nvim/plugins/mini.nvim/readmes/mini-colors.md +169 -0
  160. package/nvim/plugins/mini.nvim/readmes/mini-comment.md +183 -0
  161. package/nvim/plugins/mini.nvim/readmes/mini-completion.md +217 -0
  162. package/nvim/plugins/mini.nvim/readmes/mini-cursorword.md +137 -0
  163. package/nvim/plugins/mini.nvim/readmes/mini-deps.md +271 -0
  164. package/nvim/plugins/mini.nvim/readmes/mini-diff.md +289 -0
  165. package/nvim/plugins/mini.nvim/readmes/mini-doc.md +198 -0
  166. package/nvim/plugins/mini.nvim/readmes/mini-extra.md +158 -0
  167. package/nvim/plugins/mini.nvim/readmes/mini-files.md +247 -0
  168. package/nvim/plugins/mini.nvim/readmes/mini-fuzzy.md +134 -0
  169. package/nvim/plugins/mini.nvim/readmes/mini-git.md +168 -0
  170. package/nvim/plugins/mini.nvim/readmes/mini-hipatterns.md +176 -0
  171. package/nvim/plugins/mini.nvim/readmes/mini-hues.md +285 -0
  172. package/nvim/plugins/mini.nvim/readmes/mini-icons.md +183 -0
  173. package/nvim/plugins/mini.nvim/readmes/mini-indentscope.md +191 -0
  174. package/nvim/plugins/mini.nvim/readmes/mini-input.md +163 -0
  175. package/nvim/plugins/mini.nvim/readmes/mini-jump.md +161 -0
  176. package/nvim/plugins/mini.nvim/readmes/mini-jump2d.md +197 -0
  177. package/nvim/plugins/mini.nvim/readmes/mini-keymap.md +205 -0
  178. package/nvim/plugins/mini.nvim/readmes/mini-map.md +191 -0
  179. package/nvim/plugins/mini.nvim/readmes/mini-misc.md +145 -0
  180. package/nvim/plugins/mini.nvim/readmes/mini-move.md +163 -0
  181. package/nvim/plugins/mini.nvim/readmes/mini-notify.md +175 -0
  182. package/nvim/plugins/mini.nvim/readmes/mini-operators.md +193 -0
  183. package/nvim/plugins/mini.nvim/readmes/mini-pairs.md +158 -0
  184. package/nvim/plugins/mini.nvim/readmes/mini-pick.md +340 -0
  185. package/nvim/plugins/mini.nvim/readmes/mini-sessions.md +164 -0
  186. package/nvim/plugins/mini.nvim/readmes/mini-snippets.md +373 -0
  187. package/nvim/plugins/mini.nvim/readmes/mini-splitjoin.md +194 -0
  188. package/nvim/plugins/mini.nvim/readmes/mini-starter.md +176 -0
  189. package/nvim/plugins/mini.nvim/readmes/mini-statusline.md +155 -0
  190. package/nvim/plugins/mini.nvim/readmes/mini-surround.md +190 -0
  191. package/nvim/plugins/mini.nvim/readmes/mini-tabline.md +156 -0
  192. package/nvim/plugins/mini.nvim/readmes/mini-test.md +175 -0
  193. package/nvim/plugins/mini.nvim/readmes/mini-trailspace.md +137 -0
  194. package/nvim/plugins/mini.nvim/readmes/mini-visits.md +243 -0
  195. package/nvim/plugins/mini.nvim/scripts/dual_log.sh +13 -0
  196. package/nvim/plugins/mini.nvim/scripts/dual_push.sh +15 -0
  197. package/nvim/plugins/mini.nvim/scripts/dual_release.sh +102 -0
  198. package/nvim/plugins/mini.nvim/scripts/dual_sync.sh +115 -0
  199. package/nvim/plugins/mini.nvim/scripts/init-deps-example.lua +59 -0
  200. package/nvim/plugins/mini.nvim/scripts/lint-filename-length.sh +16 -0
  201. package/nvim/plugins/mini.nvim/scripts/lintcommit-ci.sh +18 -0
  202. package/nvim/plugins/mini.nvim/scripts/lintcommit.lua +393 -0
  203. package/nvim/plugins/mini.nvim/scripts/minidoc.lua +57 -0
  204. package/nvim/plugins/mini.nvim/scripts/minimal_init.lua +28 -0
  205. package/nvim/plugins/mini.nvim/scripts/minitest.lua +4 -0
  206. package/package.json +16 -0
  207. package/pyproject.toml +43 -0
  208. package/yazi/init.lua +12 -0
  209. package/yazi/keymap.toml +114 -0
  210. package/yazi/lesskey +5 -0
  211. package/yazi/plugins/bookmarks.yazi/main.lua +86 -0
  212. package/yazi/plugins/cd-git-root.yazi/main.lua +31 -0
  213. package/yazi/plugins/smart-enter.yazi/main.lua +11 -0
  214. package/yazi/theme.toml +26 -0
  215. package/yazi/yazi.toml +15 -0
  216. package/zsh/.zshrc +145 -0
  217. package/zsh/starship.toml +20 -0
  218. package/zsh/zsh-patina.toml +2 -0
@@ -0,0 +1,2516 @@
1
+ --- *mini.test* Test Neovim plugins
2
+ ---
3
+ --- MIT License Copyright (c) 2022 Evgeni Chasnovski
4
+
5
+ --- Features:
6
+ --- - Test action is defined as a named callable entry of a table.
7
+ ---
8
+ --- - Helper for creating child Neovim process which is designed to be used in
9
+ --- tests (including taking and verifying screenshots). See
10
+ --- |MiniTest.new_child_neovim()| and |MiniTest.expect.reference_screenshot()|.
11
+ ---
12
+ --- - Hierarchical organization of tests with custom hooks, parametrization,
13
+ --- and user data. See |MiniTest.new_set()|.
14
+ ---
15
+ --- - Emulation of [lunarmodules/busted](https://github.com/lunarmodules/busted)
16
+ --- interface (`describe`, `it`, etc.).
17
+ ---
18
+ --- - Predefined small yet usable set of expectations (`assert`-like functions).
19
+ --- See |MiniTest.expect|.
20
+ ---
21
+ --- - Customizable definition of what files should be tested.
22
+ ---
23
+ --- - Test case filtering. There are predefined wrappers for testing a file
24
+ --- (|MiniTest.run_file()|) and case at a location like current cursor position
25
+ --- (|MiniTest.run_at_location()|).
26
+ ---
27
+ --- - Customizable reporter of output results. There are two predefined ones:
28
+ --- - |MiniTest.gen_reporter.buffer()| for interactive usage.
29
+ --- - |MiniTest.gen_reporter.stdout()| for headless Neovim.
30
+ ---
31
+ --- - Customizable project specific testing script.
32
+ ---
33
+ --- - Works on Unix (Linux, MacOS, etc.) and Windows.
34
+ ---
35
+ --- What it doesn't support:
36
+ --- - Parallel execution. Due to idea of limiting implementation complexity.
37
+ ---
38
+ --- - Mocks, stubs, etc. Use child Neovim process and manually override what is
39
+ --- needed. Reset child process it afterwards.
40
+ ---
41
+ --- - "Overly specific" expectations. Tests for (no) equality and (absence of)
42
+ --- errors usually cover most of the needs. Adding new expectations is a
43
+ --- subject to weighing its usefulness against additional implementation
44
+ --- complexity. Use |MiniTest.new_expectation()| to create custom ones.
45
+ ---
46
+ --- For more information see:
47
+ --- - `TESTING.md` file for a hands-on introduction based on examples.
48
+ ---
49
+ --- - Code of this plugin's tests. Consider it to be an example of intended
50
+ --- way to use |mini.test| for test organization and creation.
51
+ ---
52
+ --- # Workflow ~
53
+ ---
54
+ --- - Organize tests in separate files. Each test file should return a test set
55
+ --- (explicitly or implicitly by using "busted" style functions).
56
+ ---
57
+ --- - Write test actions as callable entries of test set. Use child process
58
+ --- inside test actions (see |MiniTest.new_child_neovim()|) and builtin
59
+ --- expectations (see |MiniTest.expect|).
60
+ ---
61
+ --- - Run tests. This does two steps:
62
+ --- - Collect. This creates single hierarchical test set, flattens into
63
+ --- array of test cases (see |MiniTest-test-case|) while expanding with
64
+ --- parametrization, and possibly filters them.
65
+ --- - Execute. This safely calls hooks and main test actions in specified
66
+ --- order while allowing reporting progress in asynchronous fashion.
67
+ --- Detected errors means test case fail; otherwise - pass.
68
+ ---
69
+ --- # Setup ~
70
+ ---
71
+ --- This module needs a setup with `require('mini.test').setup({})` (replace
72
+ --- `{}` with your `config` table). It will create global Lua table `MiniTest`
73
+ --- which you can use for scripting or manually (with `:lua MiniTest.*`).
74
+ ---
75
+ --- See |MiniTest.config| for available config settings.
76
+ ---
77
+ --- You can override runtime config settings locally to buffer inside
78
+ --- `vim.b.minitest_config` which should have same structure as `MiniTest.config`.
79
+ --- See |mini.nvim-buffer-local-config| for more details.
80
+ ---
81
+ --- To stop module from showing non-error feedback, set `config.silent = true`.
82
+ ---
83
+ --- # Comparisons ~
84
+ ---
85
+ --- - Testing infrastructure from
86
+ --- [nvim-lua/plenary.nvim](https://github.com/nvim-lua/plenary.nvim):
87
+ --- - Executes each file in separate headless Neovim process with customizable
88
+ --- `init.vim` file. While |mini.test| executes everything in current
89
+ --- Neovim process encouraging writing tests with help of manually
90
+ --- managed child Neovim process (see |MiniTest.new_child_neovim()|).
91
+ --- - Tests are expected to be written with embedded simplified versions of
92
+ --- `lunarmodules/busted` and `lunarmodules/luassert`. While |mini.test|
93
+ --- uses concepts of test set (see |MiniTest.new_set()|) and test case
94
+ --- (see |MiniTest-test-case|). It also can emulate bigger part of
95
+ --- "busted" framework.
96
+ --- - Has single way of reporting progress (shows result after every case
97
+ --- without summary). While |mini.test| can have customized reporters
98
+ --- with defaults for interactive and headless usage (provide more
99
+ --- compact and user-friendly summaries).
100
+ --- - Allows parallel execution, while |mini.test| does not.
101
+ --- - Allows making mocks, stubs, and spies, while |mini.test| does not in
102
+ --- favor of manually overwriting functionality in child Neovim process.
103
+ ---
104
+ --- Although |mini.test| supports emulation of "busted style" testing, it will
105
+ --- be more stable to use its designed approach of defining tests (with
106
+ --- `MiniTest.new_set()` and explicit table fields). Couple of reasons:
107
+ --- - "Busted" syntax doesn't support full capabilities offered by |mini.test|.
108
+ --- Mainly it is about parametrization and supplying user data to test sets.
109
+ --- - It is an emulation, not full support. So some subtle things might not
110
+ --- work the way you expect.
111
+ ---
112
+ --- Some hints for converting from `plenary.nvim` tests to |mini.test|:
113
+ --- - Rename files from `***_spec.lua` to `test_***.lua` and put them in
114
+ --- "tests" directory.
115
+ --- - Replace `assert` calls with |mini.test| expectations. See |MiniTest.expect|.
116
+ --- - Create main test set `T = MiniTest.new_set()` and eventually return it.
117
+ --- - Make new sets (|MiniTest.new_set()|) from `describe` blocks. Convert
118
+ --- `before_each()` and `after_each` to `pre_case` and `post_case` hooks.
119
+ --- - Make test cases from `it` blocks.
120
+ ---
121
+ --- # Highlight groups ~
122
+ --- *MiniTest-hl-groups*
123
+ ---
124
+ --- - `MiniTestEmphasis` - emphasis highlighting. By default it is a bold text.
125
+ --- - `MiniTestFail` - highlighting of failed cases. By default it is a bold
126
+ --- text with `vim.g.terminal_color_1` color (red).
127
+ --- - `MiniTestPass` - highlighting of passed cases. By default it is a bold
128
+ --- text with `vim.g.terminal_color_2` color (green).
129
+ ---
130
+ --- To change any highlight group, set it directly with |nvim_set_hl()|.
131
+ ---
132
+ --- # Disabling ~
133
+ ---
134
+ --- To disable, set `vim.g.minitest_disable` (globally) or `vim.b.minitest_disable`
135
+ --- (for a buffer) to `true`. Considering high number of different scenarios
136
+ --- and customization intentions, writing exact rules for disabling module's
137
+ --- functionality is left to user. See |mini.nvim-disabling-recipes| for common
138
+ --- recipes.
139
+ ---@tag MiniTest
140
+
141
+ ---@alias __test_expect_fail_reason - <fail_reason> `(string|function)` - reason for failing expectation.
142
+ --- a function is called with expectation input and should return a string.
143
+ --- Default: `nil` for default reason like "Failed expectation for ...".
144
+
145
+ -- Module definition ==========================================================
146
+ local MiniTest = {}
147
+ local H = {}
148
+
149
+ --- Module setup
150
+ ---
151
+ ---@param config table|nil Module config table. See |MiniTest.config|.
152
+ ---
153
+ ---@usage >lua
154
+ --- require('mini.test').setup() -- use default config
155
+ --- -- OR
156
+ --- require('mini.test').setup({}) -- replace {} with your config table
157
+ --- <
158
+ MiniTest.setup = function(config)
159
+ -- TODO: Remove after Neovim=0.9 support is dropped
160
+ if vim.fn.has('nvim-0.10') == 0 then
161
+ vim.notify(
162
+ '(mini.test) Neovim<0.10 is soft deprecated (module works but is not supported).'
163
+ .. " It will be deprecated after the next 'mini.nvim' release (module might not work)."
164
+ .. ' Please update your Neovim version.'
165
+ )
166
+ end
167
+
168
+ -- Export module
169
+ _G.MiniTest = MiniTest
170
+
171
+ -- Setup config
172
+ config = H.setup_config(config)
173
+
174
+ -- Apply config
175
+ H.apply_config(config)
176
+
177
+ -- Define behavior
178
+ H.create_autocommands()
179
+
180
+ -- Create default highlighting
181
+ H.create_default_hl()
182
+ end
183
+
184
+ --stylua: ignore start
185
+ --- Defaults ~
186
+ ---@eval return MiniDoc.afterlines_to_code(MiniDoc.current.eval_section)
187
+ MiniTest.config = {
188
+ -- Options for collection of test cases. See `:h MiniTest.collect()`.
189
+ collect = {
190
+ -- Temporarily emulate functions from 'busted' testing framework
191
+ -- (`describe`, `it`, `before_each`, `after_each`, and more)
192
+ emulate_busted = true,
193
+
194
+ -- Function returning array of file paths to be collected.
195
+ -- Default: all Lua files in 'tests' directory starting with 'test_'.
196
+ find_files = function()
197
+ return vim.fn.globpath('tests', '**/test_*.lua', true, true)
198
+ end,
199
+
200
+ -- Predicate function indicating if test case should be executed
201
+ filter_cases = function(case) return true end,
202
+ },
203
+
204
+ -- Options for execution of test cases. See `:h MiniTest.execute()`.
205
+ execute = {
206
+ -- Table with callable fields `start()`, `update()`, and `finish()`
207
+ reporter = nil,
208
+
209
+ -- Whether to stop execution after first error
210
+ stop_on_error = false,
211
+ },
212
+
213
+ -- Path (relative to current directory) to script which handles project
214
+ -- specific test running
215
+ script_path = 'scripts/minitest.lua',
216
+
217
+ -- Whether to disable showing non-error feedback
218
+ silent = false,
219
+ }
220
+ --minidoc_afterlines_end
221
+ --stylua: ignore end
222
+
223
+ -- Module data ================================================================
224
+ --- Table with information about current state of test execution
225
+ ---
226
+ --- Use it to examine result of |MiniTest.execute()|. It is reset at the
227
+ --- beginning of every call.
228
+ ---
229
+ --- At least these keys are supported:
230
+ --- - <all_cases> - array with all cases being currently executed. Basically,
231
+ --- an input of `MiniTest.execute()`.
232
+ --- - <case> - currently executed test case. See |MiniTest-test-case|. Use it
233
+ --- to customize execution output (like adding custom notes, etc).
234
+ MiniTest.current = { all_cases = nil, case = nil }
235
+
236
+ -- Module functionality =======================================================
237
+ --- Create test set
238
+ ---
239
+ --- Test set is one of the two fundamental data structures. It is a table that
240
+ --- defines hierarchical test organization as opposed to sequential
241
+ --- organization with |MiniTest-test-case|.
242
+ ---
243
+ --- All its elements are one of three categories:
244
+ --- - A callable (object that can be called; function or table with `__call`
245
+ --- metatble entry) is considered to define a test action. It will be called
246
+ --- with "current arguments" (result of all nested `parametrize` values, read
247
+ --- further). If it throws error, test has failed.
248
+ --- - A test set (output of this function) defines nested structure. Its
249
+ --- options during collection (see |MiniTest.collect()|) will be extended
250
+ --- with options of this (parent) test set.
251
+ --- - Any other elements are considered helpers and don't directly participate
252
+ --- in test structure.
253
+ ---
254
+ --- Set options allow customization of test collection and execution (more
255
+ --- details in `opts` description):
256
+ --- - `hooks` - table with elements that will be called without arguments at
257
+ --- predefined stages of test execution.
258
+ --- - `parametrize` - array defining different arguments with which main test
259
+ --- actions will be called. Any non-trivial parametrization will lead to
260
+ --- every element (even nested) be "multiplied" and processed with every
261
+ --- element of `parametrize`. This allows handling many different combination
262
+ --- of tests with little effort.
263
+ --- - `data` - table with user data that will be forwarded to cases. Primary
264
+ --- objective is to be used for customized case filtering.
265
+ ---
266
+ --- Notes:
267
+ --- - Preferred way of adding elements is by using syntax `T[name] = element`.
268
+ --- This way order of added elements will be preserved. Any other way won't
269
+ --- guarantee any order.
270
+ --- - Supplied options `opts` are stored in `opts` field of metatable
271
+ --- (`getmetatable(set).opts`).
272
+ ---
273
+ ---@param opts table|nil Allowed options:
274
+ --- - <hooks> - table with fields:
275
+ --- - <pre_once> - executed before first filtered node.
276
+ --- - <pre_case> - executed before each case (even nested).
277
+ --- - <post_case> - executed after each case (even nested).
278
+ --- - <post_once> - executed after last filtered node.
279
+ --- - <parametrize> - array where each element is itself an array of
280
+ --- parameters to be appended to "current parameters" of callable fields.
281
+ --- Note: don't use plain `{}` as it is equivalent to "parametrization into
282
+ --- zero cases", so no cases will be collected from this set. Calling test
283
+ --- actions with no parameters is equivalent to `{{}}` or not supplying
284
+ --- `parametrize` option at all.
285
+ --- - <data> - user data to be forwarded to cases. Can be used for a more
286
+ --- granular filtering.
287
+ --- - <n_retry> - number of times to retry each case until success.
288
+ --- Default: 1.
289
+ ---@param tbl table|nil Initial test items (possibly nested). Will be executed
290
+ --- without any guarantees on order.
291
+ ---
292
+ ---@return table A single test set.
293
+ ---
294
+ ---@usage >lua
295
+ --- -- Use with defaults
296
+ --- T = MiniTest.new_set()
297
+ --- T['works'] = function() MiniTest.expect.equality(1, 1) end
298
+ ---
299
+ --- -- Use with custom options. This will result into two actual cases: first
300
+ --- -- will pass, second - fail after two attempts.
301
+ --- T['nested'] = MiniTest.new_set({
302
+ --- hooks = { pre_case = function() _G.x = 1 end },
303
+ --- parametrize = { { 1 }, { 2 } },
304
+ --- n_retry = 2,
305
+ --- })
306
+ ---
307
+ --- T['nested']['works'] = function(x) MiniTest.expect.equality(_G.x, x) end
308
+ --- <
309
+ MiniTest.new_set = function(opts, tbl)
310
+ opts = opts or {}
311
+ tbl = tbl or {}
312
+
313
+ -- Keep track of new elements order. This allows to iterate through elements
314
+ -- in order they were added.
315
+ local metatbl = { class = 'testset', key_order = vim.tbl_keys(tbl), opts = opts }
316
+ metatbl.__newindex = function(t, key, value)
317
+ table.insert(metatbl.key_order, key)
318
+ rawset(t, key, value)
319
+ end
320
+
321
+ return setmetatable(tbl, metatbl)
322
+ end
323
+
324
+ --- Test case
325
+ ---
326
+ --- An item of sequential test organization, as opposed to hierarchical with
327
+ --- test set (see |MiniTest.new_set()|). It is created as result of test
328
+ --- collection with |MiniTest.collect()| to represent all necessary information
329
+ --- of test execution.
330
+ ---
331
+ --- Execution of test case goes by the following rules:
332
+ --- - Call functions in order:
333
+ --- - All elements of `hooks.pre` from first to last without arguments.
334
+ --- - Field `test` with arguments unpacked from `args`. If execution fails,
335
+ --- retry it (along with hooks that come from `pre_case` and `post_case`)
336
+ --- at most `n_retry` times until first success (if any).
337
+ --- - All elements of `hooks.post` from first to last without arguments.
338
+ --- - Error in any call gets appended to `exec.fails`, meaning error in any
339
+ --- hook will lead to test fail.
340
+ --- - State (`exec.state`) is changed before every call and after last call.
341
+ ---
342
+ ---@class Test-case
343
+ ---
344
+ ---@field args table Array of arguments with which `test` will be called.
345
+ ---@field data table User data: all fields of `opts.data` from nested test sets.
346
+ ---@field desc table Description: array of fields from nested test sets.
347
+ ---@field exec table|nil Information about test case execution. Value of `nil` means
348
+ --- that this particular case was not (yet) executed. Has following fields:
349
+ --- - <fails> - array of strings with failing information.
350
+ --- - <notes> - array of strings with non-failing information.
351
+ --- - <state> - state of test execution. One of:
352
+ --- - `'Executing <name of what is being executed>'` (during execution).
353
+ --- - `'Pass'` (no fails, no notes).
354
+ --- - `'Pass with notes'` (no fails, some notes).
355
+ --- - `'Fail'` (some fails, no notes).
356
+ --- - `'Fail with notes'` (some fails, some notes).
357
+ ---@field hooks table Hooks to be executed as part of test case. Has fields:
358
+ --- - <pre> and <post> - arrays of functions to be consecutively executed
359
+ --- before and after every execution of `test`.
360
+ --- - <pre_source> and <post_source> - arrays of strings with sources of
361
+ --- corresponding elements in <pre> and <post> arrays. Source is one of
362
+ --- `"once"` (for `pre_once` and `post_once` hooks) and
363
+ --- `"case"` (for `pre_case` and `post_case` hooks).
364
+ ---@field test function|table Main callable object representing test action.
365
+ ---@tag MiniTest-test-case
366
+
367
+ --- Skip the rest of current case
368
+ ---
369
+ --- Notes:
370
+ --- - When called inside test case, stops execution while adding message to notes.
371
+ --- - When called inside `pre_case` hook, registers skip at the start of its
372
+ --- test case. Calling in other hooks has no effect.
373
+ --- - Currently implemented as a specially handled type of error.
374
+ ---
375
+ ---@param msg string|nil Message to be added to current case notes.
376
+ MiniTest.skip = function(msg)
377
+ H.cache.skip_message = msg or 'Skip test'
378
+ error(H.cache.skip_message, 0)
379
+ end
380
+
381
+ --- Add note to currently executed test case
382
+ ---
383
+ --- Appends `msg` to `exec.notes` field of `case` in |MiniTest.current|.
384
+ ---
385
+ ---@param msg string Note to add.
386
+ MiniTest.add_note = function(msg)
387
+ local case = MiniTest.current.case
388
+ case.exec = case.exec or {}
389
+ case.exec.notes = case.exec.notes or {}
390
+ table.insert(case.exec.notes, msg)
391
+ end
392
+
393
+ --- Register callable execution after current callable
394
+ ---
395
+ --- Can be used several times inside hooks and main test callable of test case.
396
+ ---
397
+ ---@param f function|table Callable to be executed after current callable is
398
+ --- finished executing (regardless of whether it ended with error or not).
399
+ MiniTest.finally = function(f) table.insert(H.cache.finally, f) end
400
+
401
+ --- Run tests
402
+ ---
403
+ --- - Try executing project specific script at path `opts.script_path`. If
404
+ --- successful (no errors), then stop.
405
+ --- - Collect cases with |MiniTest.collect()| and `opts.collect`.
406
+ --- - Execute collected cases with |MiniTest.execute()| and `opts.execute`.
407
+ ---
408
+ ---@param opts table|nil Options with structure similar to |MiniTest.config|.
409
+ --- Absent values are inferred from there.
410
+ MiniTest.run = function(opts)
411
+ if H.is_disabled() then return end
412
+
413
+ -- Try sourcing project specific script first
414
+ local success = H.execute_project_script(opts)
415
+ if success then return end
416
+
417
+ -- Collect and execute
418
+ opts = H.get_config(opts)
419
+ local cases = MiniTest.collect(opts.collect)
420
+ MiniTest.execute(cases, opts.execute)
421
+ end
422
+
423
+ --- Run specific test file
424
+ ---
425
+ --- Basically a |MiniTest.run()| wrapper with custom `collect.find_files` option.
426
+ ---
427
+ ---@param file string|nil Path to test file. By default a path of current buffer.
428
+ ---@param opts table|nil Options for |MiniTest.run()|.
429
+ MiniTest.run_file = function(file, opts)
430
+ file = vim.fn.fnamemodify(file or vim.api.nvim_buf_get_name(0), ':p:.')
431
+
432
+ local stronger_opts = { collect = { find_files = function() return { file } end } }
433
+ opts = vim.tbl_deep_extend('force', opts or {}, stronger_opts)
434
+
435
+ MiniTest.run(opts)
436
+ end
437
+
438
+ --- Run case(s) covering location
439
+ ---
440
+ --- Try filtering case(s) covering location, meaning that definition of its
441
+ --- main `test` action (as taken from builtin `debug.getinfo`) is located in
442
+ --- specified file and covers specified line. Note that it can result in
443
+ --- multiple cases if they come from parametrized test set (see `parametrize`
444
+ --- option in |MiniTest.new_set()|).
445
+ ---
446
+ --- Basically a |MiniTest.run()| wrapper with custom `collect.find_files` option.
447
+ ---
448
+ ---@param location table|nil Table with fields <file> (path to file) and <line>
449
+ --- (line number in that file). Default is taken from current cursor position.
450
+ MiniTest.run_at_location = function(location, opts)
451
+ if location == nil then
452
+ local cur_file = vim.fn.fnamemodify(vim.api.nvim_buf_get_name(0), ':.')
453
+ local cur_pos = vim.api.nvim_win_get_cursor(0)
454
+ location = { file = cur_file, line = cur_pos[1] }
455
+ end
456
+
457
+ local stronger_opts = {
458
+ collect = {
459
+ find_files = function() return { location.file } end,
460
+ filter_cases = function(case)
461
+ local info = debug.getinfo(case.test)
462
+
463
+ return info.short_src == location.file
464
+ and info.linedefined <= location.line
465
+ and location.line <= info.lastlinedefined
466
+ end,
467
+ },
468
+ }
469
+ opts = vim.tbl_deep_extend('force', opts or {}, stronger_opts)
470
+
471
+ MiniTest.run(opts)
472
+ end
473
+
474
+ --- Collect test cases
475
+ ---
476
+ --- Overview of collection process:
477
+ --- - If `opts.emulate_busted` is `true`, temporary make special global
478
+ --- functions (removed at the end of collection). They can be used inside
479
+ --- test files to create hierarchical structure of test cases.
480
+ --- - Source each file from array output of `opts.find_files`. It should output
481
+ --- a test set (see |MiniTest.new_set()|) or `nil` (if "busted" style is used;
482
+ --- test set is created implicitly).
483
+ --- - Combine all test sets into single set with fields equal to its file path.
484
+ --- - Convert from hierarchical test configuration to sequential: from single
485
+ --- test set to array of test cases (see |MiniTest-test-case|). Conversion is
486
+ --- done in the form of "for every table element do: for every `parametrize`
487
+ --- element do: ...". Details:
488
+ --- - If element is a callable, construct test case with it being main
489
+ --- `test` action. Description is appended with key of element in current
490
+ --- test set table. Hooks, arguments, and data are taken from "current
491
+ --- nested" ones. Add case to output array.
492
+ --- - If element is a test set, process it in similar, recursive fashion.
493
+ --- The "current nested" information is expanded:
494
+ --- - `args` is extended with "current element" from `parametrize`.
495
+ --- - `desc` is appended with element key.
496
+ --- - `hooks` are appended to their appropriate places. `*_case` hooks
497
+ --- will be inserted closer to all child cases than hooks from parent
498
+ --- test sets: `pre_case` at end, `post_case` at start.
499
+ --- - `data` is extended via |vim.tbl_deep_extend()|.
500
+ --- - Any other element is not processed.
501
+ --- - Filter array with `opts.filter_cases`. Note that input case doesn't contain
502
+ --- all hooks, as `*_once` hooks will be added after filtration.
503
+ --- - Add `*_once` hooks to appropriate cases.
504
+ ---
505
+ ---@param opts table|nil Options controlling case collection. Possible fields:
506
+ --- - <emulate_busted> - whether to emulate `lunarmodules/busted` interface.
507
+ --- It emulates these global functions: `describe`, `it`, `setup`, `teardown`,
508
+ --- `before_each`, `after_each`. Use |MiniTest.skip()| instead of `pending()`
509
+ --- and |MiniTest.finally()| instead of `finally`.
510
+ --- - <find_files> - function which when called without arguments returns
511
+ --- array with file paths. Each file should be a Lua file returning single
512
+ --- test set or `nil`.
513
+ --- - <filter_cases> - function which when called with single test case
514
+ --- (see |MiniTest-test-case|) returns `false` if this case should be filtered
515
+ --- out; `true` otherwise.
516
+ ---
517
+ ---@return table Array of test cases ready to be used by |MiniTest.execute()|.
518
+ MiniTest.collect = function(opts)
519
+ opts = vim.tbl_deep_extend('force', H.get_config().collect, opts or {})
520
+
521
+ -- Make single test set
522
+ local set = MiniTest.new_set()
523
+
524
+ for _, file in ipairs(opts.find_files()) do
525
+ -- Possibly emulate 'busted' with current file. This allows to wrap all
526
+ -- implicit cases from that file into single set with file's name.
527
+ if opts.emulate_busted then
528
+ set[file] = MiniTest.new_set()
529
+ H.busted_emulate(set[file])
530
+ end
531
+
532
+ -- Execute file
533
+ local ok, t = pcall(dofile, file)
534
+
535
+ -- Catch errors
536
+ if not ok then
537
+ local msg = string.format('Sourcing %s resulted into following error: %s', vim.inspect(file), t)
538
+ H.error(msg)
539
+ end
540
+ local is_output_correct = (opts.emulate_busted and vim.tbl_count(set[file]) > 0) or H.is_instance(t, 'testset')
541
+ if not is_output_correct then
542
+ local msg = string.format(
543
+ [[%s does not define a test set. Did you return `MiniTest.new_set()` or created 'busted' tests?]],
544
+ vim.inspect(file)
545
+ )
546
+ H.error(msg)
547
+ end
548
+
549
+ -- If output is test set, always use it (even if 'busted' tests were added)
550
+ if H.is_instance(t, 'testset') then set[file] = t end
551
+ end
552
+
553
+ H.busted_deemulate()
554
+
555
+ -- Convert to test cases. This also creates separate aligned array of hooks
556
+ -- which should be executed once regarding test case. This is needed to
557
+ -- correctly inject those hooks after filtering is done.
558
+ local raw_cases, raw_hooks_once = H.set_to_testcases(set)
559
+
560
+ -- Filter cases (at this stage don't have injected `hooks_once`)
561
+ local cases, hooks_once = {}, {}
562
+ for i, c in ipairs(raw_cases) do
563
+ if opts.filter_cases(c) then
564
+ table.insert(cases, c)
565
+ table.insert(hooks_once, raw_hooks_once[i])
566
+ end
567
+ end
568
+
569
+ -- Inject `hooks_once` into appropriate cases
570
+ H.inject_hooks_once(cases, hooks_once)
571
+
572
+ return cases
573
+ end
574
+
575
+ --- Execute array of test cases
576
+ ---
577
+ --- Overview of execution process:
578
+ --- - Reset `all_cases` in |MiniTest.current| with `cases` input.
579
+ --- - Call `reporter.start(cases)` (if present).
580
+ --- - Execute each case in natural array order (aligned with their integer
581
+ --- keys). Set `MiniTest.current.case` to currently executed case. Detailed
582
+ --- test case execution is described in |MiniTest-test-case|. After any state
583
+ --- change (including case retry attempts), call `reporter.update(case_num)`
584
+ --- (if present), where `case_num` is an integer key of current test case.
585
+ --- - Call `reporter.finish()` (if present).
586
+ ---
587
+ --- Notes:
588
+ --- - Execution is done in asynchronous fashion with scheduling. This allows
589
+ --- making meaningful progress report during execution.
590
+ --- - This function doesn't return anything. Instead, it updates `cases` in
591
+ --- place with proper `exec` field. Use `all_cases` at |MiniTest.current| to
592
+ --- look at execution result.
593
+ ---
594
+ ---@param cases table Array of test cases (see |MiniTest-test-case|).
595
+ ---@param opts table|nil Options controlling case collection. Possible fields:
596
+ --- - <reporter> - table with possible callable fields `start`, `update`,
597
+ --- `finish`. Default: |MiniTest.gen_reporter.buffer()| in interactive
598
+ --- usage and |MiniTest.gen_reporter.stdout()| in headless usage.
599
+ --- - <stop_on_error> - whether to stop execution (see |MiniTest.stop()|)
600
+ --- after first error. Default: `false`.
601
+ MiniTest.execute = function(cases, opts)
602
+ H.check_type('cases', cases, 'table')
603
+
604
+ MiniTest.current.all_cases = cases
605
+
606
+ -- Verify correct arguments
607
+ if #cases == 0 then
608
+ H.message('No cases to execute.')
609
+ return
610
+ end
611
+
612
+ opts = vim.tbl_deep_extend('force', H.get_config().execute, opts or {})
613
+ local reporter = opts.reporter or (H.is_headless and MiniTest.gen_reporter.stdout() or MiniTest.gen_reporter.buffer())
614
+ if type(reporter) ~= 'table' then
615
+ H.message('`opts.reporter` should be table or `nil`.')
616
+ return
617
+ end
618
+ opts.reporter = reporter
619
+
620
+ -- Plan execution in order
621
+ H.cache = { is_executing = true }
622
+
623
+ local queue = {}
624
+ table.insert(queue, function() H.exec_callable(reporter.start, cases) end)
625
+ for case_num, cur_case in ipairs(cases) do
626
+ table.insert(queue, H.make_case(cur_case, case_num, opts))
627
+ end
628
+ table.insert(queue, function() H.exec_callable(reporter.finish) end)
629
+ -- - Use separate call to ensure that `reporter.finish` error won't interfere
630
+ table.insert(queue, function() H.cache.is_executing = false end)
631
+
632
+ -- Execute queue ensuring order
633
+ -- NOTE: Directly `vim.schedule` each step without an explicit queue handling
634
+ -- is possible, but it might interfere with async-adjacent yet synchronous
635
+ -- functions (as `vim.wait()`) callsed inside cases outside of child process.
636
+ local exec_queue_step, n_queue = function(_) end, #queue
637
+ exec_queue_step = function(n)
638
+ queue[n]()
639
+ if n < n_queue then vim.schedule(function() exec_queue_step(n + 1) end) end
640
+ end
641
+ vim.schedule(function() exec_queue_step(1) end)
642
+ end
643
+
644
+ --- Stop test execution
645
+ ---
646
+ ---@param opts table|nil Options with fields:
647
+ --- - <close_all_child_neovim> - whether to close all child neovim processes
648
+ --- created with |MiniTest.new_child_neovim()|. Default: `true`.
649
+ MiniTest.stop = function(opts)
650
+ opts = vim.tbl_deep_extend('force', { close_all_child_neovim = true }, opts or {})
651
+
652
+ -- Register intention to stop execution
653
+ H.cache.should_stop_execution = true
654
+
655
+ -- Possibly stop all child Neovim processes
656
+ if not opts.close_all_child_neovim then return end
657
+
658
+ for _, child in ipairs(H.child_neovim_registry) do
659
+ pcall(child.stop)
660
+ end
661
+ H.child_neovim_registry = {}
662
+ end
663
+
664
+ --- Check if tests are being executed
665
+ ---
666
+ ---@return boolean
667
+ MiniTest.is_executing = function() return H.cache.is_executing == true end
668
+
669
+ -- Expectations ---------------------------------------------------------------
670
+ --- Table with expectation functions
671
+ ---
672
+ --- Each function has the following behavior:
673
+ --- - Silently returns `true` if expectation is fulfilled.
674
+ --- - Throws an informative error with information helpful for debugging.
675
+ --- Allows customizable fail reason to provide more context.
676
+ ---
677
+ --- Mostly designed to be used within |mini.test| framework.
678
+ ---
679
+ ---@usage >lua
680
+ --- local x = 1 + 1
681
+ --- MiniTest.expect.equality(x, 2) -- passes
682
+ --- MiniTest.expect.equality(x, 1, { fail_reason = 'Not equal' }) -- fails
683
+ --- <
684
+ MiniTest.expect = {}
685
+
686
+ --- Expect equality of two objects
687
+ ---
688
+ --- Equality is tested via |vim.deep_equal()|. It also tries to compute more
689
+ --- detailed cause for equality (for easier spotting the difference):
690
+ --- - If they have different types.
691
+ --- - For strings if they have different length or if some character is different.
692
+ --- - For tables it shows a "key branch" at which values are different along with
693
+ --- the actual values. A single difference is shown, there might be more.
694
+ --- For not nested tables key branch is just a key. If the difference is inside
695
+ --- nested tables, the key branch shows a "path through nested tables" to
696
+ --- a different value. Examples: >lua
697
+ ---
698
+ --- local eq = MiniTest.expect.equality
699
+ --- eq({ 1, 2 }, { 1, 3 }) -- Key branch is `2`
700
+ --- eq({ 1, { 2 } }, { 1, 'c' }) -- Key branch is `2` ('c' is not a table)
701
+ --- eq({ 1, { 2 } }, { 1, { 3 } }) -- Key branch is `2->1`
702
+ ---
703
+ --- -- Key branch is either `1->1->"a"` or `1->1->"b"`
704
+ --- eq({ { { a = 1 } } }, { { { b = 2 } } })
705
+ --- <
706
+ ---@param left any First object.
707
+ ---@param right any Second object.
708
+ ---@param opts table|nil Options. Possible fields:
709
+ --- __test_expect_fail_reason
710
+ MiniTest.expect.equality = function(left, right, opts)
711
+ if vim.deep_equal(left, right) then return true end
712
+
713
+ opts = opts or {}
714
+ local fail_reason = H.normalize_reason(opts.fail_reason, 'Failed expectation for equality', left, right)
715
+ local cause = H.compute_no_equality_cause(left, right)
716
+ local context = string.format('Cause: %s\nLeft: %s\nRight: %s', cause, vim.inspect(left), vim.inspect(right))
717
+ H.error_with_emphasis(fail_reason, context)
718
+ end
719
+
720
+ --- Expect no equality of two objects
721
+ ---
722
+ --- Equality is tested via |vim.deep_equal()|.
723
+ ---
724
+ ---@param left any First object.
725
+ ---@param right any Second object.
726
+ ---@param opts table|nil Options. Possible fields:
727
+ --- __test_expect_fail_reason
728
+ MiniTest.expect.no_equality = function(left, right, opts)
729
+ if not vim.deep_equal(left, right) then return true end
730
+
731
+ opts = opts or {}
732
+ local fail_reason = H.normalize_reason(opts.fail_reason, 'Failed expectation for *no* equality', left, right)
733
+ local context = string.format('Object: %s', vim.inspect(left))
734
+ H.error_with_emphasis(fail_reason, context)
735
+ end
736
+
737
+ --- Expect function call to raise error
738
+ ---
739
+ ---@param f function Function to be tested for raising error.
740
+ ---@param pattern string|nil Pattern which error message should match.
741
+ --- Use `nil` or empty string to not test for pattern matching.
742
+ ---@param opts table|nil Options. Possible fields:
743
+ --- __test_expect_fail_reason
744
+ MiniTest.expect.error = function(f, pattern, opts, ...)
745
+ H.check_type('pattern', pattern, 'string', true)
746
+
747
+ -- Provide backward compatibility for `(f, pattern, ...)` signature.
748
+ -- TODO: Remove after releasing 'mini.nvim' 0.18.0
749
+ local args = { ... }
750
+ local is_valid_opts = type(opts) == 'table'
751
+ and (opts.fail_reason == nil or type(opts.fail_reason) == 'string' or vim.is_callable(opts.fail_reason))
752
+ if select('#', ...) > 0 or not (opts == nil or is_valid_opts) then
753
+ table.insert(args, 1, opts)
754
+ opts = {}
755
+ vim.notify(
756
+ '(mini.test) `expect.error` now does not accept extra arguments for tested function.'
757
+ .. " It will mostly work until the next 'mini.nvim' release, but not after that."
758
+ .. ' Use them explicitly inside anonymous function: `expect.error(f, "", 1, 2)` ->'
759
+ .. ' `expect.error(function() f(1, 2) end, "")`.'
760
+ .. '\nSorry for the inconvenience.',
761
+ vim.log.levels.WARN
762
+ )
763
+ end
764
+ local ok, err = pcall(f, unpack(args))
765
+
766
+ err = tostring(err)
767
+ local has_matched_error = not ok and string.find(err, pattern or '') ~= nil
768
+ if has_matched_error then return true end
769
+
770
+ opts = opts or {}
771
+ local pattern_suffix = pattern == nil and '' or (' matching pattern ' .. vim.inspect(pattern))
772
+ local fail_reason = H.normalize_reason(opts.fail_reason, 'Failed expectation for error' .. pattern_suffix, f, pattern)
773
+ local context = ok and 'Observed no error' or ('Observed error: ' .. err)
774
+ H.error_with_emphasis(fail_reason, context)
775
+ end
776
+
777
+ --- Expect function call to not raise error
778
+ ---
779
+ ---@param f function Function to be tested for not raising error.
780
+ ---@param opts table|nil Options. Possible fields:
781
+ --- __test_expect_fail_reason
782
+ MiniTest.expect.no_error = function(f, opts, ...)
783
+ -- Provide backward compatibility for `(f, ...)` signature.
784
+ -- TODO: Remove after releasing 'mini.nvim' 0.18.0
785
+ local args = { ... }
786
+ local is_valid_opts = type(opts) == 'table' and (opts.fail_prefix == nil or type(opts.fail_prefix) == 'string')
787
+ if select('#', ...) > 0 or not (opts == nil or is_valid_opts) then
788
+ table.insert(args, 1, opts)
789
+ opts = {}
790
+ vim.notify(
791
+ '(mini.test) `expect.no_error` now does not accept extra arguments for tested function.'
792
+ .. " It will mostly work until the next 'mini.nvim' release, but not after that."
793
+ .. ' Use them explicitly inside anonymous function: `expect.no_error(f, 1, 2)` ->'
794
+ .. ' `expect.no_error(function() f(1, 2) end)`.'
795
+ .. '\nSorry for the inconvenience.',
796
+ vim.log.levels.WARN
797
+ )
798
+ end
799
+ local ok, err = pcall(f, unpack(args))
800
+ if ok then return true end
801
+
802
+ opts = opts or {}
803
+ local fail_reason = H.normalize_reason(opts.fail_reason, 'Failed expectation for *no* error', f)
804
+ H.error_with_emphasis(fail_reason, 'Observed error: ' .. tostring(err))
805
+ end
806
+
807
+ --- Expect equality to reference screenshot
808
+ ---
809
+ ---@param screenshot table|nil Array with screenshot information. Usually an output
810
+ --- of `child.get_screenshot()` (see |MiniTest-child-neovim-get_screenshot()|).
811
+ --- If `nil`, expectation passed.
812
+ ---@param path string|nil Path to reference screenshot. If `nil`, constructed
813
+ --- automatically in directory `opts.directory` from current case info and
814
+ --- total number of times it was called inside current case. If there is no
815
+ --- file at `path`, it is created with content of `screenshot`.
816
+ ---@param opts table|nil Options:
817
+ --- - <force> `(boolean)` - whether to forcefully create reference screenshot.
818
+ --- Temporary useful during test writing. Default: `false`.
819
+ --- - <ignore_text> `(boolean|table)` - whether to ignore all or some text lines.
820
+ --- If `true` - ignore all, if number array - ignore text of those lines,
821
+ --- if `false` - do not ignore any. Default: `false`.
822
+ --- - <ignore_attr> `(boolean|table)` - whether to ignore all or some attr lines.
823
+ --- If `true` - ignore all, if number array - ignore attr of those lines,
824
+ --- if `false` - do not ignore any. Default: `false`.
825
+ --- - <directory> `(string)` - directory where automatically constructed `path`
826
+ --- is located. Default: "tests/screenshots".
827
+ --- __test_expect_fail_reason
828
+ MiniTest.expect.reference_screenshot = function(screenshot, path, opts)
829
+ if screenshot == nil then return true end
830
+
831
+ local default_opts = { force = false, ignore_text = false, ignore_attr = false, directory = 'tests/screenshots' }
832
+ opts = vim.tbl_extend('force', default_opts, opts or {})
833
+
834
+ H.cache.n_screenshots = H.cache.n_screenshots + 1
835
+
836
+ if path == nil then
837
+ -- Sanitize path. Replace any control characters, whitespace, OS specific
838
+ -- forbidden characters with '-' (with some useful exception)
839
+ local linux_forbidden = [[/]]
840
+ local windows_forbidden = [[<>:"/\|?*]]
841
+ local pattern = string.format('[%%c%%s%s%s]', vim.pesc(linux_forbidden), vim.pesc(windows_forbidden))
842
+ local replacements = setmetatable({ ['"'] = "'" }, { __index = function() return '-' end })
843
+ local name = H.case_to_stringid(MiniTest.current.case):gsub(pattern, replacements)
844
+
845
+ -- Don't end with whitespace or dot (forbidden on Windows)
846
+ name = name:gsub('[%s%.]$', '-')
847
+ path = vim.fs.normalize(opts.directory) .. '/' .. name
848
+
849
+ -- Deal with multiple screenshots
850
+ if H.cache.n_screenshots > 1 then path = path .. string.format('-%03d', H.cache.n_screenshots) end
851
+ end
852
+
853
+ -- If there is no readable screenshot file, create it. Pass with note.
854
+ if opts.force or vim.fn.filereadable(path) == 0 then
855
+ local dir_path = vim.fn.fnamemodify(path, ':p:h')
856
+ vim.fn.mkdir(dir_path, 'p')
857
+ H.screenshot_write(screenshot, path)
858
+
859
+ MiniTest.add_note('Created reference screenshot at path ' .. vim.inspect(path))
860
+ return true
861
+ end
862
+
863
+ local reference = H.screenshot_read(path)
864
+
865
+ -- Compare
866
+ local same_text, cause_text = H.screenshot_compare_part('text', reference, screenshot, opts)
867
+ local same_attr, cause_attr = H.screenshot_compare_part('attr', reference, screenshot, opts)
868
+ if same_text and same_attr then return true end
869
+
870
+ local fail_reason_fallback = 'Failed expectation for screenshot equality to reference at ' .. vim.inspect(path)
871
+ local fail_reason = H.normalize_reason(opts.fail_reason, fail_reason_fallback, screenshot, path)
872
+ local cause = same_text and cause_attr or cause_text
873
+ local context = string.format('%s\nReference:\n%s\n\nObserved:\n%s', cause, tostring(reference), tostring(screenshot))
874
+ H.error_with_emphasis(fail_reason, context)
875
+ end
876
+
877
+ --- Create new expectation function
878
+ ---
879
+ --- Helper for writing custom functions with behavior similar to other methods
880
+ --- of |MiniTest.expect|.
881
+ ---
882
+ ---@param subject string|function|table Subject of expectation. If callable,
883
+ --- called with expectation input arguments to produce string value.
884
+ ---@param predicate function|table Predicate callable. Called with expectation
885
+ --- input arguments. Output `false` or `nil` means failed expectation.
886
+ ---@param fail_context string|function|table Information about fail. If callable,
887
+ --- called with expectation input arguments to produce string value.
888
+ ---
889
+ ---@return function Expectation function.
890
+ ---
891
+ ---@usage >lua
892
+ --- local expect_truthy = MiniTest.new_expectation(
893
+ --- 'truthy',
894
+ --- function(x) return x end,
895
+ --- function(x) return 'Object: ' .. vim.inspect(x) end
896
+ --- )
897
+ --- <
898
+ MiniTest.new_expectation = function(subject, predicate, fail_context)
899
+ return function(...)
900
+ if predicate(...) then return true end
901
+
902
+ local cur_subject = vim.is_callable(subject) and subject(...) or subject
903
+ local cur_context = vim.is_callable(fail_context) and fail_context(...) or fail_context
904
+ H.error_with_emphasis('Failed expectation for ' .. cur_subject, cur_context)
905
+ end
906
+ end
907
+
908
+ -- Reporters ------------------------------------------------------------------
909
+ --- Table with pre-configured report generators
910
+ ---
911
+ --- Each element is a function which returns reporter - table with callable
912
+ --- `start`, `update`, and `finish` fields.
913
+ MiniTest.gen_reporter = {}
914
+
915
+ --- Generate buffer reporter
916
+ ---
917
+ --- This is a default choice for interactive (not headless) usage. Opens a window
918
+ --- with dedicated non-terminal buffer and updates it with throttled redraws.
919
+ ---
920
+ --- Opened buffer has the following helpful Normal mode mappings:
921
+ --- - `<Esc>` - stop test execution if executing (see |MiniTest.is_executing()|
922
+ --- and |MiniTest.stop()|). Close window otherwise.
923
+ --- - `q` - same as `<Esc>` for convenience and compatibility.
924
+ ---
925
+ --- General idea:
926
+ --- - Group cases by concatenating first `opts.group_depth` elements of case
927
+ --- description (`desc` field). Groups by collected files if using default values.
928
+ --- - In `start()` show some stats to know how much is scheduled to be executed.
929
+ --- - In `update()` show symbolic overview of current group and state of current
930
+ --- case. Each symbol represents one case and its state:
931
+ --- - `?` - case didn't finish executing.
932
+ --- - `o` - pass.
933
+ --- - `O` - pass with notes.
934
+ --- - `x` - fail.
935
+ --- - `X` - fail with notes.
936
+ --- - In `finish()` show all fails and notes ordered by case.
937
+ ---
938
+ ---@param opts table|nil Table with options. Used fields:
939
+ --- - <group_depth> - number of first elements of case description (can be zero)
940
+ --- used for grouping. Higher values mean higher granularity of output.
941
+ --- Default: 1.
942
+ --- - <throttle_delay> - minimum number of milliseconds to wait between
943
+ --- redrawing. Reduces screen flickering but not amount of computations.
944
+ --- Default: 10.
945
+ --- - <window> - definition of window to open. Can take one of the forms:
946
+ --- - Callable. It is called expecting output to be target window id
947
+ --- (current window is used if output is `nil`). Use this to open in
948
+ --- "normal" window (like `function() vim.cmd('vsplit') end`).
949
+ --- - Table. Used as `config` argument in |nvim_open_win()|.
950
+ --- Default: table for centered floating window.
951
+ MiniTest.gen_reporter.buffer = function(opts)
952
+ -- NOTE: another choice of implementing this is to use terminal buffer
953
+ -- `vim.api.nvim_open_term()`.
954
+ -- Pros:
955
+ -- - Renders ANSI escape sequences (mostly) correctly, i.e. no need in
956
+ -- replacing them with Neovim range highlights.
957
+ -- - This reporter and `stdout` one can share more of a codebase.
958
+ -- Cons:
959
+ -- - Couldn't manage to implement "redraw on every update".
960
+ -- - Extra steps still are needed in order to have richer output information.
961
+ -- This involves ANSI sequences that move cursor, which have same issues as
962
+ -- in `stdout`, albeit easier to overcome:
963
+ -- - Handling of scroll.
964
+ -- - Hard wrapping of lines leading to need of using window width.
965
+ local default_opts = { group_depth = 1, throttle_delay = 10, window = H.buffer_reporter.default_window_opts() }
966
+ opts = vim.tbl_deep_extend('force', default_opts, opts or {})
967
+
968
+ local buf_id, win_id
969
+ local is_valid_buf_win = function() return vim.api.nvim_buf_is_valid(buf_id) and vim.api.nvim_win_is_valid(win_id) end
970
+
971
+ -- Helpers
972
+ local set_cursor = function(line)
973
+ vim.api.nvim_win_set_cursor(win_id, { line or vim.api.nvim_buf_line_count(buf_id), 0 })
974
+ end
975
+
976
+ -- Define "write from cursor line" function with throttled redraw
977
+ local latest_draw_time = 0
978
+ local replace_last = function(n_replace, lines, force)
979
+ H.buffer_reporter.set_lines(buf_id, lines, -n_replace - 1, -1)
980
+
981
+ -- Throttle redraw to reduce flicker
982
+ local cur_time = vim.loop.hrtime()
983
+ local is_enough_time_passed = (cur_time - latest_draw_time) > opts.throttle_delay * 1000000
984
+ if is_enough_time_passed or force then
985
+ vim.cmd('redraw')
986
+ latest_draw_time = cur_time
987
+ end
988
+ end
989
+
990
+ -- Create reporter functions
991
+ local res = {}
992
+ local all_cases, all_groups, latest_group_name
993
+
994
+ res.start = function(cases)
995
+ -- Set up buffer and window
996
+ buf_id, win_id = H.buffer_reporter.setup_buf_and_win(opts.window)
997
+
998
+ -- Set up data (taking into account possible not first time run)
999
+ all_cases = cases
1000
+ all_groups = H.overview_reporter.compute_groups(cases, opts.group_depth)
1001
+ latest_group_name = nil
1002
+
1003
+ -- Write lines
1004
+ local lines = H.overview_reporter.start_lines(all_cases, all_groups)
1005
+ replace_last(1, lines)
1006
+ set_cursor()
1007
+ end
1008
+
1009
+ res.update = function(case_num)
1010
+ if not is_valid_buf_win() then return end
1011
+
1012
+ local case, cur_group_name = all_cases[case_num], all_groups[case_num].name
1013
+
1014
+ -- Update symbol
1015
+ local state = type(case.exec) == 'table' and case.exec.state or nil
1016
+ all_groups[case_num].symbol = H.reporter_symbols[state]
1017
+
1018
+ local n_replace = H.buffer_reporter.update_step_n_replace(latest_group_name, cur_group_name)
1019
+ local lines = H.buffer_reporter.update_step_lines(case_num, all_cases, all_groups)
1020
+ replace_last(n_replace, lines)
1021
+ set_cursor()
1022
+
1023
+ latest_group_name = cur_group_name
1024
+ end
1025
+
1026
+ res.finish = function()
1027
+ if not is_valid_buf_win() then return end
1028
+
1029
+ -- Cache final cursor position to overwrite 'Current case state' header
1030
+ local start_line = vim.api.nvim_buf_line_count(buf_id) - 1
1031
+
1032
+ -- Force writing lines
1033
+ local lines = H.overview_reporter.finish_lines(all_cases)
1034
+ replace_last(2, lines, true)
1035
+ set_cursor(start_line)
1036
+ end
1037
+
1038
+ return res
1039
+ end
1040
+
1041
+ --- Generate stdout reporter
1042
+ ---
1043
+ --- This is a default choice for headless usage. Writes to `stdout`. Uses
1044
+ --- coloring ANSI escape sequences to make pretty and informative output
1045
+ --- (should work in most modern terminals and continuous integration providers).
1046
+ ---
1047
+ --- It has same general idea as |MiniTest.gen_reporter.buffer()| with slightly
1048
+ --- less output (it doesn't overwrite previous text) to overcome typical
1049
+ --- terminal limitations.
1050
+ ---
1051
+ ---@param opts table|nil Table with options. Used fields:
1052
+ --- - <group_depth> - number of first elements of case description (can be zero)
1053
+ --- used for grouping. Higher values mean higher granularity of output.
1054
+ --- Default: 1.
1055
+ --- - <quit_on_finish> - whether to quit after finishing test execution.
1056
+ --- Default: `true`.
1057
+ MiniTest.gen_reporter.stdout = function(opts)
1058
+ opts = vim.tbl_deep_extend('force', { group_depth = 1, quit_on_finish = true }, opts or {})
1059
+
1060
+ local write = function(text)
1061
+ text = type(text) == 'table' and table.concat(text, '\n') or text
1062
+ io.stdout:write(text)
1063
+ io.flush()
1064
+ end
1065
+
1066
+ local all_cases, all_groups, latest_group_name
1067
+ local default_symbol = H.reporter_symbols[nil]
1068
+
1069
+ local res = {}
1070
+
1071
+ res.start = function(cases)
1072
+ -- Set up data
1073
+ all_cases = cases
1074
+ all_groups = H.overview_reporter.compute_groups(cases, opts.group_depth)
1075
+
1076
+ -- Write lines
1077
+ local lines = H.overview_reporter.start_lines(all_cases, all_groups)
1078
+ write(lines)
1079
+ end
1080
+
1081
+ res.update = function(case_num)
1082
+ local cur_case = all_cases[case_num]
1083
+ local cur_group_name = all_groups[case_num].name
1084
+
1085
+ -- Possibly start overview of new group
1086
+ if cur_group_name ~= latest_group_name then
1087
+ write('\n')
1088
+ write(cur_group_name)
1089
+ if cur_group_name ~= '' then write(': ') end
1090
+ end
1091
+
1092
+ -- Possibly show new symbol
1093
+ local state = type(cur_case.exec) == 'table' and cur_case.exec.state or nil
1094
+ local cur_symbol = H.reporter_symbols[state]
1095
+ if cur_symbol ~= default_symbol then write(cur_symbol) end
1096
+
1097
+ latest_group_name = cur_group_name
1098
+ end
1099
+
1100
+ res.finish = function()
1101
+ write('\n\n')
1102
+ local lines = H.overview_reporter.finish_lines(all_cases)
1103
+ write(lines)
1104
+ write('\n')
1105
+
1106
+ -- Possibly quit
1107
+ if not opts.quit_on_finish then return end
1108
+ local command = string.format('silent! %scquit', H.has_fails(all_cases) and 1 or 0)
1109
+ vim.cmd(command)
1110
+ end
1111
+
1112
+ return res
1113
+ end
1114
+
1115
+ -- Exported utility functions -------------------------------------------------
1116
+ --- Create child Neovim process
1117
+ ---
1118
+ --- This creates an object designed to be a fundamental piece of |mini.test|
1119
+ --- methodology. It can start/stop/restart a separate (child) Neovim process
1120
+ --- (headless, but fully functioning) together with convenience helpers to
1121
+ --- interact with it through |RPC| messages.
1122
+ ---
1123
+ --- For more information see |MiniTest-child-neovim|.
1124
+ ---
1125
+ ---@return MiniTest.child Object of |MiniTest-child-neovim|.
1126
+ ---
1127
+ ---@usage >lua
1128
+ --- -- Initiate
1129
+ --- local child = MiniTest.new_child_neovim()
1130
+ --- child.start()
1131
+ ---
1132
+ --- -- Use API functions
1133
+ --- child.api.nvim_buf_set_lines(0, 0, -1, true, { 'Line inside child Neovim' })
1134
+ ---
1135
+ --- -- Execute Lua code, Vimscript commands, etc.
1136
+ --- child.lua('_G.n = 0')
1137
+ --- child.cmd('au CursorMoved * lua _G.n = _G.n + 1')
1138
+ --- child.type_keys('l')
1139
+ --- print(child.lua_get('_G.n')) -- Should be 1
1140
+ ---
1141
+ --- -- Use other `vim.xxx` Lua wrappers (executed inside child process)
1142
+ --- vim.b.aaa = 'current process'
1143
+ --- child.b.aaa = 'child process'
1144
+ --- print(child.lua_get('vim.b.aaa')) -- Should be 'child process'
1145
+ ---
1146
+ --- -- Always stop process after it is not needed
1147
+ --- child.stop()
1148
+ --- <
1149
+ MiniTest.new_child_neovim = function()
1150
+ local child = {}
1151
+ local start_args, start_opts
1152
+
1153
+ local ensure_running = function()
1154
+ if child.is_running() then return end
1155
+ H.error('Child process is not running. Did you call `child.start()`?')
1156
+ end
1157
+
1158
+ local prevent_hanging = function(method)
1159
+ if not child.is_blocked() then return end
1160
+
1161
+ local msg = string.format('Can not use `child.%s` because child process is blocked.', method)
1162
+ H.error_with_emphasis(msg)
1163
+ end
1164
+
1165
+ -- Start headless Neovim instance
1166
+ child.start = function(args, opts)
1167
+ if child.is_running() then
1168
+ H.message('Child process is already running. Use `child.restart()`.')
1169
+ return
1170
+ end
1171
+
1172
+ args = args or {}
1173
+ opts = vim.tbl_deep_extend('force', { nvim_executable = vim.v.progpath, connection_timeout = 5000 }, opts or {})
1174
+
1175
+ -- Make unique name for `--listen` pipe
1176
+ local job = { address = vim.fn.tempname() }
1177
+
1178
+ if vim.fn.has('win32') == 1 then
1179
+ -- Use special local pipe prefix on Windows with (hopefully) unique name
1180
+ -- Source: https://learn.microsoft.com/en-us/windows/win32/ipc/pipe-names
1181
+ job.address = [[\\.\pipe\mininvim]] .. vim.fn.fnamemodify(job.address, ':t')
1182
+ end
1183
+
1184
+ --stylua: ignore
1185
+ local full_args = {
1186
+ opts.nvim_executable, '--clean', '-n', '--listen', job.address,
1187
+ -- Setting 'lines' and 'columns' makes headless process more like
1188
+ -- interactive for closer to reality testing
1189
+ '--headless', '--cmd', 'set lines=24 columns=80'
1190
+ }
1191
+ vim.list_extend(full_args, args)
1192
+
1193
+ -- Using 'jobstart' for creating a job is crucial for getting this to work
1194
+ -- in Github Actions. Other approaches:
1195
+ -- - Using `{ pty = true }` seems crucial to make this work on GitHub CI.
1196
+ -- - Using `vim.loop.spawn()` is doable, but has some issues:
1197
+ -- - https://github.com/neovim/neovim/issues/21630
1198
+ -- - https://github.com/neovim/neovim/issues/21886
1199
+ job.id = vim.fn.jobstart(full_args)
1200
+
1201
+ local step = 10
1202
+ local connected, i, max_tries = nil, 0, math.floor(opts.connection_timeout / step)
1203
+ repeat
1204
+ i = i + 1
1205
+ vim.loop.sleep(step)
1206
+ connected, job.channel = pcall(vim.fn.sockconnect, 'pipe', job.address, { rpc = true })
1207
+ until connected or i >= max_tries
1208
+
1209
+ if not connected then
1210
+ local err = ' ' .. job.channel:gsub('\n', '\n ')
1211
+ H.error('Failed to make connection to child Neovim with the following error:\n' .. err)
1212
+ child.stop()
1213
+ end
1214
+
1215
+ child.job = job
1216
+ start_args, start_opts = args, opts
1217
+ end
1218
+
1219
+ child.stop = function()
1220
+ if not child.is_running() then return end
1221
+
1222
+ -- Properly exit Neovim. `pcall` avoids `channel closed by client` error.
1223
+ -- Also wait for it to actually close. This reduces simultaneously opened
1224
+ -- Neovim instances and CPU load (overall reducing flacky tests).
1225
+ pcall(child.cmd, 'silent! 0cquit')
1226
+ vim.fn.jobwait({ child.job.id }, 1000)
1227
+
1228
+ -- Close all used channels. Prevents `too many open files` type of errors.
1229
+ pcall(vim.fn.chanclose, child.job.channel)
1230
+ pcall(vim.fn.chanclose, child.job.id)
1231
+
1232
+ -- Remove file for address to reduce chance of "can't open file" errors, as
1233
+ -- address uses temporary unique files
1234
+ pcall(vim.fn.delete, child.job.address)
1235
+
1236
+ child.job = nil
1237
+ end
1238
+
1239
+ child.restart = function(args, opts)
1240
+ args = args or start_args
1241
+ opts = vim.tbl_deep_extend('force', start_opts or {}, opts or {})
1242
+
1243
+ child.stop()
1244
+ child.start(args, opts)
1245
+ end
1246
+
1247
+ -- Wrappers for common `vim.xxx` objects (will get executed inside child)
1248
+ child.api = setmetatable({}, {
1249
+ __index = function(_, key)
1250
+ ensure_running()
1251
+ return function(...) return vim.rpcrequest(child.job.channel, key, ...) end
1252
+ end,
1253
+ })
1254
+
1255
+ -- Variant of `api` functions called with `vim.rpcnotify`. Useful for making
1256
+ -- blocking requests (like `getcharstr()`).
1257
+ child.api_notify = setmetatable({}, {
1258
+ __index = function(_, key)
1259
+ ensure_running()
1260
+ return function(...) return vim.rpcnotify(child.job.channel, key, ...) end
1261
+ end,
1262
+ })
1263
+
1264
+ ---@return table Emulates `vim.xxx` table (like `vim.fn`)
1265
+ ---@private
1266
+ local redirect_to_child = function(tbl_name)
1267
+ -- TODO: try to figure out the best way to operate on tables with function
1268
+ -- values (needs "deep encode/decode" of function objects)
1269
+ return setmetatable({}, {
1270
+ __index = function(_, key)
1271
+ ensure_running()
1272
+
1273
+ local short_name = ('%s.%s'):format(tbl_name, key)
1274
+ local obj_name = ('vim[%s][%s]'):format(vim.inspect(tbl_name), vim.inspect(key))
1275
+
1276
+ prevent_hanging(short_name)
1277
+ local value_type = child.api.nvim_exec_lua(('return type(%s)'):format(obj_name), {})
1278
+
1279
+ if value_type == 'function' then
1280
+ -- This allows syntax like `child.fn.mode(1)`
1281
+ return function(...)
1282
+ prevent_hanging(short_name)
1283
+ return child.api.nvim_exec_lua(('return %s(...)'):format(obj_name), { ... })
1284
+ end
1285
+ end
1286
+
1287
+ -- This allows syntax like `child.bo.buftype`
1288
+ prevent_hanging(short_name)
1289
+ return child.api.nvim_exec_lua(('return %s'):format(obj_name), {})
1290
+ end,
1291
+ __newindex = function(_, key, value)
1292
+ ensure_running()
1293
+
1294
+ local short_name = ('%s.%s'):format(tbl_name, key)
1295
+ local obj_name = ('vim[%s][%s]'):format(vim.inspect(tbl_name), vim.inspect(key))
1296
+
1297
+ -- This allows syntax like `child.b.aaa = function(x) return x + 1 end`
1298
+ -- (inherits limitations of `string.dump`: no upvalues, etc.)
1299
+ if type(value) == 'function' then
1300
+ local dumped = vim.inspect(string.dump(value))
1301
+ value = ('loadstring(%s)'):format(dumped)
1302
+ else
1303
+ value = vim.inspect(value)
1304
+ end
1305
+
1306
+ prevent_hanging(short_name)
1307
+ child.api.nvim_exec_lua(('%s = %s'):format(obj_name, value), {})
1308
+ end,
1309
+ })
1310
+ end
1311
+
1312
+ --stylua: ignore start
1313
+ local supported_vim_tables = {
1314
+ -- Collections
1315
+ 'diagnostic', 'fn', 'highlight', 'hl', 'json', 'loop', 'lsp', 'mpack', 'spell', 'treesitter', 'ui', 'fs',
1316
+ -- Variables
1317
+ 'g', 'b', 'w', 't', 'v', 'env',
1318
+ -- Options (no 'opt' because not really useful due to use of metatables)
1319
+ 'o', 'go', 'bo', 'wo',
1320
+ }
1321
+ --stylua: ignore end
1322
+ for _, v in ipairs(supported_vim_tables) do
1323
+ child[v] = redirect_to_child(v)
1324
+ end
1325
+
1326
+ -- Convenience wrappers
1327
+ child.type_keys = function(wait, ...)
1328
+ ensure_running()
1329
+
1330
+ local has_wait = type(wait) == 'number'
1331
+ local keys = has_wait and { ... } or { wait, ... }
1332
+ keys = H.tbl_flatten(keys)
1333
+
1334
+ -- From `nvim_input` docs: "On execution error: does not fail, but
1335
+ -- updates v:errmsg.". So capture it manually. NOTE: Have it global to
1336
+ -- allow sending keys which will block in the middle (like `[[<C-\>]]` and
1337
+ -- `<C-n>`). Otherwise, later check will assume that there was an error.
1338
+ local cur_errmsg
1339
+ for _, k in ipairs(keys) do
1340
+ if type(k) ~= 'string' then
1341
+ error('In `type_keys()` each argument should be either string or array of strings.')
1342
+ end
1343
+
1344
+ -- But do that only if Neovim is not "blocked". Otherwise, usage of
1345
+ -- `child.v` will block execution.
1346
+ if not child.is_blocked() then
1347
+ cur_errmsg = child.v.errmsg
1348
+ child.v.errmsg = ''
1349
+ end
1350
+
1351
+ -- Need to escape bare `<` (see `:h nvim_input`)
1352
+ child.api.nvim_input(k == '<' and '<LT>' or k)
1353
+
1354
+ -- Possibly throw error manually
1355
+ if not child.is_blocked() then
1356
+ if child.v.errmsg ~= '' then
1357
+ error(child.v.errmsg, 2)
1358
+ else
1359
+ child.v.errmsg = cur_errmsg or ''
1360
+ end
1361
+ end
1362
+
1363
+ -- Possibly wait
1364
+ if has_wait and wait > 0 then vim.loop.sleep(wait) end
1365
+ end
1366
+ end
1367
+
1368
+ child.cmd = function(str)
1369
+ ensure_running()
1370
+ prevent_hanging('cmd')
1371
+ return child.api.nvim_exec(str, false)
1372
+ end
1373
+
1374
+ child.cmd_capture = function(str)
1375
+ ensure_running()
1376
+ prevent_hanging('cmd_capture')
1377
+ return child.api.nvim_exec(str, true)
1378
+ end
1379
+
1380
+ child.lua = function(str, args)
1381
+ ensure_running()
1382
+ prevent_hanging('lua')
1383
+ return child.api.nvim_exec_lua(str, args or {})
1384
+ end
1385
+
1386
+ child.lua_notify = function(str, args)
1387
+ ensure_running()
1388
+ return child.api_notify.nvim_exec_lua(str, args or {})
1389
+ end
1390
+
1391
+ child.lua_get = function(str, args)
1392
+ ensure_running()
1393
+ prevent_hanging('lua_get')
1394
+ return child.api.nvim_exec_lua('return ' .. str, args or {})
1395
+ end
1396
+
1397
+ child.lua_func = function(f, ...)
1398
+ ensure_running()
1399
+ prevent_hanging('lua_func')
1400
+ return child.api.nvim_exec_lua(
1401
+ 'local f = ...; return assert(loadstring(f))(select(2, ...))',
1402
+ { string.dump(f), ... }
1403
+ )
1404
+ end
1405
+
1406
+ child.is_blocked = function()
1407
+ ensure_running()
1408
+ return child.api.nvim_get_mode()['blocking']
1409
+ end
1410
+
1411
+ child.is_running = function() return child.job ~= nil end
1412
+
1413
+ -- Various wrappers
1414
+ child.ensure_normal_mode = function()
1415
+ ensure_running()
1416
+ child.type_keys([[<C-\>]], '<C-n>')
1417
+ end
1418
+
1419
+ child.get_screenshot = function(opts)
1420
+ ensure_running()
1421
+ prevent_hanging('get_screenshot')
1422
+
1423
+ opts = vim.tbl_deep_extend('force', { redraw = true }, opts or {})
1424
+
1425
+ if opts.redraw then child.cmd('redraw') end
1426
+
1427
+ local res = child.lua([[
1428
+ local text, attr = {}, {}
1429
+ for i = 1, vim.o.lines do
1430
+ local text_line, attr_line = {}, {}
1431
+ for j = 1, vim.o.columns do
1432
+ table.insert(text_line, vim.fn.screenstring(i, j))
1433
+ table.insert(attr_line, vim.fn.screenattr(i, j))
1434
+ end
1435
+ table.insert(text, text_line)
1436
+ table.insert(attr, attr_line)
1437
+ end
1438
+ return { text = text, attr = attr }
1439
+ ]])
1440
+ res.attr = H.screenshot_encode_attr(res.attr)
1441
+
1442
+ return H.screenshot_new(res)
1443
+ end
1444
+
1445
+ -- Register `child` for automatic stop in case of emergency
1446
+ table.insert(H.child_neovim_registry, child)
1447
+
1448
+ return child
1449
+ end
1450
+
1451
+ --- Child class
1452
+ ---
1453
+ --- It offers a great set of tools to write reliable and reproducible tests by
1454
+ --- allowing to use fresh process in any test action. Interaction with it is done
1455
+ --- through |RPC| protocol.
1456
+ ---
1457
+ --- Although quite flexible, at the moment it has certain limitations:
1458
+ --- - Doesn't allow using functions or userdata for child's both inputs and
1459
+ --- outputs. Usual solution is to move computations from current Neovim process
1460
+ --- to child process. Use `child.lua()` and `child.lua_get()` for that.
1461
+ --- - When writing tests, it is common to end up with "hanging" process: it
1462
+ --- stops executing without any output. Most of the time it is because Neovim
1463
+ --- process is "blocked", i.e. it waits for user input and won't return from
1464
+ --- other call (like `child.api.nvim_exec_lua()`). Common causes are active
1465
+ --- |hit-enter-prompt| (increase prompt height to a bigger value) or
1466
+ --- Operator-pending mode (exit it). To mitigate this experience, most helpers
1467
+ --- will throw an error if its immediate execution will lead to hanging state.
1468
+ --- Also in case of hanging state try `child.api_notify` instead of `child.api`.
1469
+ ---
1470
+ --- Notes:
1471
+ --- - An important type of field is a "redirection table". It acts as a
1472
+ --- convenience wrapper for corresponding `vim.*` table. Can be used both to
1473
+ --- return and set values. Examples:
1474
+ --- - `child.api.nvim_buf_line_count(0)` will execute
1475
+ --- `vim.api.nvim_buf_line_count(0)` inside child process and return its
1476
+ --- output to current process.
1477
+ --- - `child.bo.filetype = 'lua'` will execute `vim.bo.filetype = 'lua'`
1478
+ --- inside child process.
1479
+ --- They still have same limitations listed above, so are not perfect. In
1480
+ --- case of a doubt, use `child.lua()`.
1481
+ --- - Almost all methods use |vim.rpcrequest()| (i.e. wait for call to finish and
1482
+ --- then return value). See for `*_notify` variant to use |vim.rpcnotify()|.
1483
+ --- - All fields and methods should be called with `.`, not `:`.
1484
+ ---
1485
+ ---@class MiniTest.child
1486
+ ---
1487
+ ---@field start function Start child process. See |MiniTest-child-neovim-start()|.
1488
+ ---@field stop function Stop current child process.
1489
+ ---@field restart function Restart child process: stop if running and then
1490
+ --- start a new one. Takes same arguments as `child.start()` but uses values
1491
+ --- from most recent `start()` call as defaults.
1492
+ ---
1493
+ ---@field type_keys function Emulate typing keys.
1494
+ --- See |MiniTest-child-neovim-type_keys()|. Doesn't check for blocked state.
1495
+ ---
1496
+ ---@field cmd function Execute Vimscript code from a string.
1497
+ --- A wrapper for |nvim_exec()| without capturing output.
1498
+ ---@field cmd_capture function Execute Vimscript code from a string and
1499
+ --- capture output. A wrapper for |nvim_exec()| with capturing output.
1500
+ ---
1501
+ ---@field lua function Execute Lua code. A wrapper for |nvim_exec_lua()|.
1502
+ ---@field lua_notify function Execute Lua code without waiting for output.
1503
+ ---@field lua_get function Execute Lua code and return result. A wrapper
1504
+ --- for |nvim_exec_lua()| but prepends string code with `return`.
1505
+ ---@field lua_func function Execute Lua function and return it's result.
1506
+ --- Function will be called with all extra parameters (second one and later).
1507
+ --- Note: usage of upvalues (data from outside function scope) is not allowed.
1508
+ ---
1509
+ ---@field is_blocked function Check whether child process is blocked.
1510
+ ---@field is_running function Check whether child process is currently running.
1511
+ ---
1512
+ ---@field ensure_normal_mode function Ensure normal mode.
1513
+ ---@field get_screenshot function Returns table with two "2d arrays" of single
1514
+ --- characters representing what is displayed on screen and how it looks.
1515
+ --- Has `opts` table argument for optional configuratnion.
1516
+ ---
1517
+ ---@field job table|nil Information about current job. If `nil`, child is not running.
1518
+ ---
1519
+ ---@field api table Redirection table for `vim.api`. Doesn't check for blocked state.
1520
+ ---@field api_notify table Same as `api`, but uses |vim.rpcnotify()|.
1521
+ ---
1522
+ ---@field diagnostic table Redirection table for |vim.diagnostic|.
1523
+ ---@field fn table Redirection table for |vim.fn|.
1524
+ ---@field highlight table Redirection table for |vim.highlight|.
1525
+ ---@field hl table Redirection table for |vim.hl|.
1526
+ ---@field json table Redirection table for `vim.json`.
1527
+ ---@field loop table Redirection table for |vim.loop|.
1528
+ ---@field lsp table Redirection table for `vim.lsp` (|lsp-core|).
1529
+ ---@field mpack table Redirection table for |vim.mpack|.
1530
+ ---@field spell table Redirection table for |vim.spell|.
1531
+ ---@field treesitter table Redirection table for `vim.treesitter` (|lua-treesitter-core|).
1532
+ ---@field ui table Redirection table for |vim.ui|. Currently of no
1533
+ --- use because it requires sending function through RPC, which is impossible
1534
+ --- at the moment.
1535
+ ---@field fs table Redirection table for |vim.fs|.
1536
+ ---
1537
+ ---@field g table Redirection table for |vim.g|.
1538
+ ---@field b table Redirection table for |vim.b|.
1539
+ ---@field w table Redirection table for |vim.w|.
1540
+ ---@field t table Redirection table for |vim.t|.
1541
+ ---@field v table Redirection table for |vim.v|.
1542
+ ---@field env table Redirection table for |vim.env|.
1543
+ ---
1544
+ ---@field o table Redirection table for |vim.o|.
1545
+ ---@field go table Redirection table for |vim.go|.
1546
+ ---@field bo table Redirection table for |vim.bo|.
1547
+ ---@field wo table Redirection table for |vim.wo|.
1548
+ ---@tag MiniTest-child-neovim
1549
+
1550
+ --- `child.start`({args}, {opts})
1551
+ ---
1552
+ --- Start child process and connect to it. Won't work if child is already running.
1553
+ ---
1554
+ ---@param args table Array with arguments for executable. Will be prepended with
1555
+ --- the following default arguments (see |startup-options|): >lua
1556
+ --- { '--clean', '-n', '--listen', <some address>,
1557
+ --- '--headless', '--cmd', 'set lines=24 columns=80' }
1558
+ --- <
1559
+ ---@param opts table|nil Options:
1560
+ --- - <nvim_executable> - name of Neovim executable. Default: |v:progpath|.
1561
+ --- - <connection_timeout> - stop trying to connect after this amount of
1562
+ --- milliseconds. Default: 5000.
1563
+ ---
1564
+ ---@usage >lua
1565
+ --- child = MiniTest.new_child_neovim()
1566
+ ---
1567
+ --- -- Start default clean Neovim instance
1568
+ --- child.start()
1569
+ ---
1570
+ --- -- Start with custom 'init.lua' file
1571
+ --- child.start({ '-u', 'scripts/minimal_init.lua' })
1572
+ --- <
1573
+ ---@tag MiniTest-child-neovim-start()
1574
+
1575
+ --- `child.type_keys`({wait}, {...})
1576
+ ---
1577
+ --- Basically a wrapper for |nvim_input()| applied inside child process.
1578
+ --- Differences:
1579
+ --- - Can wait after each group of characters.
1580
+ --- - Raises error if typing keys resulted into error in child process (i.e. its
1581
+ --- |v:errmsg| was updated).
1582
+ --- - Key `<` as a separate entry may not be escaped as `<LT>`.
1583
+ ---
1584
+ ---@param wait number|nil Number of milliseconds to wait after each entry. May be
1585
+ --- omitted, in which case no waiting is done.
1586
+ ---@param ... string|table<number,string> Separate entries for |nvim_input()|,
1587
+ --- after which `wait` will be applied. Can be either string or array of strings.
1588
+ ---
1589
+ ---@usage >lua
1590
+ --- -- All of these type keys 'c', 'a', 'w'
1591
+ --- child.type_keys('caw')
1592
+ --- child.type_keys('c', 'a', 'w')
1593
+ --- child.type_keys('c', { 'a', 'w' })
1594
+ ---
1595
+ --- -- Waits 5 ms after `c` and after 'w'
1596
+ --- child.type_keys(5, 'c', { 'a', 'w' })
1597
+ ---
1598
+ --- -- Special keys can also be used
1599
+ --- child.type_keys('i', 'Hello world', '<Esc>')
1600
+ --- <
1601
+ ---@tag MiniTest-child-neovim-type_keys()
1602
+
1603
+ --- `child.get_screenshot`({opts})
1604
+ ---
1605
+ --- Compute what is displayed on (default TUI) screen and how it is displayed.
1606
+ --- This basically calls |screenstring()| and |screenattr()| for every visible
1607
+ --- cell (row from 1 to |'lines'|, column from 1 to |'columns'|).
1608
+ ---
1609
+ --- Notes:
1610
+ --- - To make output more portable and visually useful, outputs of
1611
+ --- `screenattr()` are coded with single character symbols. Those are taken from
1612
+ --- 94 characters (ASCII codes between 33 and 126), so there will be duplicates
1613
+ --- in case of more than 94 different ways text is displayed on screen.
1614
+ ---
1615
+ ---@param opts table|nil Options. Possieble fields:
1616
+ --- - <redraw> `(boolean)` - whether to call |:redraw| prior to computing
1617
+ --- screenshot. Default: `true`.
1618
+ ---
1619
+ ---@return table|nil Screenshot table with the following fields:
1620
+ --- - <text> - "2d array" (row-column) of single characters displayed at
1621
+ --- particular cells.
1622
+ --- - <attr> - "2d array" (row-column) of symbols representing how text is
1623
+ --- displayed (basically, "coded" appearance/highlighting). They should be
1624
+ --- used only in relation to each other: same/different symbols for two
1625
+ --- cells mean same/different visual appearance. Note: there will be false
1626
+ --- positives if there are more than 94 different attribute values.
1627
+ --- It also can be used with `tostring()` to convert to single string (used
1628
+ --- for writing to reference file). It results into two visual parts
1629
+ --- (separated by empty line), for `text` and `attr`. Each part has "ruler"
1630
+ --- above content and line numbers for each line.
1631
+ --- Returns `nil` if couldn't get a reasonable screenshot.
1632
+ ---
1633
+ ---@usage >lua
1634
+ --- local screenshot = child.get_screenshot()
1635
+ ---
1636
+ --- -- Show character displayed row=3 and column=4
1637
+ --- print(screenshot.text[3][4])
1638
+ ---
1639
+ --- -- Convert to string
1640
+ --- tostring(screenshot)
1641
+ --- <
1642
+ ---@tag MiniTest-child-neovim-get_screenshot()
1643
+
1644
+ -- Helper data ================================================================
1645
+ -- Module default config
1646
+ H.default_config = vim.deepcopy(MiniTest.config)
1647
+
1648
+ -- Whether instance is running in headless mode
1649
+ H.is_headless = #vim.api.nvim_list_uis() == 0
1650
+
1651
+ -- Cache for various data
1652
+ H.cache = {
1653
+ -- Message with which case is meant to be skipped
1654
+ skip_message = nil,
1655
+ -- Queue of callables to be executed after step (hook or test function)
1656
+ finally = {},
1657
+ -- Whether to stop async execution
1658
+ should_stop_execution = false,
1659
+ -- Number of screenshots made in current case
1660
+ n_screenshots = 0,
1661
+ }
1662
+
1663
+ -- Registry of all Neovim child processes
1664
+ H.child_neovim_registry = {}
1665
+
1666
+ -- ANSI codes for common cases
1667
+ H.ansi_codes = {
1668
+ fail = '\27[1;31m', -- Bold red
1669
+ pass = '\27[1;32m', -- Bold green
1670
+ emphasis = '\27[1m', -- Bold
1671
+ reset = '\27[0m',
1672
+ }
1673
+
1674
+ -- Highlight groups for common ANSI codes
1675
+ H.hl_groups = {
1676
+ ['\27[1;31m'] = 'MiniTestFail',
1677
+ ['\27[1;32m'] = 'MiniTestPass',
1678
+ ['\27[1m'] = 'MiniTestEmphasis',
1679
+ }
1680
+
1681
+ -- Symbols used in reporter output
1682
+ --stylua: ignore
1683
+ H.reporter_symbols = setmetatable({
1684
+ ['Pass'] = H.ansi_codes.pass .. 'o' .. H.ansi_codes.reset,
1685
+ ['Pass with notes'] = H.ansi_codes.pass .. 'O' .. H.ansi_codes.reset,
1686
+ ['Fail'] = H.ansi_codes.fail .. 'x' .. H.ansi_codes.reset,
1687
+ ['Fail with notes'] = H.ansi_codes.fail .. 'X' .. H.ansi_codes.reset,
1688
+ }, {
1689
+ __index = function() return H.ansi_codes.emphasis .. '?' .. H.ansi_codes.reset end,
1690
+ })
1691
+
1692
+ -- Helper functionality =======================================================
1693
+ -- Settings -------------------------------------------------------------------
1694
+ H.setup_config = function(config)
1695
+ H.check_type('config', config, 'table', true)
1696
+ config = vim.tbl_deep_extend('force', vim.deepcopy(H.default_config), config or {})
1697
+
1698
+ H.check_type('collect', config.collect, 'table')
1699
+ H.check_type('collect.emulate_busted', config.collect.emulate_busted, 'boolean')
1700
+ H.check_type('collect.find_files', config.collect.find_files, 'function')
1701
+ H.check_type('collect.filter_cases', config.collect.filter_cases, 'function')
1702
+
1703
+ H.check_type('execute', config.execute, 'table')
1704
+ H.check_type('execute.reporter', config.execute.reporter, 'table', true)
1705
+ H.check_type('execute.stop_on_error', config.execute.stop_on_error, 'boolean')
1706
+
1707
+ H.check_type('script_path', config.script_path, 'string')
1708
+ H.check_type('silent', config.silent, 'boolean')
1709
+
1710
+ return config
1711
+ end
1712
+
1713
+ H.apply_config = function(config) MiniTest.config = config end
1714
+
1715
+ H.create_autocommands = function()
1716
+ local gr = vim.api.nvim_create_augroup('MiniTest', {})
1717
+ vim.api.nvim_create_autocmd('ColorScheme', { group = gr, callback = H.create_default_hl, desc = 'Ensure colors' })
1718
+ end
1719
+
1720
+ H.create_default_hl = function()
1721
+ local set_default_hl = function(name, data)
1722
+ data.default = true
1723
+ vim.api.nvim_set_hl(0, name, data)
1724
+ end
1725
+
1726
+ set_default_hl('MiniTestFail', { fg = vim.g.terminal_color_1 or '#FF0000', bold = true })
1727
+ set_default_hl('MiniTestPass', { fg = vim.g.terminal_color_2 or '#00FF00', bold = true })
1728
+ set_default_hl('MiniTestEmphasis', { bold = true })
1729
+ end
1730
+
1731
+ H.is_disabled = function() return vim.g.minitest_disable == true or vim.b.minitest_disable == true end
1732
+
1733
+ H.get_config = function(config)
1734
+ return vim.tbl_deep_extend('force', MiniTest.config, vim.b.minitest_config or {}, config or {})
1735
+ end
1736
+
1737
+ -- Work with collection -------------------------------------------------------
1738
+ H.busted_emulate = function(set)
1739
+ local cur_set = set
1740
+
1741
+ _G.describe = function(name, f)
1742
+ local cur_set_parent = cur_set
1743
+ cur_set_parent[name] = MiniTest.new_set()
1744
+ cur_set = cur_set_parent[name]
1745
+ f()
1746
+ cur_set = cur_set_parent
1747
+ end
1748
+
1749
+ _G.it = function(name, f) cur_set[name] = f end
1750
+
1751
+ local setting_hook = function(hook_name)
1752
+ return function(hook)
1753
+ local metatbl = getmetatable(cur_set)
1754
+ metatbl.opts.hooks = metatbl.opts.hooks or {}
1755
+ metatbl.opts.hooks[hook_name] = hook
1756
+ end
1757
+ end
1758
+
1759
+ _G.setup = setting_hook('pre_once')
1760
+ _G.before_each = setting_hook('pre_case')
1761
+ _G.after_each = setting_hook('post_case')
1762
+ _G.teardown = setting_hook('post_once')
1763
+ end
1764
+
1765
+ H.busted_deemulate = function()
1766
+ local fun_names = { 'describe', 'it', 'setup', 'before_each', 'after_each', 'teardown' }
1767
+ for _, f_name in ipairs(fun_names) do
1768
+ _G[f_name] = nil
1769
+ end
1770
+ end
1771
+
1772
+ -- Work with execution --------------------------------------------------------
1773
+ H.execute_project_script = function(...)
1774
+ -- Don't process script if there are more than one active `run` calls
1775
+ if H.is_inside_script then return false end
1776
+
1777
+ -- Don't process script if at least one argument is not default (`nil`)
1778
+ if #{ ... } > 0 then return end
1779
+
1780
+ -- Store information
1781
+ local config_cache = vim.deepcopy(MiniTest.config)
1782
+ local local_config_cache = vim.b.minitest_config
1783
+
1784
+ -- Pass information to a possible `run()` call inside script
1785
+ H.is_inside_script = true
1786
+
1787
+ -- Execute script
1788
+ local success = pcall(vim.cmd, 'luafile ' .. H.get_config().script_path)
1789
+
1790
+ -- Restore information
1791
+ MiniTest.config = config_cache
1792
+ vim.b.minitest_config = local_config_cache
1793
+ H.is_inside_script = nil
1794
+
1795
+ return success
1796
+ end
1797
+
1798
+ H.make_case = function(case, case_num, opts)
1799
+ local update_state = function(state)
1800
+ case.exec.state = state
1801
+ H.exec_callable(opts.reporter.update, case_num)
1802
+ end
1803
+
1804
+ local is_case_executed = false
1805
+ local on_err = function(e)
1806
+ if H.cache.skip_message ~= nil then
1807
+ -- Add skip message to notes (not fails) only during main case execution
1808
+ if is_case_executed then
1809
+ table.insert(case.exec.notes, H.cache.skip_message)
1810
+ H.cache.skip_message = nil
1811
+ end
1812
+ return true
1813
+ end
1814
+
1815
+ -- Append traceback to error message and indent lines for pretty print
1816
+ local error_lines = { tostring(e), 'Traceback:', unpack(H.traceback()) }
1817
+ local error_msg = table.concat(error_lines, '\n'):gsub('\n', '\n ')
1818
+ table.insert(case.exec.fails, error_msg)
1819
+
1820
+ return false
1821
+ end
1822
+
1823
+ local exec_step = function(f, state)
1824
+ update_state(state)
1825
+
1826
+ H.cache.finally, H.cache.n_screenshots = {}, 0
1827
+ local ok_f, ok_err = xpcall(f, on_err)
1828
+
1829
+ for _, fin in ipairs(H.cache.finally) do
1830
+ H.exec_callable(fin)
1831
+ end
1832
+
1833
+ return ok_f or ok_err
1834
+ end
1835
+
1836
+ local exec_hooks = function(name, source)
1837
+ local source_arr = case.hooks[name .. '_source']
1838
+ local state_prefix = "Executing '" .. name .. "' hook #"
1839
+ for i, h in ipairs(case.hooks[name]) do
1840
+ if source_arr[i] == source then exec_step(h, state_prefix .. i) end
1841
+ end
1842
+ end
1843
+
1844
+ return function()
1845
+ if H.cache.should_stop_execution then return end
1846
+
1847
+ case.exec = { fails = {}, notes = {} }
1848
+ MiniTest.current.case = case
1849
+
1850
+ exec_hooks('pre', 'once')
1851
+ local exec_data = case.exec
1852
+
1853
+ local ok_case
1854
+ for _ = 1, case.n_retry do
1855
+ -- Ensure that fails and notes are not accumulated during retries
1856
+ case.exec = vim.deepcopy(exec_data)
1857
+
1858
+ -- Ensure that `skip()` affects only `pre_case` hooks and case
1859
+ H.cache.skip_message = nil
1860
+
1861
+ -- Executing `*_case` hooks on every retry should ensure same case setup
1862
+ -- (like cleanly restarted child process)
1863
+ exec_hooks('pre', 'case')
1864
+
1865
+ local case_f = function() case.test(unpack(case.args)) end
1866
+ if #case.exec.fails > 0 then
1867
+ case_f = function() table.insert(case.exec.notes, 'Skip case due to error(s) in hooks.') end
1868
+ end
1869
+ if H.cache.skip_message ~= nil then case_f = function() MiniTest.skip(H.cache.skip_message) end end
1870
+
1871
+ is_case_executed = true
1872
+ ok_case = exec_step(case_f, 'Executing test')
1873
+ is_case_executed = false
1874
+
1875
+ exec_hooks('post', 'case')
1876
+
1877
+ if ok_case then break end
1878
+ end
1879
+
1880
+ exec_hooks('post', 'once')
1881
+
1882
+ update_state(H.case_final_state(case))
1883
+
1884
+ if not ok_case and opts.stop_on_error then MiniTest.stop() end
1885
+ end
1886
+ end
1887
+
1888
+ -- Work with test cases -------------------------------------------------------
1889
+ --- Convert test set to array of test cases
1890
+ ---
1891
+ ---@return ... Tuple of aligned arrays: with test cases and hooks that should
1892
+ --- be executed only once before corresponding item.
1893
+ ---@private
1894
+ H.set_to_testcases = function(set, template, hooks_once)
1895
+ template = template or { args = {}, desc = {}, hooks = { pre = {}, post = {} }, data = {}, n_retry = 1 }
1896
+ hooks_once = hooks_once or { pre = {}, post = {} }
1897
+
1898
+ local metatbl = getmetatable(set)
1899
+ local opts, key_order = metatbl.opts, metatbl.key_order
1900
+ local hooks, parametrize, data, n_retry = opts.hooks or {}, opts.parametrize or { {} }, opts.data or {}, opts.n_retry
1901
+
1902
+ -- Convert to steps only callable or test set nodes
1903
+ -- Ensure that all elements of `set` are being considered (might not be the
1904
+ -- case if `table.insert` was used, for example)
1905
+ key_order = H.ensure_all_vals(key_order, vim.tbl_keys(set))
1906
+ local node_keys = vim.tbl_filter(function(key)
1907
+ local node = set[key]
1908
+ return vim.is_callable(node) or H.is_instance(node, 'testset')
1909
+ end, key_order)
1910
+
1911
+ if #node_keys == 0 then return {}, {} end
1912
+
1913
+ -- Ensure that newly added hooks are represented by new functions.
1914
+ -- This is needed to count them later only within current set. Example: use
1915
+ -- the same function in several `_once` hooks. In `H.inject_hooks_once` it
1916
+ -- will be injected only once overall whereas it should be injected only once
1917
+ -- within corresponding test set.
1918
+ hooks_once =
1919
+ H.extend_hooks(hooks_once, { pre = H.wrap_callable(hooks.pre_once), post = H.wrap_callable(hooks.post_once) })
1920
+
1921
+ local testcase_arr, hooks_once_arr = {}, {}
1922
+ -- Process nodes in order they were added as `T[...] = x`
1923
+ for _, key in ipairs(node_keys) do
1924
+ local node = set[key]
1925
+ for _, args in ipairs(parametrize) do
1926
+ if type(args) ~= 'table' then H.error('`parametrize` should have only tables. Got ' .. vim.inspect(args)) end
1927
+
1928
+ local cur_template = H.extend_template(template, {
1929
+ args = args,
1930
+ desc = type(key) == 'string' and key:gsub('\n', '\\n') or key,
1931
+ hooks = { pre = hooks.pre_case, post = hooks.post_case },
1932
+ data = data,
1933
+ n_retry = n_retry,
1934
+ })
1935
+
1936
+ if vim.is_callable(node) then
1937
+ table.insert(testcase_arr, H.new_testcase(cur_template, node))
1938
+ table.insert(hooks_once_arr, hooks_once)
1939
+ elseif H.is_instance(node, 'testset') then
1940
+ local nest_testcase_arr, nest_hooks_once_arr = H.set_to_testcases(node, cur_template, hooks_once)
1941
+ vim.list_extend(testcase_arr, nest_testcase_arr)
1942
+ vim.list_extend(hooks_once_arr, nest_hooks_once_arr)
1943
+ end
1944
+ end
1945
+ end
1946
+
1947
+ return testcase_arr, hooks_once_arr
1948
+ end
1949
+
1950
+ H.ensure_all_vals = function(arr_subset, arr_all)
1951
+ local vals_registry = {}
1952
+ for _, v in ipairs(arr_subset) do
1953
+ vals_registry[v] = true
1954
+ end
1955
+
1956
+ for _, v in ipairs(arr_all) do
1957
+ if not vals_registry[v] then
1958
+ table.insert(arr_subset, v)
1959
+ vals_registry[v] = true
1960
+ end
1961
+ end
1962
+
1963
+ return arr_subset
1964
+ end
1965
+
1966
+ H.inject_hooks_once = function(cases, hooks_once)
1967
+ -- NOTE: this heavily relies on the equivalence of "have same object id" and
1968
+ -- "are same hooks"
1969
+ local already_injected, n = {}, #cases
1970
+
1971
+ -- Inject 'pre' hooks moving forwards
1972
+ for i = 1, n do
1973
+ local case, hooks = cases[i], hooks_once[i].pre
1974
+ case.hooks.pre_source = vim.tbl_map(function() return 'case' end, case.hooks.pre)
1975
+ local target_tbl_id = 1
1976
+ for j = 1, #hooks do
1977
+ local h = hooks[j]
1978
+ if not already_injected[h] then
1979
+ table.insert(case.hooks.pre, target_tbl_id, h)
1980
+ table.insert(case.hooks.pre_source, target_tbl_id, 'once')
1981
+ target_tbl_id, already_injected[h] = target_tbl_id + 1, true
1982
+ end
1983
+ end
1984
+ end
1985
+
1986
+ -- Inject 'post' hooks moving backwards
1987
+ for i = n, 1, -1 do
1988
+ local case, hooks = cases[i], hooks_once[i].post
1989
+ case.hooks.post_source = vim.tbl_map(function() return 'case' end, case.hooks.post)
1990
+ local target_tbl_id = #case.hooks.post + 1
1991
+ for j = #hooks, 1, -1 do
1992
+ local h = hooks[j]
1993
+ if not already_injected[h] then
1994
+ table.insert(case.hooks.post, target_tbl_id, h)
1995
+ table.insert(case.hooks.post_source, target_tbl_id, 'once')
1996
+ already_injected[h] = true
1997
+ end
1998
+ end
1999
+ end
2000
+
2001
+ return cases
2002
+ end
2003
+
2004
+ H.new_testcase = function(template, test)
2005
+ template.test = test
2006
+ return template
2007
+ end
2008
+
2009
+ H.extend_template = function(template, layer)
2010
+ local res = vim.deepcopy(template)
2011
+
2012
+ vim.list_extend(res.args, layer.args)
2013
+ table.insert(res.desc, layer.desc)
2014
+ res.hooks = H.extend_hooks(res.hooks, layer.hooks, false)
2015
+ res.data = vim.tbl_deep_extend('force', res.data, layer.data)
2016
+ res.n_retry = layer.n_retry or res.n_retry or 1
2017
+
2018
+ return res
2019
+ end
2020
+
2021
+ H.extend_hooks = function(hooks, layer, do_deepcopy)
2022
+ local res = hooks
2023
+ if do_deepcopy == nil or do_deepcopy then res = vim.deepcopy(hooks) end
2024
+
2025
+ -- Closer (in terms of nesting) hooks should be closer to test callable
2026
+ if vim.is_callable(layer.pre) then table.insert(res.pre, layer.pre) end
2027
+ if vim.is_callable(layer.post) then table.insert(res.post, 1, layer.post) end
2028
+
2029
+ return res
2030
+ end
2031
+
2032
+ H.case_to_stringid = function(case)
2033
+ local desc = table.concat(case.desc, ' | ')
2034
+ if #case.args == 0 then return desc end
2035
+ local args = vim.inspect(case.args, { newline = '', indent = '' })
2036
+ return ('%s + args %s'):format(desc, args)
2037
+ end
2038
+
2039
+ H.case_final_state = function(case)
2040
+ local pass_fail = #case.exec.fails == 0 and 'Pass' or 'Fail'
2041
+ local with_notes = #case.exec.notes == 0 and '' or ' with notes'
2042
+ return string.format('%s%s', pass_fail, with_notes)
2043
+ end
2044
+
2045
+ -- Dynamic overview reporter --------------------------------------------------
2046
+ H.overview_reporter = {}
2047
+
2048
+ H.overview_reporter.compute_groups = function(cases, group_depth)
2049
+ local default_symbol = H.reporter_symbols[nil]
2050
+ return vim.tbl_map(function(c)
2051
+ local desc_trunc = vim.list_slice(c.desc, 1, group_depth)
2052
+ local name = table.concat(desc_trunc, ' | ')
2053
+ return { name = name, symbol = default_symbol }
2054
+ end, cases)
2055
+ end
2056
+
2057
+ H.overview_reporter.start_lines = function(cases, groups)
2058
+ local unique_names = {}
2059
+ for _, g in ipairs(groups) do
2060
+ unique_names[g.name] = true
2061
+ end
2062
+ local n_groups = #vim.tbl_keys(unique_names)
2063
+
2064
+ return {
2065
+ string.format('%s %s', H.add_style('Total number of cases:', 'emphasis'), #cases),
2066
+ string.format('%s %s', H.add_style('Total number of groups:', 'emphasis'), n_groups),
2067
+ '',
2068
+ }
2069
+ end
2070
+
2071
+ H.overview_reporter.finish_lines = function(cases)
2072
+ -- Gather fails and notes (colored based on case fail/pass)
2073
+ local fails, notes = {}, {}
2074
+ local n_fails, n_notes = 0, 0
2075
+ for _, c in ipairs(cases) do
2076
+ local stringid = H.case_to_stringid(c)
2077
+ local exec = c.exec == nil and { fails = {}, notes = {} } or c.exec
2078
+
2079
+ if #exec.fails > 0 then
2080
+ table.insert(fails, '')
2081
+ local fail_prefix = string.format('%s in %s: ', H.add_style('FAIL', 'fail'), stringid)
2082
+ vim.list_extend(fails, H.add_prefix(exec.fails, fail_prefix))
2083
+ n_fails = n_fails + #exec.fails
2084
+ end
2085
+
2086
+ if #exec.notes > 0 then
2087
+ table.insert(notes, '')
2088
+ local note_color = #exec.fails > 0 and 'fail' or 'pass'
2089
+ local note_prefix = string.format('%s in %s: ', H.add_style('NOTE', note_color), stringid)
2090
+ vim.list_extend(notes, H.add_prefix(exec.notes, note_prefix))
2091
+ n_notes = n_notes + #exec.notes
2092
+ end
2093
+ end
2094
+
2095
+ -- Show all fails first, then all notes
2096
+ local header = string.format('Fails (%s) and Notes (%s)', n_fails, n_notes)
2097
+ local res = { H.add_style(header, 'emphasis') }
2098
+ vim.list_extend(res, fails)
2099
+ vim.list_extend(res, notes)
2100
+
2101
+ return vim.split(table.concat(res, '\n'), '\n')
2102
+ end
2103
+
2104
+ -- Buffer reporter utilities --------------------------------------------------
2105
+ H.buffer_reporter = { ns_id = vim.api.nvim_create_namespace('MiniTestBuffer'), n_buffer = 0 }
2106
+
2107
+ H.buffer_reporter.setup_buf_and_win = function(window_opts)
2108
+ local buf_id = vim.api.nvim_create_buf(true, true)
2109
+ H.set_buf_name(buf_id, 'buffer-reporter')
2110
+
2111
+ local win_id
2112
+ if vim.is_callable(window_opts) then
2113
+ win_id = window_opts()
2114
+ elseif type(window_opts) == 'table' then
2115
+ -- Ensure proper title
2116
+ if type(window_opts.title) == 'string' then
2117
+ window_opts.title = H.fit_to_width(window_opts.title, window_opts.width)
2118
+ end
2119
+ win_id = vim.api.nvim_open_win(buf_id, true, window_opts)
2120
+ end
2121
+ win_id = win_id or vim.api.nvim_get_current_win()
2122
+ vim.api.nvim_win_set_buf(win_id, buf_id)
2123
+
2124
+ H.buffer_reporter.set_options(buf_id, win_id)
2125
+ H.buffer_reporter.set_mappings(buf_id)
2126
+
2127
+ return buf_id, win_id
2128
+ end
2129
+
2130
+ H.buffer_reporter.default_window_opts = function()
2131
+ return {
2132
+ relative = 'editor',
2133
+ width = math.floor(0.618 * vim.o.columns),
2134
+ height = math.floor(0.618 * vim.o.lines),
2135
+ row = math.floor(0.191 * vim.o.lines),
2136
+ col = math.floor(0.191 * vim.o.columns),
2137
+ border = (vim.fn.exists('+winborder') == 0 or vim.o.winborder == '') and 'single' or nil,
2138
+ title = ' Test results ',
2139
+ }
2140
+ end
2141
+
2142
+ H.buffer_reporter.set_options = function(buf_id, win_id)
2143
+ -- Set unique name
2144
+ local n_buffer = H.buffer_reporter.n_buffer + 1
2145
+ local suffix = n_buffer == 1 and '' or (' ' .. n_buffer)
2146
+ H.buffer_reporter.n_buffer = n_buffer
2147
+
2148
+ vim.cmd('silent! set filetype=minitest')
2149
+
2150
+ --stylua: ignore start
2151
+ -- Set options for "temporary" buffer
2152
+ local buf_options = {
2153
+ bufhidden = 'wipe', buflisted = false, buftype = 'nofile', modeline = false, swapfile = false,
2154
+ }
2155
+ for name, value in pairs(buf_options) do
2156
+ vim.bo[buf_id][name] = value
2157
+ end
2158
+
2159
+ -- Set options for "clean" window
2160
+ local win_options = {
2161
+ colorcolumn = '', fillchars = 'eob: ', foldcolumn = '0', foldlevel = 999,
2162
+ number = false, relativenumber = false, spell = false, signcolumn = 'no',
2163
+ wrap = true,
2164
+ }
2165
+ for name, value in pairs(win_options) do
2166
+ vim.wo[win_id][name] = value
2167
+ end
2168
+ --stylua: ignore end
2169
+ end
2170
+
2171
+ H.buffer_reporter.set_mappings = function(buf_id)
2172
+ local rhs = [[<Cmd>lua if MiniTest.is_executing() then MiniTest.stop() else vim.cmd('close') end<CR>]]
2173
+ vim.keymap.set('n', '<Esc>', rhs, { buffer = buf_id, desc = 'Stop execution or close window' })
2174
+ vim.keymap.set('n', 'q', rhs, { buffer = buf_id, desc = 'Stop execution or close window' })
2175
+ end
2176
+
2177
+ H.buffer_reporter.set_lines = function(buf_id, lines, start, finish)
2178
+ local ns_id = H.buffer_reporter.ns_id
2179
+
2180
+ local n_lines = vim.api.nvim_buf_line_count(buf_id)
2181
+ start = (start < 0) and (n_lines + 1 + start) or start
2182
+ finish = (finish < 0) and (n_lines + 1 + finish) or finish
2183
+
2184
+ -- Remove ANSI codes while tracking appropriate highlight data
2185
+ local new_lines, hl_ranges = {}, {}
2186
+ for i, l in ipairs(lines) do
2187
+ local n_removed = 0
2188
+ local new_l = l:gsub('\n', '\\n'):gsub('()(\27%[.-m)(.-)\27%[0m', function(...)
2189
+ local dots = { ... }
2190
+ local left = dots[1] - n_removed
2191
+ table.insert(
2192
+ hl_ranges,
2193
+ { hl = H.hl_groups[dots[2]], line = start + i - 1, left = left - 1, right = left + dots[3]:len() - 1 }
2194
+ )
2195
+
2196
+ -- Here `4` is `string.len('\27[0m')`
2197
+ n_removed = n_removed + dots[2]:len() + 4
2198
+ return dots[3]
2199
+ end)
2200
+ table.insert(new_lines, new_l)
2201
+ end
2202
+
2203
+ -- Clear highlighting on updated lines. Crucial because otherwise it will
2204
+ -- lead to A LOT of memory consumption.
2205
+ vim.api.nvim_buf_clear_namespace(buf_id, H.buffer_reporter.ns_id, start, finish)
2206
+
2207
+ -- Set lines
2208
+ vim.api.nvim_buf_set_lines(buf_id, start, finish, true, new_lines)
2209
+
2210
+ -- Add highlight
2211
+ for _, hl_data in ipairs(hl_ranges) do
2212
+ H.highlight_range(buf_id, ns_id, hl_data.hl, { hl_data.line, hl_data.left }, { hl_data.line, hl_data.right })
2213
+ end
2214
+ end
2215
+
2216
+ H.buffer_reporter.update_step_lines = function(case_num, cases, groups)
2217
+ local cur_case = cases[case_num]
2218
+ local cur_group = groups[case_num].name
2219
+
2220
+ -- Don't show anything before empty group name (when `group_depth` is 0)
2221
+ local cur_group_suffix = cur_group == '' and '' or ': '
2222
+ local cur_group_symbols = vim.tbl_map(
2223
+ function(g) return g.symbol end,
2224
+ vim.tbl_filter(function(g) return g.name == cur_group end, groups)
2225
+ )
2226
+
2227
+ return {
2228
+ -- Group overview
2229
+ string.format('%s%s%s', cur_group, cur_group_suffix, table.concat(cur_group_symbols)),
2230
+ '',
2231
+ H.add_style('Current case state', 'emphasis'),
2232
+ string.format('%s: %s', H.case_to_stringid(cur_case), cur_case.exec.state),
2233
+ }
2234
+ end
2235
+
2236
+ H.buffer_reporter.update_step_n_replace = function(latest_group_name, cur_group_name)
2237
+ -- By default rewrite latest group symbol overview
2238
+ local res = 4
2239
+
2240
+ if latest_group_name == nil then
2241
+ -- Nothing to rewrite on first ever call
2242
+ res = 0
2243
+ elseif latest_group_name ~= cur_group_name then
2244
+ -- Write just under latest group symbol overview
2245
+ res = 3
2246
+ end
2247
+
2248
+ return res
2249
+ end
2250
+
2251
+ -- Predicates -----------------------------------------------------------------
2252
+ H.is_instance = function(x, class)
2253
+ local metatbl = getmetatable(x)
2254
+ return type(metatbl) == 'table' and metatbl.class == class
2255
+ end
2256
+
2257
+ H.has_fails = function(cases)
2258
+ for _, c in ipairs(cases) do
2259
+ local n_fails = c.exec == nil and 0 or #c.exec.fails
2260
+ if n_fails > 0 then return true end
2261
+ end
2262
+ return false
2263
+ end
2264
+
2265
+ -- Expectation utilities ------------------------------------------------------
2266
+ H.normalize_reason = function(reason, fallback, ...)
2267
+ if vim.is_callable(reason) then reason = reason(...) end
2268
+ if type(reason) ~= 'string' then reason = fallback end
2269
+ return reason
2270
+ end
2271
+
2272
+ H.error_with_emphasis = function(msg, context)
2273
+ local lines = { '', H.add_style(msg, 'emphasis'), context }
2274
+ error(table.concat(lines, '\n'), 0)
2275
+ end
2276
+
2277
+ H.traceback = function()
2278
+ local level, res = 1, {}
2279
+ local info = debug.getinfo(level, 'Snl')
2280
+ local this_short_src = info.short_src
2281
+ while info ~= nil do
2282
+ local is_from_file = info.source:sub(1, 1) == '@'
2283
+ local is_from_this_file = info.short_src == this_short_src
2284
+ if is_from_file and not is_from_this_file then
2285
+ local line = string.format([[ %s:%s]], info.short_src, info.currentline)
2286
+ table.insert(res, line)
2287
+ end
2288
+ level = level + 1
2289
+ info = debug.getinfo(level, 'Snl')
2290
+ end
2291
+
2292
+ return res
2293
+ end
2294
+
2295
+ H.compute_no_equality_cause = function(left, right)
2296
+ if type(left) ~= type(right) then return 'different types' end
2297
+
2298
+ if type(left) == 'string' then
2299
+ if vim.fn.strchars(left) ~= vim.fn.strchars(right) then return 'different string length' end
2300
+ for i = 1, vim.fn.strchars(left) do
2301
+ local lchar, rchar = vim.fn.strcharpart(left, i - 1, 1), vim.fn.strcharpart(right, i - 1, 1)
2302
+ if lchar ~= rchar then
2303
+ return string.format('different character at position %s', i)
2304
+ .. string.format(', left = %s, right = %s', vim.inspect(lchar), vim.inspect(rchar))
2305
+ end
2306
+ end
2307
+ end
2308
+
2309
+ if type(left) ~= 'table' then return 'different values' end
2310
+
2311
+ -- Find key branch with different values
2312
+ local traverse
2313
+ traverse = function(branch, diff, a, b)
2314
+ if not (type(a) == 'table' and type(b) == 'table') then return end
2315
+ local keys = vim.tbl_keys(a)
2316
+ table.sort(keys, function(x, y) return tostring(x) < tostring(y) end)
2317
+ for _, k in ipairs(keys) do
2318
+ if not vim.deep_equal(a[k], b[k]) then
2319
+ table.insert(branch, k)
2320
+ diff.left, diff.right = a[k], b[k]
2321
+ return traverse(branch, diff, a[k], b[k])
2322
+ end
2323
+ end
2324
+ end
2325
+
2326
+ -- - Traverse with both table orders to find the longest possible branch.
2327
+ -- This also covers the "present in one but not the other" cases.
2328
+ local left_branch, left_diff = {}, {}
2329
+ traverse(left_branch, left_diff, left, right)
2330
+ local right_branch, right_diff = {}, {}
2331
+ traverse(right_branch, right_diff, right, left)
2332
+
2333
+ local branch, ldiff, rdiff = left_branch, left_diff.left, left_diff.right
2334
+ if #left_branch < #right_branch then
2335
+ branch, ldiff, rdiff = right_branch, right_diff.right, right_diff.left
2336
+ end
2337
+ ldiff = vim.inspect(ldiff, { newline = ' ', indent = '' })
2338
+ rdiff = vim.inspect(rdiff, { newline = ' ', indent = '' })
2339
+ local key_branch = table.concat(vim.tbl_map(vim.inspect, branch), '->')
2340
+ return string.format('different values at key %s%s', #branch > 1 and 'branch ' or '', key_branch)
2341
+ .. string.format(', left = %s, right = %s', ldiff, rdiff)
2342
+ end
2343
+
2344
+ -- Screenshots ----------------------------------------------------------------
2345
+ H.screenshot_new = function(t)
2346
+ local process_screen = function(arr_2d)
2347
+ local n_lines, n_cols = #arr_2d, #arr_2d[1]
2348
+
2349
+ -- Prepend lines with line number of the form `01|`
2350
+ local n_digits = math.floor(math.log10(n_lines)) + 1
2351
+ local format = string.format('%%0%dd|%%s', n_digits)
2352
+ local lines = {}
2353
+ for i = 1, n_lines do
2354
+ table.insert(lines, string.format(format, i, table.concat(arr_2d[i])))
2355
+ end
2356
+
2357
+ -- Make ruler
2358
+ local prefix = string.rep('-', n_digits) .. '|'
2359
+ local ruler = prefix .. ('---------|'):rep(math.ceil(0.1 * n_cols)):sub(1, n_cols)
2360
+
2361
+ return string.format('%s\n%s', ruler, table.concat(lines, '\n'))
2362
+ end
2363
+
2364
+ return setmetatable(t, {
2365
+ __tostring = function(x) return string.format('%s\n\n%s', process_screen(x.text), process_screen(x.attr)) end,
2366
+ })
2367
+ end
2368
+
2369
+ H.screenshot_encode_attr = function(attr)
2370
+ local attr_codes, res = {}, {}
2371
+ -- Use 48 so that codes start from `'0'`
2372
+ local cur_code_id = 48
2373
+ for _, l in ipairs(attr) do
2374
+ local res_line = {}
2375
+ for _, s in ipairs(l) do
2376
+ -- Assign character codes to numerical attributes in order of their
2377
+ -- appearance on the screen. This leads to be a more reliable way of
2378
+ -- comparing two different screenshots (at cost of bigger effect when
2379
+ -- screenshot changes slightly).
2380
+ if not attr_codes[s] then
2381
+ attr_codes[s] = string.char(cur_code_id)
2382
+ -- Cycle through 33...126
2383
+ cur_code_id = math.fmod(cur_code_id + 1 - 33, 94) + 33
2384
+ end
2385
+ table.insert(res_line, attr_codes[s])
2386
+ end
2387
+ table.insert(res, res_line)
2388
+ end
2389
+
2390
+ return res
2391
+ end
2392
+
2393
+ H.screenshot_compare_part = function(part, ref, obs, opts)
2394
+ local ignore_part = opts['ignore_' .. part]
2395
+ if ignore_part == true then return true, '' end
2396
+
2397
+ local compare = function(x, y, desc)
2398
+ if x == y then return true, '' end
2399
+ return false, ('Cause: different %s, reference = %s, observed = %s'):format(desc, vim.inspect(x), vim.inspect(y))
2400
+ end
2401
+
2402
+ local ok, cause
2403
+ ok, cause = compare(#ref[part], #obs[part], 'number of `' .. part .. '` lines')
2404
+ if not ok then return ok, cause end
2405
+
2406
+ local lines_to_check = {}
2407
+ for i = 1, #ref[part] do
2408
+ local is_ignore_part = type(ignore_part) == 'table' and vim.tbl_contains(ignore_part, i)
2409
+ if not is_ignore_part then table.insert(lines_to_check, i) end
2410
+ end
2411
+
2412
+ for _, i in ipairs(lines_to_check) do
2413
+ ok, cause = compare(#ref[part][i], #obs[part][i], 'number of columns in `' .. part .. '` line ' .. i)
2414
+ if not ok then return ok, cause end
2415
+
2416
+ for j = 1, #ref[part][i] do
2417
+ ok, cause = compare(ref[part][i][j], obs[part][i][j], '`' .. part .. '` cell at line ' .. i .. ' column ' .. j)
2418
+ if not ok then return ok, cause end
2419
+ end
2420
+ end
2421
+
2422
+ return true, ''
2423
+ end
2424
+
2425
+ H.screenshot_write = function(screenshot, path) vim.fn.writefile(vim.split(tostring(screenshot), '\n'), path) end
2426
+
2427
+ H.screenshot_read = function(path)
2428
+ -- General structure of screenshot with `n` lines:
2429
+ -- 1: ruler-separator
2430
+ -- 2, n+1: `prefix`|`text`
2431
+ -- n+2: empty line
2432
+ -- n+3: ruler-separator
2433
+ -- n+4, 2n+3: `prefix`|`attr`
2434
+ local lines = vim.fn.readfile(path)
2435
+ local n = 0.5 * (#lines - 3)
2436
+ local text_lines, attr_lines = vim.list_slice(lines, 2, n + 1), vim.list_slice(lines, n + 4, 2 * n + 3)
2437
+
2438
+ local f = function(x) return H.string_to_screenchars(x:gsub('^%d+|', '')) end
2439
+ return H.screenshot_new({ text = vim.tbl_map(f, text_lines), attr = vim.tbl_map(f, attr_lines) })
2440
+ end
2441
+
2442
+ -- Utilities ------------------------------------------------------------------
2443
+ H.error = function(msg) error('(mini.test) ' .. msg, 0) end
2444
+
2445
+ H.check_type = function(name, val, ref, allow_nil)
2446
+ if type(val) == ref or (ref == 'callable' and vim.is_callable(val)) or (allow_nil and val == nil) then return end
2447
+ H.error(string.format('`%s` should be %s, not %s', name, ref, type(val)))
2448
+ end
2449
+
2450
+ H.set_buf_name = function(buf_id, name) vim.api.nvim_buf_set_name(buf_id, 'minitest://' .. buf_id .. '/' .. name) end
2451
+
2452
+ H.echo = function(msg, is_important)
2453
+ if H.get_config().silent then return end
2454
+
2455
+ -- Construct message chunks
2456
+ msg = type(msg) == 'string' and { { msg } } or msg
2457
+ table.insert(msg, 1, { '(mini.test) ', 'WarningMsg' })
2458
+
2459
+ -- Echo. Force redraw to ensure that it is effective (`:h echo-redraw`)
2460
+ vim.cmd([[echo '' | redraw]])
2461
+ vim.api.nvim_echo(msg, is_important, {})
2462
+ end
2463
+
2464
+ H.message = function(msg) H.echo(msg, true) end
2465
+
2466
+ H.wrap_callable = function(f)
2467
+ if not vim.is_callable(f) then return end
2468
+ return function(...) return f(...) end
2469
+ end
2470
+
2471
+ H.exec_callable = function(f, ...)
2472
+ if not vim.is_callable(f) then return end
2473
+ return f(...)
2474
+ end
2475
+
2476
+ H.add_prefix = function(tbl, prefix)
2477
+ return vim.tbl_map(function(x)
2478
+ local p = prefix
2479
+ -- Do not create trailing whitespace
2480
+ if x:sub(1, 1) == '\n' then p = p:gsub('%s*$', '') end
2481
+ return ('%s%s'):format(p, x)
2482
+ end, tbl)
2483
+ end
2484
+
2485
+ H.add_style = function(x, ansi_code) return string.format('%s%s%s', H.ansi_codes[ansi_code], x, H.ansi_codes.reset) end
2486
+
2487
+ H.fit_to_width = function(text, width)
2488
+ local t_width = vim.fn.strchars(text)
2489
+ return t_width <= width and text or ('…' .. vim.fn.strcharpart(text, t_width - width + 1, width - 1))
2490
+ end
2491
+
2492
+ H.string_to_screenchars = function(s)
2493
+ -- Can't use `vim.split(s, '')` because of multibyte characters
2494
+ local res = {}
2495
+ for i = 1, vim.fn.strchars(s) do
2496
+ local ch = vim.fn.strcharpart(s, i - 1, 1)
2497
+ table.insert(res, ch)
2498
+ -- Not single-width characters are read as a single char, but result into
2499
+ -- `{ ch, '', ... }` when computing observed screenshot (as this is how
2500
+ -- `vim.fn.screenstring()` works)
2501
+ for _ = 1, vim.fn.strdisplaywidth(ch) - 1 do
2502
+ table.insert(res, '')
2503
+ end
2504
+ end
2505
+ return res
2506
+ end
2507
+
2508
+ -- TODO: Remove after compatibility with Neovim=0.9 is dropped
2509
+ H.tbl_flatten = vim.fn.has('nvim-0.10') == 1 and function(x) return vim.iter(x):flatten(math.huge):totable() end
2510
+ or vim.tbl_flatten
2511
+
2512
+ -- TODO: Remove after compatibility with Neovim=0.10 is dropped
2513
+ H.highlight_range = function(...) vim.hl.range(...) end
2514
+ if vim.fn.has('nvim-0.11') == 0 then H.highlight_range = function(...) vim.highlight.range(...) end end
2515
+
2516
+ return MiniTest