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