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.
- package/.conda/retrovim.yaml +259 -0
- package/.conda/terax.icns +0 -0
- package/.conda/terax.ico +0 -0
- package/.conda/terax.png +0 -0
- package/.github/workflows/release.yml +95 -0
- package/.gitmodules +3 -0
- package/.npm/kanata_touchcursor.js +28 -0
- package/.npm/nvim.js +26 -0
- package/.npm/yazi.js +26 -0
- package/.npm/zsh.js +17 -0
- package/.pip/kanata_touchcursor/__init__.py +28 -0
- package/.pip/nvim/__init__.py +31 -0
- package/.pip/yazi/__init__.py +31 -0
- package/.pip/zsh/__init__.py +22 -0
- package/CHANGELOG.md +26 -0
- package/LICENSE +674 -0
- package/README.md +336 -0
- package/kanata/touchcursor.kbd +164 -0
- package/nvim/init.lua +813 -0
- package/nvim/plugins/mini.nvim/.github/DISCUSSION_TEMPLATE/q-a.yml +72 -0
- package/nvim/plugins/mini.nvim/.github/ISSUE_TEMPLATE/bug-report.yml +136 -0
- package/nvim/plugins/mini.nvim/.github/ISSUE_TEMPLATE/config.yml +5 -0
- package/nvim/plugins/mini.nvim/.github/ISSUE_TEMPLATE/feature-request.yml +75 -0
- package/nvim/plugins/mini.nvim/.github/PULL_REQUEST_TEMPLATE.md +2 -0
- package/nvim/plugins/mini.nvim/.github/workflows/quality-control.yml +150 -0
- package/nvim/plugins/mini.nvim/.pre-commit-config.yaml +20 -0
- package/nvim/plugins/mini.nvim/.stylua.toml +7 -0
- package/nvim/plugins/mini.nvim/.styluaignore +1 -0
- package/nvim/plugins/mini.nvim/CHANGELOG.md +1894 -0
- package/nvim/plugins/mini.nvim/CODE_OF_CONDUCT.md +132 -0
- package/nvim/plugins/mini.nvim/CONTRIBUTING.md +331 -0
- package/nvim/plugins/mini.nvim/LICENSE +21 -0
- package/nvim/plugins/mini.nvim/MAINTAINING.md +330 -0
- package/nvim/plugins/mini.nvim/Makefile +51 -0
- package/nvim/plugins/mini.nvim/README.md +261 -0
- package/nvim/plugins/mini.nvim/TESTING.md +1011 -0
- package/nvim/plugins/mini.nvim/benchmarks/starter/README.md +35 -0
- package/nvim/plugins/mini.nvim/benchmarks/starter/benchmark.sh +56 -0
- package/nvim/plugins/mini.nvim/benchmarks/starter/init-files/init_dashboard-alpha.lua +21 -0
- package/nvim/plugins/mini.nvim/benchmarks/starter/init-files/init_dashboard-original.lua +17 -0
- package/nvim/plugins/mini.nvim/benchmarks/starter/init-files/init_dashboard-starter.lua +18 -0
- package/nvim/plugins/mini.nvim/benchmarks/starter/init-files/init_empty.lua +4 -0
- package/nvim/plugins/mini.nvim/benchmarks/starter/init-files/init_starter-default.lua +7 -0
- package/nvim/plugins/mini.nvim/benchmarks/starter/init-files/init_startify-alpha.lua +10 -0
- package/nvim/plugins/mini.nvim/benchmarks/starter/init-files/init_startify-original.lua +7 -0
- package/nvim/plugins/mini.nvim/benchmarks/starter/init-files/init_startify-starter.lua +20 -0
- package/nvim/plugins/mini.nvim/benchmarks/starter/install.sh +10 -0
- package/nvim/plugins/mini.nvim/benchmarks/starter/make_summary.py +68 -0
- package/nvim/plugins/mini.nvim/benchmarks/starter/startup-summary.md +10 -0
- package/nvim/plugins/mini.nvim/colors/miniautumn.lua +55 -0
- package/nvim/plugins/mini.nvim/colors/minicyan.lua +95 -0
- package/nvim/plugins/mini.nvim/colors/minischeme.lua +95 -0
- package/nvim/plugins/mini.nvim/colors/minispring.lua +53 -0
- package/nvim/plugins/mini.nvim/colors/minisummer.lua +49 -0
- package/nvim/plugins/mini.nvim/colors/miniwinter.lua +51 -0
- package/nvim/plugins/mini.nvim/colors/randomhue.lua +16 -0
- package/nvim/plugins/mini.nvim/doc/mini-ai.txt +889 -0
- package/nvim/plugins/mini.nvim/doc/mini-align.txt +923 -0
- package/nvim/plugins/mini.nvim/doc/mini-animate.txt +937 -0
- package/nvim/plugins/mini.nvim/doc/mini-base16.txt +285 -0
- package/nvim/plugins/mini.nvim/doc/mini-basics.txt +359 -0
- package/nvim/plugins/mini.nvim/doc/mini-bracketed.txt +622 -0
- package/nvim/plugins/mini.nvim/doc/mini-bufremove.txt +123 -0
- package/nvim/plugins/mini.nvim/doc/mini-clue.txt +760 -0
- package/nvim/plugins/mini.nvim/doc/mini-cmdline.txt +392 -0
- package/nvim/plugins/mini.nvim/doc/mini-colors.txt +880 -0
- package/nvim/plugins/mini.nvim/doc/mini-comment.txt +230 -0
- package/nvim/plugins/mini.nvim/doc/mini-completion.txt +477 -0
- package/nvim/plugins/mini.nvim/doc/mini-cursorword.txt +105 -0
- package/nvim/plugins/mini.nvim/doc/mini-deps.txt +590 -0
- package/nvim/plugins/mini.nvim/doc/mini-diff.txt +661 -0
- package/nvim/plugins/mini.nvim/doc/mini-doc.txt +434 -0
- package/nvim/plugins/mini.nvim/doc/mini-extra.txt +856 -0
- package/nvim/plugins/mini.nvim/doc/mini-files.txt +1018 -0
- package/nvim/plugins/mini.nvim/doc/mini-fuzzy.txt +160 -0
- package/nvim/plugins/mini.nvim/doc/mini-git.txt +425 -0
- package/nvim/plugins/mini.nvim/doc/mini-hipatterns.txt +489 -0
- package/nvim/plugins/mini.nvim/doc/mini-hues.txt +420 -0
- package/nvim/plugins/mini.nvim/doc/mini-icons.txt +508 -0
- package/nvim/plugins/mini.nvim/doc/mini-indentscope.txt +485 -0
- package/nvim/plugins/mini.nvim/doc/mini-input.txt +932 -0
- package/nvim/plugins/mini.nvim/doc/mini-jump.txt +202 -0
- package/nvim/plugins/mini.nvim/doc/mini-jump2d.txt +525 -0
- package/nvim/plugins/mini.nvim/doc/mini-keymap.txt +449 -0
- package/nvim/plugins/mini.nvim/doc/mini-map.txt +717 -0
- package/nvim/plugins/mini.nvim/doc/mini-misc.txt +431 -0
- package/nvim/plugins/mini.nvim/doc/mini-move.txt +181 -0
- package/nvim/plugins/mini.nvim/doc/mini-notify.txt +434 -0
- package/nvim/plugins/mini.nvim/doc/mini-nvim.txt +431 -0
- package/nvim/plugins/mini.nvim/doc/mini-operators.txt +472 -0
- package/nvim/plugins/mini.nvim/doc/mini-pairs.txt +346 -0
- package/nvim/plugins/mini.nvim/doc/mini-pick.txt +1535 -0
- package/nvim/plugins/mini.nvim/doc/mini-sessions.txt +239 -0
- package/nvim/plugins/mini.nvim/doc/mini-snippets.txt +1291 -0
- package/nvim/plugins/mini.nvim/doc/mini-splitjoin.txt +536 -0
- package/nvim/plugins/mini.nvim/doc/mini-starter.txt +618 -0
- package/nvim/plugins/mini.nvim/doc/mini-statusline.txt +361 -0
- package/nvim/plugins/mini.nvim/doc/mini-surround.txt +863 -0
- package/nvim/plugins/mini.nvim/doc/mini-tabline.txt +151 -0
- package/nvim/plugins/mini.nvim/doc/mini-test.txt +966 -0
- package/nvim/plugins/mini.nvim/doc/mini-trailspace.txt +91 -0
- package/nvim/plugins/mini.nvim/doc/mini-visits.txt +937 -0
- package/nvim/plugins/mini.nvim/logo.png +0 -0
- package/nvim/plugins/mini.nvim/lua/mini/ai.lua +2244 -0
- package/nvim/plugins/mini.nvim/lua/mini/align.lua +2076 -0
- package/nvim/plugins/mini.nvim/lua/mini/animate.lua +2127 -0
- package/nvim/plugins/mini.nvim/lua/mini/base16.lua +1752 -0
- package/nvim/plugins/mini.nvim/lua/mini/basics.lua +772 -0
- package/nvim/plugins/mini.nvim/lua/mini/bracketed.lua +2013 -0
- package/nvim/plugins/mini.nvim/lua/mini/bufremove.lua +283 -0
- package/nvim/plugins/mini.nvim/lua/mini/clue.lua +2096 -0
- package/nvim/plugins/mini.nvim/lua/mini/cmdline.lua +1182 -0
- package/nvim/plugins/mini.nvim/lua/mini/colors.lua +2483 -0
- package/nvim/plugins/mini.nvim/lua/mini/comment.lua +586 -0
- package/nvim/plugins/mini.nvim/lua/mini/completion.lua +2094 -0
- package/nvim/plugins/mini.nvim/lua/mini/cursorword.lua +311 -0
- package/nvim/plugins/mini.nvim/lua/mini/deps.lua +1660 -0
- package/nvim/plugins/mini.nvim/lua/mini/diff.lua +1938 -0
- package/nvim/plugins/mini.nvim/lua/mini/doc.lua +1371 -0
- package/nvim/plugins/mini.nvim/lua/mini/extra.lua +2290 -0
- package/nvim/plugins/mini.nvim/lua/mini/files.lua +3145 -0
- package/nvim/plugins/mini.nvim/lua/mini/fuzzy.lua +356 -0
- package/nvim/plugins/mini.nvim/lua/mini/git.lua +1737 -0
- package/nvim/plugins/mini.nvim/lua/mini/hipatterns.lua +1042 -0
- package/nvim/plugins/mini.nvim/lua/mini/hues.lua +2088 -0
- package/nvim/plugins/mini.nvim/lua/mini/icons.lua +2254 -0
- package/nvim/plugins/mini.nvim/lua/mini/indentscope.lua +1158 -0
- package/nvim/plugins/mini.nvim/lua/mini/init.lua +427 -0
- package/nvim/plugins/mini.nvim/lua/mini/input.lua +2183 -0
- package/nvim/plugins/mini.nvim/lua/mini/jump.lua +609 -0
- package/nvim/plugins/mini.nvim/lua/mini/jump2d.lua +1244 -0
- package/nvim/plugins/mini.nvim/lua/mini/keymap.lua +880 -0
- package/nvim/plugins/mini.nvim/lua/mini/map.lua +1709 -0
- package/nvim/plugins/mini.nvim/lua/mini/misc.lua +969 -0
- package/nvim/plugins/mini.nvim/lua/mini/move.lua +509 -0
- package/nvim/plugins/mini.nvim/lua/mini/notify.lua +938 -0
- package/nvim/plugins/mini.nvim/lua/mini/operators.lua +1349 -0
- package/nvim/plugins/mini.nvim/lua/mini/pairs.lua +657 -0
- package/nvim/plugins/mini.nvim/lua/mini/pick.lua +3876 -0
- package/nvim/plugins/mini.nvim/lua/mini/sessions.lua +689 -0
- package/nvim/plugins/mini.nvim/lua/mini/snippets.lua +2828 -0
- package/nvim/plugins/mini.nvim/lua/mini/splitjoin.lua +1132 -0
- package/nvim/plugins/mini.nvim/lua/mini/starter.lua +1599 -0
- package/nvim/plugins/mini.nvim/lua/mini/statusline.lua +686 -0
- package/nvim/plugins/mini.nvim/lua/mini/surround.lua +2388 -0
- package/nvim/plugins/mini.nvim/lua/mini/tabline.lua +564 -0
- package/nvim/plugins/mini.nvim/lua/mini/test.lua +2516 -0
- package/nvim/plugins/mini.nvim/lua/mini/trailspace.lua +215 -0
- package/nvim/plugins/mini.nvim/lua/mini/visits.lua +1577 -0
- package/nvim/plugins/mini.nvim/readmes/mini-ai.md +190 -0
- package/nvim/plugins/mini.nvim/readmes/mini-align.md +203 -0
- package/nvim/plugins/mini.nvim/readmes/mini-animate.md +214 -0
- package/nvim/plugins/mini.nvim/readmes/mini-base16.md +198 -0
- package/nvim/plugins/mini.nvim/readmes/mini-basics.md +184 -0
- package/nvim/plugins/mini.nvim/readmes/mini-bracketed.md +193 -0
- package/nvim/plugins/mini.nvim/readmes/mini-bufremove.md +139 -0
- package/nvim/plugins/mini.nvim/readmes/mini-clue.md +260 -0
- package/nvim/plugins/mini.nvim/readmes/mini-cmdline.md +183 -0
- package/nvim/plugins/mini.nvim/readmes/mini-colors.md +169 -0
- package/nvim/plugins/mini.nvim/readmes/mini-comment.md +183 -0
- package/nvim/plugins/mini.nvim/readmes/mini-completion.md +217 -0
- package/nvim/plugins/mini.nvim/readmes/mini-cursorword.md +137 -0
- package/nvim/plugins/mini.nvim/readmes/mini-deps.md +271 -0
- package/nvim/plugins/mini.nvim/readmes/mini-diff.md +289 -0
- package/nvim/plugins/mini.nvim/readmes/mini-doc.md +198 -0
- package/nvim/plugins/mini.nvim/readmes/mini-extra.md +158 -0
- package/nvim/plugins/mini.nvim/readmes/mini-files.md +247 -0
- package/nvim/plugins/mini.nvim/readmes/mini-fuzzy.md +134 -0
- package/nvim/plugins/mini.nvim/readmes/mini-git.md +168 -0
- package/nvim/plugins/mini.nvim/readmes/mini-hipatterns.md +176 -0
- package/nvim/plugins/mini.nvim/readmes/mini-hues.md +285 -0
- package/nvim/plugins/mini.nvim/readmes/mini-icons.md +183 -0
- package/nvim/plugins/mini.nvim/readmes/mini-indentscope.md +191 -0
- package/nvim/plugins/mini.nvim/readmes/mini-input.md +163 -0
- package/nvim/plugins/mini.nvim/readmes/mini-jump.md +161 -0
- package/nvim/plugins/mini.nvim/readmes/mini-jump2d.md +197 -0
- package/nvim/plugins/mini.nvim/readmes/mini-keymap.md +205 -0
- package/nvim/plugins/mini.nvim/readmes/mini-map.md +191 -0
- package/nvim/plugins/mini.nvim/readmes/mini-misc.md +145 -0
- package/nvim/plugins/mini.nvim/readmes/mini-move.md +163 -0
- package/nvim/plugins/mini.nvim/readmes/mini-notify.md +175 -0
- package/nvim/plugins/mini.nvim/readmes/mini-operators.md +193 -0
- package/nvim/plugins/mini.nvim/readmes/mini-pairs.md +158 -0
- package/nvim/plugins/mini.nvim/readmes/mini-pick.md +340 -0
- package/nvim/plugins/mini.nvim/readmes/mini-sessions.md +164 -0
- package/nvim/plugins/mini.nvim/readmes/mini-snippets.md +373 -0
- package/nvim/plugins/mini.nvim/readmes/mini-splitjoin.md +194 -0
- package/nvim/plugins/mini.nvim/readmes/mini-starter.md +176 -0
- package/nvim/plugins/mini.nvim/readmes/mini-statusline.md +155 -0
- package/nvim/plugins/mini.nvim/readmes/mini-surround.md +190 -0
- package/nvim/plugins/mini.nvim/readmes/mini-tabline.md +156 -0
- package/nvim/plugins/mini.nvim/readmes/mini-test.md +175 -0
- package/nvim/plugins/mini.nvim/readmes/mini-trailspace.md +137 -0
- package/nvim/plugins/mini.nvim/readmes/mini-visits.md +243 -0
- package/nvim/plugins/mini.nvim/scripts/dual_log.sh +13 -0
- package/nvim/plugins/mini.nvim/scripts/dual_push.sh +15 -0
- package/nvim/plugins/mini.nvim/scripts/dual_release.sh +102 -0
- package/nvim/plugins/mini.nvim/scripts/dual_sync.sh +115 -0
- package/nvim/plugins/mini.nvim/scripts/init-deps-example.lua +59 -0
- package/nvim/plugins/mini.nvim/scripts/lint-filename-length.sh +16 -0
- package/nvim/plugins/mini.nvim/scripts/lintcommit-ci.sh +18 -0
- package/nvim/plugins/mini.nvim/scripts/lintcommit.lua +393 -0
- package/nvim/plugins/mini.nvim/scripts/minidoc.lua +57 -0
- package/nvim/plugins/mini.nvim/scripts/minimal_init.lua +28 -0
- package/nvim/plugins/mini.nvim/scripts/minitest.lua +4 -0
- package/package.json +16 -0
- package/pyproject.toml +43 -0
- package/yazi/init.lua +12 -0
- package/yazi/keymap.toml +114 -0
- package/yazi/lesskey +5 -0
- package/yazi/plugins/bookmarks.yazi/main.lua +86 -0
- package/yazi/plugins/cd-git-root.yazi/main.lua +31 -0
- package/yazi/plugins/smart-enter.yazi/main.lua +11 -0
- package/yazi/theme.toml +26 -0
- package/yazi/yazi.toml +15 -0
- package/zsh/.zshrc +145 -0
- package/zsh/starship.toml +20 -0
- 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
|