-
Notifications
You must be signed in to change notification settings - Fork 177
/
test.lua
2311 lines (2000 loc) · 85 KB
/
test.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
--- *mini.test* Test Neovim plugins
--- *MiniTest*
---
--- 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 'Olivine-Labs/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.
---
--- 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':
--- - 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
--- 'Olivine-Labs/busted' and 'Olivine-Labs/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 ~
---
--- * `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, modify it directly with |:highlight|.
---
--- # 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.
-- Module definition ==========================================================
local MiniTest = {}
local H = {}
--- Module setup
---
---@param config table|nil 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)
-- 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
--- Module config
---
--- Default values:
---@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
-- 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 }
-- 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 table|nil 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.
---@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.
--- T['nested'] = MiniTest.new_set({
--- hooks = { pre_case = function() _G.x = 1 end },
--- parametrize = { { 1 }, { 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
--- 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`.
--- - 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> with arrays to be consecutively executed before and
--- after execution of `test`.
---@field test function|table Main callable object representing test action.
---@tag MiniTest-test-case
--- Skip rest of current callable execution
---
--- Can be used inside hooks and main test callable of test case. Note: at the
--- moment 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.error_is_from_skip = true
error(msg or 'Skip test', 0)
end
--- Add note to currently executed test case
---
--- Appends `msg` to `exec.notes` field of |MiniTest.current.case|.
---
---@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 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) 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 table|nil 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 table|nil 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 table|nil 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 table|nil Options controlling case collection. Possible fields:
--- - <emulate_busted> - whether to emulate 'Olivine-Labs/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 table 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, 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 table Array of test cases (see |MiniTest-test-case|).
---@param opts table|nil 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)
vim.validate({ 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
-- Start execution
H.cache = { is_executing = true }
vim.schedule(function() H.exec_callable(reporter.start, cases) end)
for case_num, cur_case in ipairs(cases) do
-- Schedule execution in async fashion. This allows doing other things
-- while tests are executed.
local schedule_step = H.make_step_scheduler(cur_case, case_num, opts)
vim.schedule(function() MiniTest.current.case = cur_case end)
for i, hook_pre in ipairs(cur_case.hooks.pre) do
schedule_step(hook_pre, 'hook_pre', [[Executing 'pre' hook #]] .. i)
end
schedule_step(function() cur_case.test(unpack(cur_case.args)) end, 'case', 'Executing test')
for i, hook_post in ipairs(cur_case.hooks.post) do
schedule_step(hook_post, 'hook_post', [[Executing 'post' hook #]] .. i)
end
-- Finalize state
schedule_step(nil, 'finalize', function() return H.case_final_state(cur_case) end)
end
vim.schedule(function() H.exec_callable(reporter.finish) end)
-- Use separate call to ensure that `reporter.finish` error won't interfere
vim.schedule(function() H.cache.is_executing = false end)
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.
---
--- 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) -- fails
--- <
MiniTest.expect = {}
--- Expect equality of two objects
---
--- Equality is tested via |vim.deep_equal()|.
---
---@param left any First object.
---@param right any Second object.
MiniTest.expect.equality = function(left, right)
if vim.deep_equal(left, right) then return true end
local context = string.format('Left: %s\nRight: %s', vim.inspect(left), vim.inspect(right))
H.error_expect('equality', 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.
MiniTest.expect.no_equality = function(left, right)
if not vim.deep_equal(left, right) then return true end
local context = string.format('Object: %s', vim.inspect(left))
H.error_expect('*no* equality', context)
end
--- Expect function call to raise error
---
---@param f function|table Callable 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 ... any Extra arguments with which `f` will be called.
MiniTest.expect.error = function(f, pattern, ...)
vim.validate({ pattern = { pattern, 'string', true } })
local ok, err = pcall(f, ...)
err = err or ''
local has_matched_error = not ok and string.find(err, pattern or '') ~= nil
if has_matched_error then return true end
local matching_pattern = pattern == nil and '' or (' matching pattern %s'):format(vim.inspect(pattern))
local subject = 'error' .. matching_pattern
local context = ok and 'Observed no error' or ('Observed error: ' .. err)
H.error_expect(subject, context)
end
--- Expect function call to not raise error
---
---@param f function|table Callable to be tested for raising error.
---@param ... any Extra arguments with which `f` will be called.
MiniTest.expect.no_error = function(f, ...)
local ok, err = pcall(f, ...)
err = err or ''
if ok then return true end
H.error_expect('*no* error', 'Observed error: ' .. err)
end
--- Expect equality to reference screenshot
---
---@param screenshot table|nil Array with screenshot information. Usually an output
--- of `child.get_screenshot()` (see |MiniTest-child-neovim.get_screenshot()|).
--- If `nil`, expectation passed.
---@param path string|nil Path to reference screenshot. If `nil`, constructed
--- automatically in directory 'tests/screenshots' from current case info and
--- total number of times it was called inside current case. If there is no
--- file at `path`, it is created with content of `screenshot`.
---@param opts table|nil Options:
--- - <force> `(boolean)` - whether to forcefully create reference screenshot.
--- Temporary useful during test writing. Default: `false`.
--- - <ignore_lines> `(table)` - array of line numbers to ignore during compare.
--- Default: `nil` to check all lines.
MiniTest.expect.reference_screenshot = function(screenshot, path, opts)
if screenshot == nil then return true end
opts = vim.tbl_deep_extend('force', { force = false }, opts or {})
H.cache.n_screenshots = H.cache.n_screenshots + 1
if path == nil then
-- Sanitize path. Replace any control characters, whitespace, OS specific
-- forbidden characters with '-' (with some useful exception)
local linux_forbidden = [[/]]
local windows_forbidden = [[<>:"/\|?*]]
local pattern = string.format('[%%c%%s%s%s]', vim.pesc(linux_forbidden), vim.pesc(windows_forbidden))
local replacements = setmetatable({ ['"'] = "'" }, { __index = function() return '-' end })
local name = H.case_to_stringid(MiniTest.current.case):gsub(pattern, replacements)
-- Don't end with whitespace or dot (forbidden on Windows)
name = name:gsub('[%s%.]$', '-')
path = 'tests/screenshots/' .. name
-- Deal with multiple screenshots
if H.cache.n_screenshots > 1 then path = path .. string.format('-%03d', H.cache.n_screenshots) end
end
-- If there is no readable screenshot file, create it. Pass with note.
if opts.force or vim.fn.filereadable(path) == 0 then
local dir_path = vim.fn.fnamemodify(path, ':p:h')
vim.fn.mkdir(dir_path, 'p')
H.screenshot_write(screenshot, path)
MiniTest.add_note('Created reference screenshot at path ' .. vim.inspect(path))
return true
end
local reference = H.screenshot_read(path)
-- Compare
local are_same, cause = H.screenshot_compare(reference, screenshot, opts)
if are_same then return true end
local subject = 'screenshot equality to reference at ' .. vim.inspect(path)
local context = string.format('%s\nReference:\n%s\n\nObserved:\n%s', cause, tostring(reference), tostring(screenshot))
H.error_expect(subject, context)
end
--- Create new expectation function
---
--- Helper for writing custom functions with behavior similar to other methods
--- of |MiniTest.expect|.
---
---@param subject string|function|table Subject of expectation. If callable,
--- called with expectation input arguments to produce string value.
---@param predicate function|table Predicate callable. Called with expectation
--- input arguments. Output `false` or `nil` means failed expectation.
---@param fail_context string|function|table Information about fail. If callable,
--- called with expectation input arguments to produce string value.
---
---@return function Expectation function.
---
---@usage >lua
--- local expect_truthy = MiniTest.new_expectation(
--- 'truthy',
--- function(x) return x end,
--- function(x) return 'Object: ' .. vim.inspect(x) end
--- )
--- <
MiniTest.new_expectation = function(subject, predicate, fail_context)
return function(...)
if predicate(...) then return true end
local cur_subject = vim.is_callable(subject) and subject(...) or subject
local cur_context = vim.is_callable(fail_context) and fail_context(...) or fail_context
H.error_expect(cur_subject, cur_context)
end
end
-- Reporters ------------------------------------------------------------------
--- Table with pre-configured report generators
---
--- Each element is a function which returns reporter - table with callable
--- `start`, `update`, and `finish` fields.
MiniTest.gen_reporter = {}
--- Generate buffer reporter
---
--- This is a default choice for interactive (not headless) usage. Opens a window
--- with dedicated non-terminal buffer and updates it with throttled redraws.
---
--- Opened buffer has the following helpful Normal mode mappings:
--- - `<Esc>` - stop test execution if executing (see |MiniTest.is_executing()|
--- and |MiniTest.stop()|). Close window otherwise.
--- - `q` - same as `<Esc>` for convenience and compatibility.
---
--- General idea:
--- - Group cases by concatenating first `opts.group_depth` elements of case
--- description (`desc` field). Groups by collected files if using default values.
--- - In `start()` show some stats to know how much is scheduled to be executed.
--- - In `update()` show symbolic overview of current group and state of current
--- case. Each symbol represents one case and its state:
--- - `?` - case didn't finish executing.
--- - `o` - pass.
--- - `O` - pass with notes.
--- - `x` - fail.
--- - `X` - fail with notes.
--- - In `finish()` show all fails and notes ordered by case.
---
---@param opts table|nil Table with options. Used fields:
--- - <group_depth> - number of first elements of case description (can be zero)
--- used for grouping. Higher values mean higher granularity of output.
--- Default: 1.
--- - <throttle_delay> - minimum number of milliseconds to wait between
--- redrawing. Reduces screen flickering but not amount of computations.
--- Default: 10.
--- - <window> - definition of window to open. Can take one of the forms:
--- - Callable. It is called expecting output to be target window id
--- (current window is used if output is `nil`). Use this to open in
--- "normal" window (like `function() vim.cmd('vsplit') end`).
--- - Table. Used as `config` argument in |nvim_open_win()|.
--- Default: table for centered floating window.
MiniTest.gen_reporter.buffer = function(opts)
-- NOTE: another choice of implementing this is to use terminal buffer
-- `vim.api.nvim_open_term()`.
-- Pros:
-- - Renders ANSI escape sequences (mostly) correctly, i.e. no need in
-- replacing them with Neovim range highlights.
-- - This reporter and `stdout` one can share more of a codebase.
-- Cons:
-- - Couldn't manage to implement "redraw on every update".
-- - Extra steps still are needed in order to have richer output information.
-- This involves ANSI sequences that move cursor, which have same issues as
-- in `stdout`, albeit easier to overcome:
-- - Handling of scroll.
-- - Hard wrapping of lines leading to need of using window width.
opts = vim.tbl_deep_extend(
'force',
{ group_depth = 1, throttle_delay = 10, window = H.buffer_reporter.default_window_opts() },
opts or {}
)
local buf_id, win_id
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
-- Helpers
local set_cursor = function(line)
vim.api.nvim_win_set_cursor(win_id, { line or vim.api.nvim_buf_line_count(buf_id), 0 })
end
-- Define "write from cursor line" function with throttled redraw
local latest_draw_time = 0
local replace_last = function(n_replace, lines, force)
H.buffer_reporter.set_lines(buf_id, lines, -n_replace - 1, -1)
-- Throttle redraw to reduce flicker
local cur_time = vim.loop.hrtime()
local is_enough_time_passed = (cur_time - latest_draw_time) > opts.throttle_delay * 1000000
if is_enough_time_passed or force then
vim.cmd('redraw')
latest_draw_time = cur_time
end
end
-- Create reporter functions
local res = {}
local all_cases, all_groups, latest_group_name
res.start = function(cases)
-- Set up buffer and window
buf_id, win_id = H.buffer_reporter.setup_buf_and_win(opts.window)
-- Set up data (taking into account possible not first time run)
all_cases = cases
all_groups = H.overview_reporter.compute_groups(cases, opts.group_depth)
latest_group_name = nil
-- Write lines
local lines = H.overview_reporter.start_lines(all_cases, all_groups)
replace_last(1, lines)
set_cursor()
end
res.update = function(case_num)
if not is_valid_buf_win() then return end
local case, cur_group_name = all_cases[case_num], all_groups[case_num].name
-- Update symbol
local state = type(case.exec) == 'table' and case.exec.state or nil
all_groups[case_num].symbol = H.reporter_symbols[state]
local n_replace = H.buffer_reporter.update_step_n_replace(latest_group_name, cur_group_name)
local lines = H.buffer_reporter.update_step_lines(case_num, all_cases, all_groups)
replace_last(n_replace, lines)
set_cursor()
latest_group_name = cur_group_name
end
res.finish = function()
if not is_valid_buf_win() then return end
-- Cache final cursor position to overwrite 'Current case state' header
local start_line = vim.api.nvim_buf_line_count(buf_id) - 1
-- Force writing lines
local lines = H.overview_reporter.finish_lines(all_cases)
replace_last(2, lines, true)
set_cursor(start_line)
end
return res
end
--- Generate stdout reporter
---
--- This is a default choice for headless usage. Writes to `stdout`. Uses
--- coloring ANSI escape sequences to make pretty and informative output
--- (should work in most modern terminals and continuous integration providers).
---
--- It has same general idea as |MiniTest.gen_reporter.buffer()| with slightly
--- less output (it doesn't overwrite previous text) to overcome typical
--- terminal limitations.
---
---@param opts table|nil Table with options. Used fields:
--- - <group_depth> - number of first elements of case description (can be zero)
--- used for grouping. Higher values mean higher granularity of output.
--- Default: 1.
--- - <quit_on_finish> - whether to quit after finishing test execution.
--- Default: `true`.
MiniTest.gen_reporter.stdout = function(opts)
opts = vim.tbl_deep_extend('force', { group_depth = 1, quit_on_finish = true }, opts or {})
local write = function(text)
text = type(text) == 'table' and table.concat(text, '\n') or text
io.stdout:write(text)
io.flush()
end
local all_cases, all_groups, latest_group_name
local default_symbol = H.reporter_symbols[nil]
local res = {}
res.start = function(cases)
-- Set up data
all_cases = cases
all_groups = H.overview_reporter.compute_groups(cases, opts.group_depth)
-- Write lines
local lines = H.overview_reporter.start_lines(all_cases, all_groups)
write(lines)
end
res.update = function(case_num)
local cur_case = all_cases[case_num]
local cur_group_name = all_groups[case_num].name
-- Possibly start overview of new group
if cur_group_name ~= latest_group_name then