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