1" This script is sourced while editing the .vim file with the tests. 2" When the script is successful the .res file will be created. 3" Errors are appended to the test.log file. 4" 5" To execute only specific test functions, add a second argument. It will be 6" matched against the names of the Test_ function. E.g.: 7" ../vim -u NONE -S runtest.vim test_channel.vim open_delay 8" The output can be found in the "messages" file. 9" 10" If the environment variable $TEST_FILTER is set then only test functions 11" matching this pattern are executed. E.g. for sh/bash: 12" export TEST_FILTER=Test_channel 13" For csh: 14" setenv TEST_FILTER Test_channel 15" 16" To ignore failure for tests that are known to fail in a certain environment, 17" set $TEST_MAY_FAIL to a comma separated list of function names. E.g. for 18" sh/bash: 19" export TEST_MAY_FAIL=Test_channel_one,Test_channel_other 20" The failure report will then not be included in the test.log file and 21" "make test" will not fail. 22" 23" The test script may contain anything, only functions that start with 24" "Test_" are special. These will be invoked and should contain assert 25" functions. See test_assert.vim for an example. 26" 27" It is possible to source other files that contain "Test_" functions. This 28" can speed up testing, since Vim does not need to restart. But be careful 29" that the tests do not interfere with each other. 30" 31" If an error cannot be detected properly with an assert function add the 32" error to the v:errors list: 33" call add(v:errors, 'test foo failed: Cannot find xyz') 34" 35" If preparation for each Test_ function is needed, define a SetUp function. 36" It will be called before each Test_ function. 37" 38" If cleanup after each Test_ function is needed, define a TearDown function. 39" It will be called after each Test_ function. 40" 41" When debugging a test it can be useful to add messages to v:errors: 42" call add(v:errors, "this happened") 43 44 45" Without the +eval feature we can't run these tests, bail out. 46so small.vim 47 48" Check that the screen size is at least 24 x 80 characters. 49if &lines < 24 || &columns < 80 50 let error = 'Screen size too small! Tests require at least 24 lines with 80 characters, got ' .. &lines .. ' lines with ' .. &columns .. ' characters' 51 echoerr error 52 split test.log 53 $put =error 54 write 55 split messages 56 call append(line('$'), '') 57 call append(line('$'), 'From ' . expand('%') . ':') 58 call append(line('$'), error) 59 write 60 qa! 61endif 62 63if has('reltime') 64 let s:start_time = reltime() 65endif 66 67" Common with all tests on all systems. 68source setup.vim 69 70" For consistency run all tests with 'nocompatible' set. 71" This also enables use of line continuation. 72set nocp viminfo+=nviminfo 73 74" Use utf-8 by default, instead of whatever the system default happens to be. 75" Individual tests can overrule this at the top of the file and use 76" g:orig_encoding if needed. 77let g:orig_encoding = &encoding 78set encoding=utf-8 79 80" REDIR_TEST_TO_NULL has a very permissive SwapExists autocommand which is for 81" the test_name.vim file itself. Replace it here with a more restrictive one, 82" so we still catch mistakes. 83let s:test_script_fname = expand('%') 84au! SwapExists * call HandleSwapExists() 85func HandleSwapExists() 86 " Ignore finding a swap file for the test script (the user might be 87 " editing it and do ":make test_name") and the output file. 88 " Report finding another swap file and chose 'q' to avoid getting stuck. 89 if expand('<afile>') == 'messages' || expand('<afile>') =~ s:test_script_fname 90 let v:swapchoice = 'e' 91 else 92 call assert_report('Unexpected swap file: ' .. v:swapname) 93 let v:swapchoice = 'q' 94 endif 95endfunc 96 97" Avoid stopping at the "hit enter" prompt 98set nomore 99 100" Output all messages in English. 101lang mess C 102 103" Always use forward slashes. 104set shellslash 105 106let s:srcdir = expand('%:p:h:h') 107 108" Prepare for calling test_garbagecollect_now(). 109let v:testing = 1 110 111" Support function: get the alloc ID by name. 112function GetAllocId(name) 113 exe 'split ' . s:srcdir . '/alloc.h' 114 let top = search('typedef enum') 115 if top == 0 116 call add(v:errors, 'typedef not found in alloc.h') 117 endif 118 let lnum = search('aid_' . a:name . ',') 119 if lnum == 0 120 call add(v:errors, 'Alloc ID ' . a:name . ' not defined') 121 endif 122 close 123 return lnum - top - 1 124endfunc 125 126func RunTheTest(test) 127 echo 'Executing ' . a:test 128 if has('reltime') 129 let func_start = reltime() 130 endif 131 132 " Avoid stopping at the "hit enter" prompt 133 set nomore 134 135 " Avoid a three second wait when a message is about to be overwritten by the 136 " mode message. 137 set noshowmode 138 139 " Clear any overrides. 140 call test_override('ALL', 0) 141 142 " Some tests wipe out buffers. To be consistent, always wipe out all 143 " buffers. 144 %bwipe! 145 146 " The test may change the current directory. Save and restore the 147 " directory after executing the test. 148 let save_cwd = getcwd() 149 150 if exists("*SetUp") 151 try 152 call SetUp() 153 catch 154 call add(v:errors, 'Caught exception in SetUp() before ' . a:test . ': ' . v:exception . ' @ ' . v:throwpoint) 155 endtry 156 endif 157 158 if a:test =~ 'Test_nocatch_' 159 " Function handles errors itself. This avoids skipping commands after the 160 " error. 161 exe 'call ' . a:test 162 else 163 try 164 au VimLeavePre * call EarlyExit(g:testfunc) 165 exe 'call ' . a:test 166 au! VimLeavePre 167 catch /^\cskipped/ 168 call add(s:messages, ' Skipped') 169 call add(s:skipped, 'SKIPPED ' . a:test . ': ' . substitute(v:exception, '^\S*\s\+', '', '')) 170 catch 171 call add(v:errors, 'Caught exception in ' . a:test . ': ' . v:exception . ' @ ' . v:throwpoint) 172 endtry 173 endif 174 175 " In case 'insertmode' was set and something went wrong, make sure it is 176 " reset to avoid trouble with anything else. 177 set noinsertmode 178 179 if exists("*TearDown") 180 try 181 call TearDown() 182 catch 183 call add(v:errors, 'Caught exception in TearDown() after ' . a:test . ': ' . v:exception . ' @ ' . v:throwpoint) 184 endtry 185 endif 186 187 " Clear any autocommands and put back the catch-all for SwapExists. 188 au! 189 au SwapExists * call HandleSwapExists() 190 191 " Check for and close any stray popup windows. 192 if has('popupwin') 193 call assert_equal([], popup_list()) 194 call popup_clear(1) 195 endif 196 197 " Close any extra tab pages and windows and make the current one not modified. 198 while tabpagenr('$') > 1 199 quit! 200 endwhile 201 202 while 1 203 let wincount = winnr('$') 204 if wincount == 1 205 break 206 endif 207 bwipe! 208 if wincount == winnr('$') 209 " Did not manage to close a window. 210 only! 211 break 212 endif 213 endwhile 214 215 exe 'cd ' . save_cwd 216 217 let message = 'Executed ' . a:test 218 if has('reltime') 219 let message ..= ' in ' .. reltimestr(reltime(func_start)) .. ' seconds' 220 endif 221 call add(s:messages, message) 222 let s:done += 1 223endfunc 224 225func AfterTheTest(func_name) 226 if len(v:errors) > 0 227 if match(s:may_fail_list, '^' .. a:func_name) >= 0 228 let s:fail_expected += 1 229 call add(s:errors_expected, 'Found errors in ' . g:testfunc . ':') 230 call extend(s:errors_expected, v:errors) 231 else 232 let s:fail += 1 233 call add(s:errors, 'Found errors in ' . g:testfunc . ':') 234 call extend(s:errors, v:errors) 235 endif 236 let v:errors = [] 237 endif 238endfunc 239 240func EarlyExit(test) 241 " It's OK for the test we use to test the quit detection. 242 if a:test != 'Test_zz_quit_detected()' 243 call add(v:errors, 'Test caused Vim to exit: ' . a:test) 244 endif 245 246 call FinishTesting() 247endfunc 248 249" This function can be called by a test if it wants to abort testing. 250func FinishTesting() 251 call AfterTheTest('') 252 253 " Don't write viminfo on exit. 254 set viminfo= 255 256 " Clean up files created by setup.vim 257 call delete('XfakeHOME', 'rf') 258 259 if s:fail == 0 && s:fail_expected == 0 260 " Success, create the .res file so that make knows it's done. 261 exe 'split ' . fnamemodify(g:testname, ':r') . '.res' 262 write 263 endif 264 265 if len(s:errors) > 0 266 " Append errors to test.log 267 split test.log 268 call append(line('$'), '') 269 call append(line('$'), 'From ' . g:testname . ':') 270 call append(line('$'), s:errors) 271 write 272 endif 273 274 if s:done == 0 275 if s:filtered > 0 276 let message = "NO tests match $TEST_FILTER: '" .. $TEST_FILTER .. "'" 277 else 278 let message = 'NO tests executed' 279 endif 280 else 281 if s:filtered > 0 282 call add(s:messages, "Filtered " .. s:filtered .. " tests with $TEST_FILTER") 283 endif 284 let message = 'Executed ' . s:done . (s:done > 1 ? ' tests' : ' test') 285 endif 286 if s:done > 0 && has('reltime') 287 let message ..= ' in ' .. reltimestr(reltime(s:start_time)) .. ' seconds' 288 endif 289 echo message 290 call add(s:messages, message) 291 if s:fail > 0 292 let message = s:fail . ' FAILED:' 293 echo message 294 call add(s:messages, message) 295 call extend(s:messages, s:errors) 296 endif 297 if s:fail_expected > 0 298 let message = s:fail_expected . ' FAILED (matching $TEST_MAY_FAIL):' 299 echo message 300 call add(s:messages, message) 301 call extend(s:messages, s:errors_expected) 302 endif 303 304 " Add SKIPPED messages 305 call extend(s:messages, s:skipped) 306 307 " Append messages to the file "messages" 308 split messages 309 call append(line('$'), '') 310 call append(line('$'), 'From ' . g:testname . ':') 311 call append(line('$'), s:messages) 312 write 313 314 qall! 315endfunc 316 317" Source the test script. First grab the file name, in case the script 318" navigates away. g:testname can be used by the tests. 319let g:testname = expand('%') 320let s:done = 0 321let s:fail = 0 322let s:fail_expected = 0 323let s:errors = [] 324let s:errors_expected = [] 325let s:messages = [] 326let s:skipped = [] 327if expand('%') =~ 'test_vimscript.vim' 328 " this test has intentional errors, don't use try/catch. 329 source % 330else 331 try 332 source % 333 catch /^\cskipped/ 334 call add(s:messages, ' Skipped') 335 call add(s:skipped, 'SKIPPED ' . expand('%') . ': ' . substitute(v:exception, '^\S*\s\+', '', '')) 336 catch 337 let s:fail += 1 338 call add(s:errors, 'Caught exception: ' . v:exception . ' @ ' . v:throwpoint) 339 endtry 340endif 341 342" Names of flaky tests. 343let s:flaky_tests = [ 344 \ 'Test_autocmd_SafeState()', 345 \ 'Test_client_server()', 346 \ 'Test_close_and_exit_cb()', 347 \ 'Test_close_output_buffer()', 348 \ 'Test_collapse_buffers()', 349 \ 'Test_cwd()', 350 \ 'Test_diff_screen()', 351 \ 'Test_exit_callback_interval()', 352 \ 'Test_map_timeout_with_timer_interrupt()', 353 \ 'Test_out_cb()', 354 \ 'Test_pipe_through_sort_all()', 355 \ 'Test_pipe_through_sort_some()', 356 \ 'Test_popup_and_window_resize()', 357 \ 'Test_quoteplus()', 358 \ 'Test_quotestar()', 359 \ 'Test_reltime()', 360 \ 'Test_state()', 361 \ 'Test_terminal_composing_unicode()', 362 \ 'Test_terminal_does_not_truncate_last_newlines()', 363 \ 'Test_terminal_no_cmd()', 364 \ 'Test_terminal_noblock()', 365 \ 'Test_terminal_redir_file()', 366 \ 'Test_termwinscroll()', 367 \ 'Test_timer_oneshot()', 368 \ 'Test_timer_paused()', 369 \ 'Test_timer_repeat_many()', 370 \ 'Test_timer_repeat_three()', 371 \ 'Test_timer_stop_all_in_callback()', 372 \ 'Test_timer_stop_in_callback()', 373 \ 'Test_timer_with_partial_callback()', 374 \ ] 375 376" Locate Test_ functions and execute them. 377redir @q 378silent function /^Test_ 379redir END 380let s:tests = split(substitute(@q, '\(function\|def\) \(\k*()\)', '\2', 'g')) 381 382" If there is an extra argument filter the function names against it. 383if argc() > 1 384 let s:tests = filter(s:tests, 'v:val =~ argv(1)') 385endif 386 387" If the environment variable $TEST_FILTER is set then filter the function 388" names against it. 389let s:filtered = 0 390if $TEST_FILTER != '' 391 let s:filtered = len(s:tests) 392 let s:tests = filter(s:tests, 'v:val =~ $TEST_FILTER') 393 let s:filtered -= len(s:tests) 394endif 395 396let s:may_fail_list = [] 397if $TEST_MAY_FAIL != '' 398 " Split the list at commas and add () to make it match g:testfunc. 399 let s:may_fail_list = split($TEST_MAY_FAIL, ',')->map({i, v -> v .. '()'}) 400endif 401 402" Execute the tests in alphabetical order. 403for g:testfunc in sort(s:tests) 404 " Silence, please! 405 set belloff=all 406 let prev_error = '' 407 let total_errors = [] 408 let g:run_nr = 1 409 410 " A test can set g:test_is_flaky to retry running the test. 411 let g:test_is_flaky = 0 412 413 call RunTheTest(g:testfunc) 414 415 " Repeat a flaky test. Give up when: 416 " - it fails again with the same message 417 " - it fails five times (with a different message) 418 if len(v:errors) > 0 419 \ && (index(s:flaky_tests, g:testfunc) >= 0 420 \ || g:test_is_flaky) 421 while 1 422 call add(s:messages, 'Found errors in ' . g:testfunc . ':') 423 call extend(s:messages, v:errors) 424 425 call add(total_errors, 'Run ' . g:run_nr . ':') 426 call extend(total_errors, v:errors) 427 428 if g:run_nr == 5 || prev_error == v:errors[0] 429 call add(total_errors, 'Flaky test failed too often, giving up') 430 let v:errors = total_errors 431 break 432 endif 433 434 call add(s:messages, 'Flaky test failed, running it again') 435 436 " Flakiness is often caused by the system being very busy. Sleep a 437 " couple of seconds to have a higher chance of succeeding the second 438 " time. 439 sleep 2 440 441 let prev_error = v:errors[0] 442 let v:errors = [] 443 let g:run_nr += 1 444 445 call RunTheTest(g:testfunc) 446 447 if len(v:errors) == 0 448 " Test passed on rerun. 449 break 450 endif 451 endwhile 452 endif 453 454 call AfterTheTest(g:testfunc) 455endfor 456 457call FinishTesting() 458 459" vim: shiftwidth=2 sts=2 expandtab 460