8000 mini.nvim/lua/mini/test.lua at main · stasjok/mini.nvim · GitHub
[go: up one dir, main page]

Skip to content

Latest commit

 

History

History
2927 lines (2565 loc) · 114 KB

test.lua

File metadata and controls

2927 lines (2565 loc) · 114 KB
--- *mini.test* Test Neovim plugins
---
--- MIT License Copyright (c) 2022 Evgeni Chasnovski
--- Features:
--- - Test action is defined as a named callable entry of a table.
---
--- - Helper for creating child Neovim process which is designed to be used in
--- tests (including taking and verifying screenshots). See
--- |MiniTest.new_child_neovim()| and |MiniTest.expect.reference_screenshot()|.
---
--- - Hierarchical organization of tests with custom hooks, parametrization,
--- and user data. See |MiniTest.new_set()|.
---
--- - Emulation of [lunarmodules/busted](https://github.com/lunarmodules/busted)
--- interface (`describe`, `it`, etc.).
---
--- - Predefined small yet usable set of expectations (`assert`-like functions).
--- See |MiniTest.expect|.
---
--- - Customizable definition of what files should be tested.
---
--- - Test case filtering. There are predefined wrappers for testing a file
--- (|MiniTest.run_file()|) and case at a location like current cursor position
--- (|MiniTest.run_at_location()|).
---
--- - Customizable reporter of output results. There are two predefined ones:
--- - |MiniTest.gen_reporter.buffer()| for interactive usage.
--- - |MiniTest.gen_reporter.stdout()| for headless Neovim.
---
--- - Customizable project specific testing script.
---
--- - Works on Unix (Linux, MacOS, etc.) and Windows.
---
--- What it doesn't support:
--- - Parallel execution. Due to idea of limiting implementation complexity.
---
--- - Mocks, stubs, etc. Use child Neovim process and manually override what is
--- needed. Reset child process it afterwards.
---
--- - "Overly specific" expectations. Tests for (no) equality and (absence of)
--- errors usually cover most of the needs. Adding new expectations is a
--- subject to weighing its usefulness against additional implementation
--- complexity. Use |MiniTest.new_expectation()| to create custom ones.
---
--- For more information see:
--- - `TESTING.md` file for a hands-on introduction based on examples.
---
--- - Code of this plugin's tests. Consider it to be an example of intended
--- way to use |mini.test| for test organization and creation.
---
--- # Workflow ~
---
--- - Organize tests in separate files. Each test file should return a test set
--- (explicitly or implicitly by using "busted" style functions).
---
--- - Write test actions as callable entries of test set. Use child process
--- inside test actions (see |MiniTest.new_child_neovim()|) and builtin
--- expectations (see |MiniTest.expect|).
---
--- - Run tests. This does two steps:
--- - Collect. This creates single hierarchical test set, flattens into
--- array of test cases (see |MiniTest-test-case|) while expanding with
--- parametrization, and possibly filters them.
--- - Execute. This safely calls hooks and main test actions in specified
--- order while allowing reporting progress in asynchronous fashion.
--- Detected errors means test case fail; otherwise - pass.
---
--- # Setup ~
---
--- This module needs a setup with `require('mini.test').setup({})` (replace
--- `{}` with your `config` table). It will create global Lua table `MiniTest`
--- which you can use for scripting or manually (with `:lua MiniTest.*`).
---
--- See |MiniTest.config| for available config settings.
---
--- You can override runtime config settings locally to buffer inside
--- `vim.b.minitest_config` which should have same structure as `MiniTest.config`.
--- See |mini.nvim-buffer-local-config| for more details.
---
--- To stop module from showing non-error feedback, set `config.silent = true`.
---
--- # Comparisons ~
---
--- - Testing infrastructure from
--- [nvim-lua/plenary.nvim](https://github.com/nvim-lua/plenary.nvim):
--- - Executes each file in separate headless Neovim process with customizable
--- `init.vim` file. While |mini.test| executes everything in current
--- Neovim process encouraging writing tests with help of manually
--- managed child Neovim process (see |MiniTest.new_child_neovim()|).
--- - Tests are expected to be written with embedded simplified versions of
--- `lunarmodules/busted` and `lunarmodules/luassert`. While |mini.test|
--- uses concepts of test set (see |MiniTest.new_set()|) and test case
--- (see |MiniTest-test-case|). It also can emulate bigger part of
--- "busted" framework.
--- - Has single way of reporting progress (shows result after every case
--- without summary). While |mini.test| can have customized reporters
--- with defaults for interactive and headless usage (provide more
--- compact and user-friendly summaries).
--- - Allows parallel execution, while |mini.test| does not.
--- - Allows making mocks, stubs, and spies, while |mini.test| does not in
--- favor of manually overwriting functionality in child Neovim process.
---
--- Although |mini.test| supports emulation of "busted style" testing, it will
--- be more stable to use its designed approach of defining tests (with
--- `MiniTest.new_set()` and explicit table fields). Couple of reasons:
--- - "Busted" syntax doesn't support full capabilities offered by |mini.test|.
--- Mainly it is about parametrization and supplying user data to test sets.
--- - It is an emulation, not full support. So some subtle things might not
--- work the way you expect.
---
--- Some hints for converting from `plenary.nvim` tests to |mini.test|:
--- - Rename files from `***_spec.lua` to `test_***.lua` and put them in
--- "tests" directory.
--- - Replace `assert` calls with |mini.test| expectations. See |MiniTest.expect|.
--- - Create main test set `T = MiniTest.new_set()` and eventually return it.
--- - Make new sets (|MiniTest.new_set()|) from `describe` blocks. Convert
--- `before_each()` and `after_each` to `pre_case` and `post_case` hooks.
--- - Make test cases from `it` blocks.
---
--- # Highlight groups ~
--- *MiniTest-hl-groups*
---
--- - `MiniTestEmphasis` - emphasis highlighting. By default it is a bold text.
--- - `MiniTestFail` - highlighting of failed cases. By default it is a bold
--- text with `vim.g.terminal_color_1` color (red).
--- - `MiniTestPass` - highlighting of passed cases. By default it is a bold
--- text with `vim.g.terminal_color_2` color (green).
---
--- To change any highlight group, set it directly with |nvim_set_hl()|.
---
--- # Disabling ~
---
--- To disable, set `vim.g.minitest_disable` (globally) or `vim.b.minitest_disable`
--- (for a buffer) to `true`. Considering high number of different scenarios
--- and customization intentions, writing exact rules for disabling module's
--- functionality is left to user. See |mini.nvim-disabling-recipes| for common
--- recipes.
---@tag MiniTest
---@alias __test_expect_fail_reason - <fail_reason> `(string|function)` - reason for failing expectation.
--- a function is called with expectation input and should return a string.
--- Default: `nil` for default reason like "Failed expectation for ...".
---@diagnostic disable: unnecessary-if
if false then
--- Expectation options
---@class MiniTest.expect.opts
---@field fail_reason? string|function Reason for failing expectation
end
-- Module definition ==========================================================
local MiniTest = {}
local H = {}
if false then
--- MiniTest config.
---@class MiniTest.config
---@field collect? MiniTest.config.collect Options for collection of test cases.
---@field execute? MiniTest.config.execute Options for execution of test cases.
--- Path (relative to current directory) to script which handles project
--- specific test running
---@field script_path? string
---@field silent? boolean Whether to disable showing non-error feedback.
--- Options for collection of test cases.
---@class MiniTest.config.collect
---@field emulate_busted? boolean Temporarily emulate functions from 'busted' testing framework.
--- Function returning array of file paths to be collected.
--- Default: all Lua files in 'tests' directory starting with 'test_'.
---@field find_files? fun(): string[]
--- Predicate function indicating if test case should be executed.
---@field filter_cases? fun(case: MiniTest.case): boolean
--- Options for execution of test cases.
---@class MiniTest.config.execute
--- Table with callable fields `start()`, `update()`, and `finish()`
---@field reporter? MiniTest.config.execute.reporter
---@field stop_on_error? boolean Whether to stop execution after first error.
--- Custom reporters.
---@class MiniTest.config.execute.reporter
---@field start? fun(cases: MiniTest.case[])
---@field update? fun(case_num: integer)
---@field finish? function
end
--- Module setup
---
---@param config? MiniTest.config Module config table. See |MiniTest.config|.
---
---@usage >lua
--- require('mini.test').setup() -- use default config
--- -- OR
--- require('mini.test').setup({}) -- replace {} with your config table
--- <
MiniTest.setup = function(config)
-- TODO: Remove after Neovim=0.9 support is dropped
if vim.fn.has('nvim-0.10') == 0 then
vim.notify(
'(mini.test) Neovim<0.10 is soft deprecated (module works but is not supported).'
.. " It will be deprecated after the next 'mini.nvim' release (module might not work)."
.. ' Please update your Neovim version.'
)
end
-- Export module
_G.MiniTest = MiniTest
-- Setup config
config = H.setup_config(config)
-- Apply config
H.apply_config(config)
-- Define behavior
H.create_autocommands()
-- Create default highlighting
H.create_default_hl()
end
--stylua: ignore start
--- Defaults ~
---@eval return MiniDoc.afterlines_to_code(MiniDoc.current.eval_section)
MiniTest.config = {
-- Options for collection of test cases. See `:h MiniTest.collect()`.
collect = {
-- Temporarily emulate functions from 'busted' testing framework
-- (`describe`, `it`, `before_each`, `after_each`, and more)
emulate_busted = true,
-- Function returning array of file paths to be collected.
-- Default: all Lua files in 'tests' directory starting with 'test_'.
find_files = function()
return vim.fn.globpath('tests', '**/test_*.lua', true, true)
end,
-- Predicate function indicating if test case should be executed
filter_cases = function(case) return true end,
},
-- Options for execution of test cases. See `:h MiniTest.execute()`.
execute = {
-- Table with callable fields `start()`, `update()`, and `finish()`
reporter = nil,
-- Whether to stop execution after first error
stop_on_error = false,
},
-- Path (relative to current directory) to script which handles project
-- specific test running
script_path = 'scripts/minitest.lua',
-- Whether to disable showing non-error feedback
silent = false,
}
--minidoc_afterlines_end
--stylua: ignore end
if false then
--- Table with information about current state of test execution
---@class MiniTest.current
--- Array with all cases being currently executed. Basically,
--- an input of `MiniTest.execute()`.
---@field all_cases? MiniTest.case[]
--- Currently executed test case. Use it to customize
--- execution output (like adding custom notes, etc).
---@field case? MiniTest.case
end
-- Module data ================================================================
--- Table with information about current state of test execution
---
--- Use it to examine result of |MiniTest.execute()|. It is reset at the
--- beginning of every call.
---
--- At least these keys are supported:
--- - <all_cases> - array with all cases being currently executed. Basically,
--- an input of `MiniTest.execute()`.
--- - <case> - currently executed test case. See |MiniTest-test-case|. Use it
--- to customize execution output (like adding custom notes, etc).
MiniTest.current = { all_cases = nil, case = nil } --[[@as MiniTest.current]]
if false then
--- `new_set()` options.
---@class MiniTest.new_set.opts
--- A table with elements that will be called without arguments at predefined stages of test execution.
---@field hooks? MiniTest.new_set.opts.hooks
--- An array defining different arguments with which main test actions will be called.
---@field parametrize? any[][]
---@field data? table A table with user data that will be forwarded to cases.
---@field n_retry? integer Number of times to retry each case until success. Default: 1.
--- `new_set()` hooks
---@class MiniTest.new_set.opts.hooks
---@field pre_once? function A function executed before first filtered node.
---@field pre_case? function A function executed before each case (even nested).
---@field post_case? function A function executed after each case (even nested).
---@field post_once? function A function executed after last filtered node.
end
-- Module functionality =======================================================
--- Create test set
---
--- Test set is one of the two fundamental data structures. It is a table that
--- defines hierarchical test organization as opposed to sequential
--- organization with |MiniTest-test-case|.
---
--- All its elements are one of three categories:
--- - A callable (object that can be called; function or table with `__call`
--- metatble entry) is considered to define a test action. It will be called
--- with "current arguments" (result of all nested `parametrize` values, read
--- further). If it throws error, test has failed.
--- - A test set (output of this function) defines nested structure. Its
--- options during collection (see |MiniTest.collect()|) will be extended
--- with options of this (parent) test set.
--- - Any other elements are considered helpers and don't directly participate
--- in test structure.
---
--- Set options allow customization of test collection and execution (more
--- details in `opts` description):
--- - `hooks` - table with elements that will be called without arguments at
--- predefined stages of test execution.
--- - `parametrize` - array defining different arguments with which main test
--- actions will be called. Any non-trivial parametrization will lead to
--- every element (even nested) be "multiplied" and processed with every
--- element of `parametrize`. This allows handling many different combination
--- of tests with little effort.
--- - `data` - table with user data that will be forwarded to cases. Primary
--- objective is to be used for customized case filtering.
---
--- Notes:
--- - Preferred way of adding elements is by using syntax `T[name] = element`.
--- This way order of added elements will be preserved. Any other way won't
--- guarantee any order.
--- - Supplied options `opts` are stored in `opts` field of metatable
--- (`getmetatable(set).opts`).
---
---@param opts? MiniTest.new_set.opts Allowed options:
--- - <hooks> - table with fields:
--- - <pre_once> - executed before first filtered node.
--- - <pre_case> - executed before each case (even nested).
--- - <post_case> - executed after each case (even nested).
--- - <post_once> - executed after last filtered node.
--- - <parametrize> - array where each element is itself an array of
--- parameters to be appended to "current parameters" of callable fields.
--- Note: don't use plain `{}` as it is equivalent to "parametrization into
--- zero cases", so no cases will be collected from this set. Calling test
--- actions with no parameters is equivalent to `{{}}` or not supplying
--- `parametrize` option at all.
--- - <data> - user data to be forwarded to cases. Can be used for a more
--- granular filtering.
--- - <n_retry> - number of times to retry each case until success.
--- Default: 1.
---@param tbl table|nil Initial test items (possibly nested). Will be executed
--- without any guarantees on order.
---
---@return table A single test set.
---
---@usage >lua
--- -- Use with defaults
--- T = MiniTest.new_set()
--- T['works'] = function() MiniTest.expect.equality(1, 1) end
---
--- -- Use with custom options. This will result into two actual cases: first
--- -- will pass, second - fail after two attempts.
--- T['nested'] = MiniTest.new_set({
--- hooks = { pre_case = function() _G.x = 1 end },
--- parametrize = { { 1 }, { 2 } },
--- n_retry = 2,
--- })
---
--- T['nested']['works'] = function(x) MiniTest.expect.equality(_G.x, x) end
--- <
MiniTest.new_set = function(opts, tbl)
opts = opts or {}
tbl = tbl or {}
-- Keep track of new elements order. This allows to iterate through elements
-- in order they were added.
local metatbl = { class = 'testset', key_order = vim.tbl_keys(tbl), opts = opts }
metatbl.__newindex = function(t, key, value)
table.insert(metatbl.key_order, key)
rawset(t, key, value)
end
return setmetatable(tbl, metatbl)
end
if false then
--- Test case
---@class MiniTest.case
---@field args any[] Array of arguments with which `test` will be called.
---@field data table User data: all fields of `opts.data` from nested test sets.
---@field desc string[] Description: array of fields from nested test sets.
---@field exec? table|nil Information about test case execution. Value of `nil` means
--- that this particular case was not (yet) executed. Has following fields:
--- - <fails> - array of strings with failing information.
--- - <notes> - array of strings with non-failing information.
--- - <state> - state of test execution. One of:
--- - 'Executing <name of what is being executed>' (during execution).
--- - 'Pass' (no fails, no notes).
--- - 'Pass with notes' (no fails, some notes).
--- - 'Fail' (some fails, no notes).
--- - 'Fail with notes' (some fails, some notes).
---@field hooks MiniTest.case.hooks Hooks to be executed as part of test case. Has fields:
--- - <pre> and <post> - arrays of functions to be consecutively executed
--- before and after every execution of `test`.
--- - <pre_source> and <post_source> - arrays of strings with sources of
--- corresponding elements in <pre> and <post> arrays. Source is one of
--- `"once"` (for `pre_once` and `post_once` hooks) and
--- `"case"` (for `pre_case` and `post_case` hooks).
---@field test function|table Main callable object representing test action.
---@field n_retry? integer Number of times to retry each case until success. Default: 1.
--- Information about test case execution.
---@class MiniTest.test.exec
---@field fails string[] Array of strings with failing information.
---@field notes string[] Array of strings with non-failing information.
--- State of test execution. One of:
--- - 'Executing <name of what is being executed>' (during execution).
--- - 'Pass' (no fails, no notes).
--- - 'Pass with notes' (no fails, some notes).
--- - 'Fail' (some fails, no notes).
--- - 'Fail with notes' (some fails, some notes).
---@field state "Pass"|"Pass with notes"|"Fail"|"Fail with notes"|"Executing test"|string
--- Hooks to be executed as part of test case.
---@class MiniTest.case.hooks
---@field pre function[] Hooks to be executed before execution of the test.
---@field post function[] Hooks to be executed after execution of the test.
---@field pre_source ("once"|"case")[] Sources of corresponding elements in `pre` and `post` arrays.
---@field post_source ("once"|"case")[] Sources of corresponding elements in `pre` and `post` arrays.
end
--- Test case
---
--- An item of sequential test organization, as opposed to hierarchical with
--- test set (see |MiniTest.new_set()|). It is created as result of test
--- collection with |MiniTest.collect()| to represent all necessary information
--- of test execution.
---
--- Execution of test case goes by the following rules:
--- - Call functions in order:
--- - All elements of `hooks.pre` from first to last without arguments.
--- - Field `test` with arguments unpacked from `args`. If execution fails,
--- retry it (along with hooks that come from `pre_case` and `post_case`)
--- at most `n_retry` times until first success (if any).
--- - All elements of `hooks.post` from first to last without arguments.
--- - Error in any call gets appended to `exec.fails`, meaning error in any
--- hook will lead to test fail.
--- - State (`exec.state`) is changed before every call and after last call.
---
---@class Test-case
---
---@field args table Array of arguments with which `test` will be called.
---@field data table User data: all fields of `opts.data` from nested test sets.
---@field desc table Description: array of fields from nested test sets.
---@field exec table|nil Information about test case execution. Value of `nil` means
--- that this particular case was not (yet) executed. Has following fields:
--- - <fails> - array of strings with failing information.
--- - <notes> - array of strings with non-failing information.
--- - <state> - state of test execution. One of:
--- - `'Executing <name of what is being executed>'` (during execution).
--- - `'Pass'` (no fails, no notes).
--- - `'Pass with notes'` (no fails, some notes).
--- - `'Fail'` (some fails, no notes).
--- - `'Fail with notes'` (some fails, some notes).
---@field hooks table Hooks to be executed as part of test case. Has fields:
--- - <pre> and <post> - arrays of functions to be consecutively executed
--- before and after every execution of `test`.
--- - <pre_source> and <post_source> - arrays of strings with sources of
--- corresponding elements in <pre> and <post> arrays. Source is one of
--- `"once"` (for `pre_once` and `post_once` hooks) and
--- `"case"` (for `pre_case` and `post_case` hooks).
---@field test function|table Main callable object representing test action.
---@tag MiniTest-test-case
--- Skip the rest of current case
---
--- Notes:
--- - When called inside test case, stops execution while adding message to notes.
--- - When called inside `pre_case` hook, registers skip at the start of its
--- test case. Calling in other hooks has no effect.
--- - Currently implemented as a specially handled type of error.
---
---@param msg string|nil Message to be added to current case notes.
MiniTest.skip = function(msg)
H.cache.skip_message = msg or 'Skip test'
error(H.cache.skip_message, 0)
end
--- Add note to currently executed test case
---
--- Appends `msg` to `exec.notes` field of `case` in |MiniTest.current|.
---
---@param msg string Note to add.
MiniTest.add_note = function(msg)
local case = MiniTest.current.case
case.exec = case.exec or {}
case.exec.notes = case.exec.notes or {}
table.insert(case.exec.notes, msg)
end
--- Register callable execution after current callable
---
--- Can be used several times inside hooks and main test callable of test case.
---
---@param f function|table Callable to be executed after current callable is
--- finished executing (regardless of whether it ended with error or not).
MiniTest.finally = function(f) table.insert(H.cache.finally, f) end
--- Run tests
---
--- - Try executing project specific script at path `opts.script_path`. If
--- successful (no errors), then stop.
--- - Collect cases with |MiniTest.collect()| and `opts.collect`.
--- - Execute collected cases with |MiniTest.execute()| and `opts.execute`.
---
---@param opts? MiniTest.config Options with structure similar to |MiniTest.config|.
--- Absent values are inferred from there.
MiniTest.run = function(opts)
if H.is_disabled() then return end
-- Try sourcing project specific script first
local success = H.execute_project_script(opts)
if success then return end
-- Collect and execute
opts = H.get_config(opts)
local cases = MiniTest.collect(opts.collect)
MiniTest.execute(cases, opts.execute)
end
--- Run specific test file
---
--- Basically a |MiniTest.run()| wrapper with custom `collect.find_files` option.
---
---@param file string|nil Path to test file. By default a path of current buffer.
---@param opts? MiniTest.config Options for |MiniTest.run()|.
MiniTest.run_file = function(file, opts)
file = vim.fn.fnamemodify(file or vim.api.nvim_buf_get_name(0), ':p:.')
local stronger_opts = { collect = { find_files = function() return { file } end } }
opts = vim.tbl_deep_extend('force', opts or {}, stronger_opts)
MiniTest.run(opts)
end
--- Run case(s) covering location
---
--- Try filtering case(s) covering location, meaning that definition of its
--- main `test` action (as taken from builtin `debug.getinfo`) is located in
--- specified file and covers specified line. Note that it can result in
--- multiple cases if they come from parametrized test set (see `parametrize`
--- option in |MiniTest.new_set()|).
---
--- Basically a |MiniTest.run()| wrapper with custom `collect.find_files` option.
---
---@param location? MiniTest.config Table with fields <file> (path to file) and <line>
--- (line number in that file). Default is taken from current cursor position.
MiniTest.run_at_location = function(location, opts)
if location == nil then
local cur_file = vim.fn.fnamemodify(vim.api.nvim_buf_get_name(0), ':.')
local cur_pos = vim.api.nvim_win_get_cursor(0)
location = { file = cur_file, line = cur_pos[1] }
end
local stronger_opts = {
collect = {
find_files = function() return { location.file } end,
filter_cases = function(case)
local info = debug.getinfo(case.test)
return info.short_src == location.file
and info.linedefined <= location.line
and location.line <= info.lastlinedefined
end,
},
}
opts = vim.tbl_deep_extend('force', opts or {}, stronger_opts)
MiniTest.run(opts)
end
--- Collect test cases
---
--- Overview of collection process:
--- - If `opts.emulate_busted` is `true`, temporary make special global
--- functions (removed at the end of collection). They can be used inside
--- test files to create hierarchical structure of test cases.
--- - Source each file from array output of `opts.find_files`. It should output
--- a test set (see |MiniTest.new_set()|) or `nil` (if "busted" style is used;
--- test set is created implicitly).
--- - Combine all test sets into single set with fields equal to its file path.
--- - Convert from hierarchical test configuration to sequential: from single
--- test set to array of test cases (see |MiniTest-test-case|). Conversion is
--- done in the form of "for every table element do: for every `parametrize`
--- element do: ...". Details:
--- - If element is a callable, construct test case with it being main
--- `test` action. Description is appended with key of element in current
--- test set table. Hooks, arguments, and data are taken from "current
--- nested" ones. Add case to output array.
--- - If element is a test set, process it in similar, recursive fashion.
--- The "current nested" information is expanded:
--- - `args` is extended with "current element" from `parametrize`.
--- - `desc` is appended with element key.
--- - `hooks` are appended to their appropriate places. `*_case` hooks
--- will be inserted closer to all child cases than hooks from parent
--- test sets: `pre_case` at end, `post_case` at start.
--- - `data` is extended via |vim.tbl_deep_extend()|.
--- - Any other element is not processed.
--- - Filter array with `opts.filter_cases`. Note that input case doesn't contain
--- all hooks, as `*_once` hooks will be added after filtration.
--- - Add `*_once` hooks to appropriate cases.
---
---@param opts? MiniTest.config.collect Options controlling case collection. Possible fields:
--- - <emulate_busted> - whether to emulate `lunarmodules/busted` interface.
--- It emulates these global functions: `describe`, `it`, `setup`, `teardown`,
--- `before_each`, `after_each`. Use |MiniTest.skip()| instead of `pending()`
--- and |MiniTest.finally()| instead of `finally`.
--- - <find_files> - function which when called without arguments returns
--- array with file paths. Each file should be a Lua file returning single
--- test set or `nil`.
--- - <filter_cases> - function which when called with single test case
--- (see |MiniTest-test-case|) returns `false` if this case should be filtered
--- out; `true` otherwise.
---
---@return MiniTest.case[] Array of test cases ready to be used by |MiniTest.execute()|.
MiniTest.collect = function(opts)
opts = vim.tbl_deep_extend('force', H.get_config().collect, opts or {})
-- Make single test set
local set = MiniTest.new_set()
for _, file in ipairs(opts.find_files()) do
-- Possibly emulate 'busted' with current file. This allows to wrap all
-- implicit cases from that file into single set with file's name.
if opts.emulate_busted then
set[file] = MiniTest.new_set()
H.busted_emulate(set[file])
end
-- Execute file
local ok, t = pcall(dofile, file)
-- Catch errors
if not ok then
local msg = string.format('Sourcing %s resulted into following error: %s', vim.inspect(file), t)
H.error(msg)
end
local is_output_correct = (opts.emulate_busted and vim.tbl_count(set[file]) > 0) or H.is_instance(t, 'testset')
if not is_output_correct then
local msg = string.format(
[[%s does not define a test set. Did you return `MiniTest.new_set()` or created 'busted' tests?]],
vim.inspect(file)
)
H.error(msg)
end
-- If output is test set, always use it (even if 'busted' tests were added)
if H.is_instance(t, 'testset') then set[file] = t end
end
H.busted_deemulate()
-- Convert to test cases. This also creates separate aligned array of hooks
-- which should be executed once regarding test case. This is needed to
-- correctly inject those hooks after filtering is done.
local raw_cases, raw_hooks_once = H.set_to_testcases(set)
-- Filter cases (at this stage don't have injected `hooks_once`)
local cases, hooks_once = {}, {}
for i, c in ipairs(raw_cases) do
if opts.filter_cases(c) then
table.insert(cases, c)
table.insert(hooks_once, raw_hooks_once[i])
end
end
-- Inject `hooks_once` into appropriate cases
H.inject_hooks_once(cases, hooks_once)
return cases
end
--- Execute array of test cases
---
--- Overview of execution process:
--- - Reset `all_cases` in |MiniTest.current| with `cases` input.
--- - Call `reporter.start(cases)` (if present).
--- - Execute each case in natural array order (aligned with their integer
--- keys). Set `MiniTest.current.case` to currently executed case. Detailed
--- test case execution is described in |MiniTest-test-case|. After any state
--- change (including case retry attempts), call `reporter.update(case_num)`
--- (if present), where `case_num` is an integer key of current test case.
--- - Call `reporter.finish()` (if present).
---
--- Notes:
--- - Execution is done in asynchronous fashion with scheduling. This allows
--- making meaningful progress report during execution.
--- - This function doesn't return anything. Instead, it updates `cases` in
--- place with proper `exec` field. Use `all_cases` at |MiniTest.current| to
--- look at execution result.
---
---@param cases MiniTest.case[] Array of test cases (see |MiniTest-test-case|).
---@param opts? MiniTest.config.execute Options controlling case collection. Possible fields:
--- - <reporter> - table with possible callable fields `start`, `update`,
--- `finish`. Default: |MiniTest.gen_reporter.buffer()| in interactive
--- usage and |MiniTest.gen_reporter.stdout()| in headless usage.
--- - <stop_on_error> - whether to stop execution (see |MiniTest.stop()|)
--- after first error. Default: `false`.
MiniTest.execute = function(cases, opts)
H.check_type('cases', cases, 'table')
MiniTest.current.all_cases = cases
-- Verify correct arguments
if #cases == 0 then
H.message('No cases to execute.')
return
end
opts = vim.tbl_deep_extend('force', H.get_config().execute, opts or {})
local reporter = opts.reporter or (H.is_headless and MiniTest.gen_reporter.stdout() or MiniTest.gen_reporter.buffer())
if type(reporter) ~= 'table' then
H.message('`opts.reporter` should be table or `nil`.')
return
end
opts.reporter = reporter
-- Plan execution in order
H.cache = { is_executing = true }
local queue = {}
table.insert(queue, function() H.exec_callable(reporter.start, cases) end)
for case_num, cur_case in ipairs(cases) do
table.insert(queue, H.make_case(cur_case, case_num, opts))
end
table.insert(queue, function() H.exec_callable(reporter.finish) end)
-- - Use separate call to ensure that `reporter.finish` error won't interfere
table.insert(queue, function() H.cache.is_executing = false end)
-- Execute queue ensuring order
-- NOTE: Directly `vim.schedule` each step without an explicit queue handling
-- is possible, but it might interfere with async-adjacent yet synchronous
-- functions (as `vim.wait()`) callsed inside cases outside of child process.
local exec_queue_step, n_queue = function(_) end, #queue
exec_queue_step = function(n)
queue[n]()
if n < n_queue then vim.schedule(function() exec_queue_step(n + 1) end) end
end
vim.schedule(function() exec_queue_step(1) end)
end
if false then
--- Stop options
---@class MiniTest.stop.opts
---@field close_all_child_neovim? boolean Whether to close all child neovim processes. Default: true.
end
--- Stop test execution
---
---@param opts table|nil Options with fields:
--- - <close_all_child_neovim> - whether to close all child neovim processes
--- created with |MiniTest.new_child_neovim()|. Default: `true`.
MiniTest.stop = function(opts)
opts = vim.tbl_deep_extend('force', { close_all_child_neovim = true }, opts or {})
-- Register intention to stop execution
H.cache.should_stop_execution = true
-- Possibly stop all child Neovim processes
if not opts.close_all_child_neovim then return end
for _, child in ipairs(H.child_neovim_registry) do
pcall(child.stop)
end
H.child_neovim_registry = {}
end
--- Check if tests are being executed
---
---@return boolean
MiniTest.is_executing = function() return H.cache.is_executing == true end
-- Expectations ---------------------------------------------------------------
--- Table with expectation functions
---
--- Each function has the following behavior:
--- - Silently returns `true` if expectation is fulfilled.
--- - Throws an informative error with information helpful for debugging.
--- Allows customizable fail reason to provide more context.
---
--- Mostly designed to be used within |mini.test| framework.
---
---@usage >lua
--- local x = 1 + 1
--- MiniTest.expect.equality(x, 2) -- passes
--- MiniTest.expect.equality(x, 1, { fail_reason = 'Not equal' }) -- fails
--- <
MiniTest.expect = {}
--- Expect equality of two objects
---
--- Equality is tested via |vim.deep_equal()|. It also tries to compute more
--- detailed cause for equality (for easier spotting the difference):
--- - If they have different types.
--- - For strings if they have different length or if some character is different.
--- - For tables it shows a "key branch" at which values are different along with
--- the actual values. A single difference is shown, there might be more.
--- For not nested tables key branch is just a key. If the difference is inside
--- nested tables, the key branch shows a "path through nested tables" to
--- a different value. Examples: >lua
---
--- local eq = MiniTest.expect.equality
--- eq({ 1, 2 }, { 1, 3 }) -- Key branch is `2`
--- eq({ 1, { 2 } }, { 1, 'c' }) -- Key branch is `2` ('c' is not a table)
--- eq({ 1, { 2 } }, { 1, { 3 } }) -- Key branch is `2->1`
---
--- -- Key branch is either `1->1->"a"` or `1->1->"b"`
--- eq({ { { a = 1 } } }, { { { b = 2 } } })
--- <
---@param left any First object.
---@param right any Second object.
---@param opts? MiniTest.expect.opts Options. Possible fields:
--- __test_expect_fail_reason
MiniTest.expect.equality = function(left, right, opts)
if vim.deep_equal(left, right) then return true end
opts = opts or {}
local fail_reason = H.normalize_reason(opts.fail_reason, 'Failed expectation for equality', left, right)
local cause = H.compute_no_equality_cause(left, right)
local context = string.format('Cause: %s\nLeft: %s\nRight: %s', cause, vim.inspect(left), vim.inspect(right))
H.error_with_emphasis(fail_reason, context)
end
--- Expect no equality of two objects
---
--- Equality is tested via |vim.deep_equal()|.
---
---@param left any First object.
---@param right any Second object.
---@param opts? MiniTest.expect.opts Options. Possible fields:
--- __test_expect_fail_reason
MiniTest.expect.no_equality = function(left, right, opts)
if not vim.deep_equal(left, right) then return true end
opts = opts or {}
local fail_reason = H.normalize_reason(opts.fail_reason, 'Failed expectation for *no* equality', left, right)
local context = string.format('Object: %s', vim.inspect(left))
H.error_with_emphasis(fail_reason, context)
end
--- Expect function call to raise error
---
---@param f function Function to be tested for raising error.
---@param pattern string|nil Pattern which error message should match.
--- Use `nil` or empty string to not test for pattern matching.
---@param opts? MiniTest.expect.opts Options. Possible fields:
--- __test_expect_fail_reason
MiniTest.expect.error = function(f, pattern, opts, ...)
H.check_type('pattern', pattern, 'string', true)
-- Provide backward compatibility for `(f, pattern, ...)` signature.
-- TODO: Remove after releasing 'mini.nvim' 0.18.0
local args = { ... }
local is_valid_opts = type(opts) == 'table'
and (opts.fail_reason == nil or type(opts.fail_reason) == 'string' or vim.is_callable(opts.fail_reason))
if select('#', ...) > 0 or not (opts == nil or is_valid_opts) then
table.insert(args, 1, opts)
opts = {}
vim.notify(
'(mini.test) `expect.error` now does not accept extra arguments for tested function.'
.. " It will mostly work until the next 'mini.nvim' release, but not after that."
.. ' Use them explicitly inside anonymous function: `expect.error(f, "", 1, 2)` ->'
.. ' `expect.error(function() f(1, 2) end, "")`.'
.. '\nSorry for the inconvenience.',
vim.log.levels.WARN
)
end
local ok, err = pcall(f, unpack(args))
err = tostring(err)
local has_matched_error = not ok and string.find(err, pattern or '') ~= nil
if has_matched_error then return true end
opts = opts or {}
local pattern_suffix = pattern == nil and '' or (' matching pattern ' .. vim.inspect(pattern))
local fail_reason = H.normalize_reason(opts.fail_reason, 'Failed expectation for error' .. pattern_suffix, f, pattern)
local context = ok and 'Observed no error' or ('Observed error: ' .. err)
H.error_with_emphasis(fail_reason, context)
end
--- Expect function call to not raise error
---
---@param f function Function to be tested for not raising error.
---@param opts? MiniTest.expect.opts Options. Possible fields:
--- __test_expect_fail_reason
MiniTest.expect.no_error = function(f, opts, ...)
-- Provide backward compatibility for `(f, ...)` signature.
-- TODO: Remove after releasing 'mini.nvim' 0.18.0
local args = { ... }
local is_valid_opts = type(opts) == 'table' and (opts.fail_prefix == nil or type(opts.fail_prefix) == 'string')
if select('#', ...) > 0 or not (opts == nil or is_valid_opts) then
table.insert(args, 1, opts)
opts = {}
vim.notify(
'(mini.test) `expect.no_error` now does not accept extra arguments for tested function.'
.. " It will mostly work until the next 'mini.nvim' release, but not after that."
.. ' Use them explicitly inside anonymous function: `expect.no_error(f, 1, 2)` ->'
.. ' `expect.no_error(function() f(1, 2) end)`.'
.. '\nSorry for the inconvenience.',
vim.log.levels.WARN
)
end
local ok, err = pcall(f, unpack(args))
if ok then return true end
opts = opts or {}
local fail_reason = H.normalize_reason(opts.fail_reason, 'Failed expectation for *no* error', f)
H.error_with_emphasis(fail_reason, 'Observed error: ' .. tostring(err))
end
-- Error context for match expectations
local function match_fail_context(string, pattern, init, plain)
if plain ~= nil then
init = init or 'nil'
pattern = string.format('%s, %s, %s', pattern, init, plain)
elseif init then
pattern = string.format('%s, %s', pattern, init)
end
return string.format('Pattern: %s\nObserved string: %s', pattern, string)
end
--- Expect string to match pattern.
---
---@param string string String to be tested for pattern matching.
---@param pattern string Pattern which string should match.
---@param init? integer Where to start the match. Default is 1.
---@param plain? boolean Whether to match pattern literally.
---@param opts? MiniTest.expect.opts Options. Possible fields:
--- __test_expect_fail_reason
function MiniTest.expect.matching(string, pattern, init, plain, opts)
if string:find(pattern, init, plain) ~= nil then return true end
opts = opts or {}
local fail_reason = H.normalize_reason(opts.fail_reason, 'Failed expectation for string matching', string, pattern)
H.error_with_emphasis(fail_reason, match_fail_context(string, pattern, init, plain))
end
--- Expect string to not match pattern.
---
---@param string string String to be tested for pattern matching.
---@param pattern string Pattern which string should not match.
---@param init? integer Where to start the match. Default is 1.
---@param plain? boolean Whether to match pattern literally.
---@param opts? MiniTest.expect.opts Options. Possible fields:
--- __test_expect_fail_reason
function MiniTest.expect.no_matching(string, pattern, init, plain, opts)
if string:find(pattern, init, plain) == nil then return true end
opts = opts or {}
local fail_reason =
H.normalize_reason(opts.fail_reason, 'Failed expectation for *no* string matching', string, pattern)
H.error_with_emphasis(fail_reason, match_fail_context(string, pattern, init, plain))
end
--- Expect value to be truthy. Similar to `assert()`.
---
---@param value any Value to be asserted.
---@param message? string Assertion message.
---@param opts? MiniTest.expect.opts Options. Possible fields:
--- __test_expect_fail_reason
function MiniTest.expect.assertion(value, message, opts)
if value then return true end
opts = opts or {}
local fail_reason = H.normalize_reason(opts.fail_reason, 'Failed expectation for an assertion', value)
local assertion_message = message and string.format('Message: %s\n', message) or ''
local context = assertion_message .. 'Observed value: ' .. vim.inspect(value)
H.error_with_emphasis(fail_reason, context)
end
if false then
--- `expect.reference_screenshot()` options
---@class MiniTest.expect.reference_screenshot.opts: MiniTest.expect.opts
--- Whether to forcefully create reference screenshot.
--- Temporary useful during test writing. Default: `false`.
---@field force? boolean
--- Whether to ignore all or some text lines.
--- If `true` - ignore all, if number array - ignore text of those lines,
--- if `false` - do not ignore any. Default: `false`.
---@field ignore_text? boolean|integer[]
--- Whether to ignore all or some attr lines.
--- If `true` - ignore all, if number array - ignore attr of those lines,
--- if `false` - do not ignore any. Default: `false`.
0