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