runtest.vim (20635B)
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 " If the environment variable $TEST_SKIP_PAT is set then test functions 17 " matching this pattern will be skipped. It's the opposite of $TEST_FILTER. 18 " 19 " While working on a test you can make $TEST_NO_RETRY non-empty to not retry: 20 " export TEST_NO_RETRY=yes 21 " 22 " To ignore failure for tests that are known to fail in a certain environment, 23 " set $TEST_MAY_FAIL to a comma separated list of function names. E.g. for 24 " sh/bash: 25 " export TEST_MAY_FAIL=Test_channel_one,Test_channel_other 26 " The failure report will then not be included in the test.log file and 27 " "make test" will not fail. 28 " 29 " The test script may contain anything, only functions that start with 30 " "Test_" are special. These will be invoked and should contain assert 31 " functions. See test_assert.vim for an example. 32 " 33 " It is possible to source other files that contain "Test_" functions. This 34 " can speed up testing, since Vim does not need to restart. But be careful 35 " that the tests do not interfere with each other. 36 " 37 " If an error cannot be detected properly with an assert function add the 38 " error to the v:errors list: 39 " call add(v:errors, 'test foo failed: Cannot find xyz') 40 " 41 " If preparation for each Test_ function is needed, define a SetUp function. 42 " It will be called before each Test_ function. 43 " 44 " If cleanup after each Test_ function is needed, define a TearDown function. 45 " It will be called after each Test_ function. 46 " 47 " When debugging a test it can be useful to add messages to v:errors: 48 " call add(v:errors, "this happened") 49 50 51 " Without the +eval feature we can't run these tests, bail out. 52 silent! while 0 53 qa! 54 silent! endwhile 55 56 " In the GUI we can always change the screen size. 57 if has('gui_running') 58 if has('gui_gtk') 59 " to keep screendump size unchanged 60 set guifont=Monospace\ 10 61 endif 62 set columns=80 lines=25 63 endif 64 65 " Check that the screen size is at least 24 x 80 characters. 66 if &lines < 24 || &columns < 80 67 let error = 'Screen size too small! Tests require at least 24 lines with 80 characters, got ' .. &lines .. ' lines with ' .. &columns .. ' characters' 68 echoerr error 69 split test.log 70 $put =error 71 write 72 split messages 73 call append(line('$'), '') 74 call append(line('$'), 'From ' . expand('%') . ':') 75 call append(line('$'), error) 76 write 77 qa! 78 endif 79 80 if has('reltime') 81 let s:run_start_time = reltime() 82 83 if !filereadable('starttime') 84 " first test, store the overall test starting time 85 let s:test_start_time = localtime() 86 call writefile([string(s:test_start_time)], 'starttime') 87 else 88 " second or later test, read the overall test starting time 89 let s:test_start_time = readfile('starttime')[0]->str2nr() 90 endif 91 endif 92 93 " Always use forward slashes. 94 set shellslash 95 96 " Common with all tests on all systems. 97 source setup.vim 98 99 " Needed for RunningWithValgrind(). 100 source shared.vim 101 102 " Needed for the various Check commands 103 source check.vim 104 105 " For consistency run all tests with 'nocompatible' set. 106 " This also enables use of line continuation. 107 set nocp viminfo+=nviminfo 108 109 " Use utf-8 by default, instead of whatever the system default happens to be. 110 " Individual tests can overrule this at the top of the file and use 111 " g:orig_encoding if needed. 112 let g:orig_encoding = &encoding 113 set encoding=utf-8 114 115 " REDIR_TEST_TO_NULL has a very permissive SwapExists autocommand which is for 116 " the test_name.vim file itself. Replace it here with a more restrictive one, 117 " so we still catch mistakes. 118 if has("win32") 119 " replace any '/' directory separators by '\\' 120 let s:test_script_fname = substitute(expand('%'), '/', '\\', 'g') 121 else 122 let s:test_script_fname = expand('%') 123 endif 124 au! SwapExists * call HandleSwapExists() 125 func HandleSwapExists() 126 if exists('g:ignoreSwapExists') 127 if type(g:ignoreSwapExists) == v:t_string 128 let v:swapchoice = g:ignoreSwapExists 129 endif 130 return 131 endif 132 " Ignore finding a swap file for the test script (the user might be 133 " editing it and do ":make test_name") and the output file. 134 " Report finding another swap file and chose 'q' to avoid getting stuck. 135 if expand('<afile>') == 'messages' || expand('<afile>') =~ s:test_script_fname 136 let v:swapchoice = 'e' 137 else 138 call assert_report('Unexpected swap file: ' .. v:swapname) 139 let v:swapchoice = 'q' 140 endif 141 endfunc 142 143 " Avoid stopping at the "hit enter" prompt 144 set nomore 145 146 " Output all messages in English. 147 lang mess C 148 149 " Nvim: append runtime from build dir, which contains the generated doc/tags. 150 let &runtimepath ..= ',' .. expand($BUILD_DIR) .. '/runtime/' 151 " Nvim: append libdir from build dir, which contains the bundled TS parsers. 152 let &runtimepath ..= ',' .. expand($BUILD_DIR) .. '/lib/nvim/' 153 154 let s:t_bold = &t_md 155 let s:t_normal = &t_me 156 if has('win32') 157 " avoid prompt that is long or contains a line break 158 let $PROMPT = '$P$G' 159 endif 160 161 if has('mac') 162 " In macOS, when starting a shell in a terminal, a bash deprecation warning 163 " message is displayed. This breaks the terminal test. Disable the warning 164 " message. 165 let $BASH_SILENCE_DEPRECATION_WARNING = 1 166 endif 167 168 169 " Prepare for calling test_garbagecollect_now(). 170 " Also avoids some delays in Insert mode completion. 171 let v:testing = 1 172 173 let s:has_ffi = luaeval('pcall(require, "ffi")') 174 if s:has_ffi 175 lua << trim EOF 176 require('ffi').cdef([[ 177 int starting; 178 bool test_disable_char_avail; 179 ]]) 180 EOF 181 endif 182 183 " This can emulate test_override('starting') and test_override('char_avail') 184 " if LuaJIT FFI is enabled. 185 " Other flags are not supported. 186 func Ntest_override(name, val) 187 if a:name !=# 'starting' && a:name != 'char_avail' && a:name !=# 'ALL' 188 throw 'Unexpected use of Ntest_override()' 189 endif 190 if !s:has_ffi 191 throw 'Skipped: missing LuaJIT FFI' 192 endif 193 194 if a:name ==# 'starting' || a:name ==# 'ALL' 195 if a:val 196 if !exists('s:save_starting') 197 let s:save_starting = luaeval('require("ffi").C.starting') 198 endif 199 lua require("ffi").C.starting = 0 200 elseif exists('s:save_starting') 201 exe 'lua require("ffi").C.starting =' s:save_starting 202 unlet s:save_starting 203 endif 204 endif 205 206 if a:name ==# 'char_avail' || a:name ==# 'ALL' 207 exe 'lua require("ffi").C.test_disable_char_avail =' a:val 208 endif 209 endfunc 210 211 " roughly equivalent to test_setmouse() in Vim 212 func Ntest_setmouse(row, col) 213 call nvim_input_mouse('move', '', '', 0, a:row - 1, a:col - 1) 214 if state('m') == '' 215 call getchar(0) 216 endif 217 endfunc 218 219 " roughly equivalent to term_wait() in Vim 220 func Nterm_wait(buf, time = 10) 221 execute $'sleep {a:time}m' 222 endfunc 223 224 " Support function: get the alloc ID by name. 225 func GetAllocId(name) 226 exe 'split ' . s:srcdir . '/alloc.h' 227 let top = search('typedef enum') 228 if top == 0 229 call add(v:errors, 'typedef not found in alloc.h') 230 endif 231 let lnum = search('aid_' . a:name . ',') 232 if lnum == 0 233 call add(v:errors, 'Alloc ID ' . a:name . ' not defined') 234 endif 235 close 236 return lnum - top - 1 237 endfunc 238 239 " Get the list of swap files in the current directory. 240 func s:GetSwapFileList() 241 let save_dir = &directory 242 let &directory = '.' 243 let files = swapfilelist() 244 let &directory = save_dir 245 246 " remove a match with runtest.vim 247 let idx = indexof(files, 'v:val =~ "runtest.vim."') 248 if idx >= 0 249 call remove(files, idx) 250 endif 251 252 return files 253 endfunc 254 255 " A previous (failed) test run may have left swap files behind. Delete them 256 " before running tests again, they might interfere. 257 for name in s:GetSwapFileList() 258 call delete(name) 259 endfor 260 unlet! name 261 262 263 " Invoked when a test takes too much time. 264 func TestTimeout(id) 265 split test.log 266 call append(line('$'), '') 267 268 let text = 'Test timed out: ' .. g:testfunc 269 if g:timeout_start > 0 270 let text ..= strftime(' after %s seconds', localtime() - g:timeout_start) 271 endif 272 call append(line('$'), text) 273 write 274 call add(v:errors, text) 275 276 cquit! 42 277 endfunc 278 let g:timeout_start = 0 279 280 func RunTheTest(test) 281 let prefix = '' 282 if has('reltime') 283 let prefix = strftime('%M:%S', localtime() - s:test_start_time) .. ' ' 284 let g:func_start = reltime() 285 endif 286 echo prefix .. 'Executing ' .. a:test 287 288 if has('timers') 289 " No test should take longer than 45 seconds. If it takes longer we 290 " assume we are stuck and need to break out. 291 let test_timeout_timer = 292 \ timer_start(RunningWithValgrind() ? 90000 : 45000, 'TestTimeout') 293 let g:timeout_start = localtime() 294 endif 295 296 " Avoid stopping at the "hit enter" prompt 297 set nomore 298 299 " Avoid a three second wait when a message is about to be overwritten by the 300 " mode message. 301 set noshowmode 302 303 " Some tests wipe out buffers. To be consistent, always wipe out all 304 " buffers. 305 %bwipe! 306 307 " The test may change the current directory. Save and restore the 308 " directory after executing the test. 309 let save_cwd = getcwd() 310 311 " Align Nvim defaults to Vim. 312 source setup.vim 313 314 if exists("*SetUp") 315 try 316 call SetUp() 317 catch 318 call add(v:errors, 'Caught exception in SetUp() before ' . a:test . ': ' . v:exception . ' @ ' . v:throwpoint) 319 endtry 320 endif 321 322 let skipped = v:false 323 324 au VimLeavePre * call EarlyExit(g:testfunc) 325 if a:test =~ 'Test_nocatch_' 326 " Function handles errors itself. This avoids skipping commands after the 327 " error. 328 let g:skipped_reason = '' 329 exe 'call ' . a:test 330 if g:skipped_reason != '' 331 call add(s:messages, ' Skipped') 332 call add(s:skipped, 'SKIPPED ' . a:test . ': ' . g:skipped_reason) 333 let skipped = v:true 334 endif 335 elseif !s:has_ffi && execute('func ' .. a:test[:-3])->match("\n[ 0-9]*call Ntest_override(") >= 0 336 call add(s:messages, ' Skipped') 337 call add(s:skipped, 'SKIPPED ' . a:test . ': missing LuaJIT FFI' . ) 338 let skipped = v:true 339 else 340 try 341 exe 'call ' . a:test 342 catch /^\cskipped/ 343 call add(s:messages, ' Skipped') 344 call add(s:skipped, 'SKIPPED ' . a:test . ': ' . substitute(v:exception, '^\S*\s\+', '', '')) 345 let skipped = v:true 346 catch 347 call add(v:errors, 'Caught exception in ' . a:test . ': ' . v:exception . ' @ ' . v:throwpoint) 348 endtry 349 endif 350 au! VimLeavePre 351 352 if a:test =~ '_terminal_' 353 " Terminal tests sometimes hang, give extra information 354 echoconsole 'After executing ' .. a:test 355 endif 356 357 " In case 'insertmode' was set and something went wrong, make sure it is 358 " reset to avoid trouble with anything else. 359 set noinsertmode 360 361 if exists("*TearDown") 362 try 363 call TearDown() 364 catch 365 call add(v:errors, 'Caught exception in TearDown() after ' . a:test . ': ' . v:exception . ' @ ' . v:throwpoint) 366 endtry 367 endif 368 369 if has('timers') 370 call timer_stop(test_timeout_timer) 371 let g:timeout_start = 0 372 endif 373 374 " Clear any autocommands and put back the catch-all for SwapExists. 375 au! 376 au SwapExists * call HandleSwapExists() 377 378 " Close any stray popup windows 379 if has('popupwin') 380 call popup_clear() 381 endif 382 383 " Close any extra tab pages and windows and make the current one not modified. 384 while tabpagenr('$') > 1 385 let winid = win_getid() 386 quit! 387 if winid == win_getid() 388 echoerr 'Could not quit window' 389 break 390 endif 391 endwhile 392 393 while 1 394 let wincount = winnr('$') 395 if wincount == 1 396 break 397 endif 398 bwipe! 399 if wincount == winnr('$') 400 " Did not manage to close a window. 401 only! 402 break 403 endif 404 endwhile 405 406 exe 'cd ' . save_cwd 407 408 if a:test =~ '_terminal_' 409 " Terminal tests sometimes hang, give extra information 410 echoconsole 'Finished ' . a:test 411 endif 412 413 let message = 'Executed ' . a:test 414 if has('reltime') 415 let message ..= repeat(' ', 50 - len(message)) 416 let time = reltime(g:func_start) 417 if reltimefloat(time) > 0.1 418 let message = s:t_bold .. message 419 endif 420 let message ..= ' in ' .. reltimestr(time) .. ' seconds' 421 if reltimefloat(time) > 0.1 422 let message ..= s:t_normal 423 endif 424 endif 425 call add(s:messages, message) 426 let s:done += 1 427 428 " close any split windows 429 while winnr('$') > 1 430 noswapfile bwipe! 431 endwhile 432 433 " May be editing some buffer, wipe it out. Then we may end up in another 434 " buffer, continue until we end up in an empty no-name buffer without a swap 435 " file. 436 while bufname() != '' || execute('swapname') !~ 'No swap file' 437 let bn = bufnr() 438 439 noswapfile bwipe! 440 441 if bn == bufnr() 442 " avoid getting stuck in the same buffer 443 break 444 endif 445 endwhile 446 447 if !skipped 448 " Check if the test has left any swap files behind. Delete them before 449 " running tests again, they might interfere. 450 let swapfiles = s:GetSwapFileList() 451 if len(swapfiles) > 0 452 call add(s:messages, "Found swap files: " .. string(swapfiles)) 453 for name in swapfiles 454 call delete(name) 455 endfor 456 endif 457 endif 458 endfunc 459 460 function Delete_Xtest_Files() 461 for file in glob('X*', v:false, v:true) 462 if file ==? 'XfakeHOME' 463 " Clean up files created by setup.vim 464 call delete('XfakeHOME', 'rf') 465 continue 466 endif 467 " call add(v:errors, file .. " exists when it shouldn't, trying to delete it!") 468 call delete(file) 469 if !empty(glob(file, v:false, v:true)) 470 " call add(v:errors, file .. " still exists after trying to delete it!") 471 if has('unix') 472 call system('rm -rf ' .. file) 473 endif 474 endif 475 endfor 476 endfunc 477 478 func AfterTheTest(func_name) 479 if len(v:errors) > 0 480 if match(s:may_fail_list, '^' .. a:func_name) >= 0 481 let s:fail_expected += 1 482 call add(s:errors_expected, 'Found errors in ' . g:testfunc . ':') 483 call extend(s:errors_expected, v:errors) 484 else 485 let s:fail += 1 486 call add(s:errors, 'Found errors in ' . g:testfunc . ':') 487 call extend(s:errors, v:errors) 488 endif 489 let v:errors = [] 490 endif 491 endfunc 492 493 func EarlyExit(test) 494 " It's OK for the test we use to test the quit detection. 495 if a:test != 'Test_zz_quit_detected()' 496 call add(v:errors, v:errmsg) 497 call add(v:errors, 'Test caused Vim to exit: ' . a:test) 498 endif 499 500 call FinishTesting() 501 endfunc 502 503 " This function can be called by a test if it wants to abort testing. 504 func FinishTesting() 505 call AfterTheTest('') 506 call Delete_Xtest_Files() 507 508 " Don't write viminfo on exit. 509 set viminfo= 510 511 if s:fail == 0 && s:fail_expected == 0 512 " Success, create the .res file so that make knows it's done. 513 exe 'split ' . fnamemodify(g:testname, ':r') . '.res' 514 write 515 endif 516 517 if len(s:errors) > 0 518 " Append errors to test.log 519 split test.log 520 call append(line('$'), '') 521 call append(line('$'), 'From ' . g:testname . ':') 522 call append(line('$'), s:errors) 523 write 524 endif 525 526 if s:done == 0 527 if s:filtered > 0 528 if $TEST_FILTER != '' 529 let message = "NO tests match $TEST_FILTER: '" .. $TEST_FILTER .. "'" 530 else 531 let message = "ALL tests match $TEST_SKIP_PAT: '" .. $TEST_SKIP_PAT .. "'" 532 endif 533 else 534 let message = 'NO tests executed' 535 endif 536 else 537 if s:filtered > 0 538 call add(s:messages, "Filtered " .. s:filtered .. " tests with $TEST_FILTER and $TEST_SKIP_PAT") 539 endif 540 let message = 'Executed ' . s:done . (s:done > 1 ? ' tests' : ' test') 541 endif 542 if s:done > 0 && has('reltime') 543 let message = s:t_bold .. message .. repeat(' ', 40 - len(message)) 544 let message ..= ' in ' .. reltimestr(reltime(s:run_start_time)) .. ' seconds' 545 let message ..= s:t_normal 546 endif 547 echo message 548 call add(s:messages, message) 549 if s:fail > 0 550 let message = s:fail . ' FAILED:' 551 echo message 552 call add(s:messages, message) 553 call extend(s:messages, s:errors) 554 endif 555 if s:fail_expected > 0 556 let message = s:fail_expected . ' FAILED (matching $TEST_MAY_FAIL):' 557 echo message 558 call add(s:messages, message) 559 call extend(s:messages, s:errors_expected) 560 endif 561 562 " Add SKIPPED messages 563 call extend(s:messages, s:skipped) 564 565 " Append messages to the file "messages" 566 split messages 567 call append(line('$'), '') 568 call append(line('$'), 'From ' . g:testname . ':') 569 call append(line('$'), s:messages) 570 write 571 572 qall! 573 endfunc 574 575 " Source the test script. First grab the file name, in case the script 576 " navigates away. g:testname can be used by the tests. 577 let g:testname = expand('%') 578 let s:done = 0 579 let s:fail = 0 580 let s:fail_expected = 0 581 let s:errors = [] 582 let s:errors_expected = [] 583 let s:messages = [] 584 let s:skipped = [] 585 if expand('%') =~ 'test_vimscript.vim' 586 " this test has intentional errors, don't use try/catch. 587 source % 588 else 589 try 590 source % 591 catch /^\cskipped/ 592 call add(s:messages, ' Skipped') 593 call add(s:skipped, 'SKIPPED ' . expand('%') . ': ' . substitute(v:exception, '^\S*\s\+', '', '')) 594 catch 595 let s:fail += 1 596 call add(s:errors, 'Caught exception: ' . v:exception . ' @ ' . v:throwpoint) 597 endtry 598 endif 599 600 " Delete the .res file, it may change behavior for completion 601 call delete(fnamemodify(g:testname, ':r') .. '.res') 602 603 " Locate Test_ functions and execute them. 604 redir @q 605 silent function /^Test_ 606 redir END 607 let s:tests = split(substitute(@q, 'function \(\k*()\)', '\1', 'g')) 608 609 " If there is an extra argument filter the function names against it. 610 if argc() > 1 611 let s:tests = filter(s:tests, 'v:val =~ argv(1)') 612 endif 613 614 " If the environment variable $TEST_FILTER is set then filter the function 615 " names against it. 616 let s:filtered = 0 617 if $TEST_FILTER != '' 618 let s:filtered = len(s:tests) 619 let s:tests = filter(s:tests, 'v:val =~ $TEST_FILTER') 620 let s:filtered -= len(s:tests) 621 endif 622 623 let s:may_fail_list = [] 624 if $TEST_MAY_FAIL != '' 625 " Split the list at commas and add () to make it match g:testfunc. 626 let s:may_fail_list = split($TEST_MAY_FAIL, ',')->map({i, v -> v .. '()'}) 627 endif 628 629 " Execute the tests in alphabetical order. 630 for g:testfunc in sort(s:tests) 631 if $TEST_SKIP_PAT != '' && g:testfunc =~ $TEST_SKIP_PAT 632 call add(s:messages, g:testfunc .. ' matches $TEST_SKIP_PAT') 633 let s:filtered += 1 634 continue 635 endif 636 637 " Silence, please! 638 set belloff=all 639 let prev_error = '' 640 let total_errors = [] 641 let g:run_nr = 1 642 643 " A test can set g:test_is_flaky to retry running the test. 644 let g:test_is_flaky = 0 645 646 let g:check_screendump_called = v:false 647 648 " A test can set g:max_run_nr to change the max retry count. 649 let g:max_run_nr = 5 650 if has('mac') 651 let g:max_run_nr = 10 652 endif 653 654 " By default, give up if the same error occurs. A test can set 655 " g:giveup_same_error to 0 to not give up on the same error and keep trying. 656 let g:giveup_same_error = 1 657 658 let starttime = strftime("%H:%M:%S") 659 call RunTheTest(g:testfunc) 660 661 " Repeat a flaky test. Give up when: 662 " - $TEST_NO_RETRY is not empty 663 " - it fails again with the same message 664 " - it fails five times (with a different message) 665 if len(v:errors) > 0 666 \ && $TEST_NO_RETRY == '' 667 \ && g:test_is_flaky 668 while 1 669 call add(s:messages, 'Found errors in ' .. g:testfunc .. ':') 670 call extend(s:messages, v:errors) 671 672 let endtime = strftime("%H:%M:%S") 673 if has('reltime') 674 let suffix = $' in{reltimestr(reltime(g:func_start))} seconds' 675 else 676 let suffix = '' 677 endif 678 call add(total_errors, $'Run {g:run_nr}, {starttime} - {endtime}{suffix}:') 679 call extend(total_errors, v:errors) 680 681 if g:run_nr >= g:max_run_nr || g:giveup_same_error && prev_error == v:errors[0] 682 call add(total_errors, 'Flaky test failed too often, giving up') 683 let v:errors = total_errors 684 break 685 endif 686 687 call add(s:messages, 'Flaky test failed, running it again') 688 689 " Flakiness is often caused by the system being very busy. Sleep a 690 " couple of seconds to have a higher chance of succeeding the second 691 " time. 692 let delay = g:run_nr * 2 693 exe 'sleep' delay 694 695 let prev_error = v:errors[0] 696 let v:errors = [] 697 let g:run_nr += 1 698 699 let starttime = strftime("%H:%M:%S") 700 call RunTheTest(g:testfunc) 701 702 if len(v:errors) == 0 703 " Test passed on rerun. 704 break 705 endif 706 endwhile 707 endif 708 709 call AfterTheTest(g:testfunc) 710 endfor 711 712 call FinishTesting() 713 714 " vim: shiftwidth=2 sts=2 expandtab