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 echo '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 exe 'call ' . a:test 200 else 201 try 202 au VimLeavePre * call EarlyExit(g:testfunc) 203 exe 'call ' . a:test 204 au! VimLeavePre 205 catch /^\cskipped/ 206 call add(s:messages, ' Skipped') 207 call add(s:skipped, 'SKIPPED ' . a:test . ': ' . substitute(v:exception, '^\S*\s\+', '', '')) 208 catch 209 call add(v:errors, 'Caught exception in ' . a:test . ': ' . v:exception . ' @ ' . v:throwpoint) 210 endtry 211 endif 212 213 " In case 'insertmode' was set and something went wrong, make sure it is 214 " reset to avoid trouble with anything else. 215 set noinsertmode 216 217 if exists("*TearDown") 218 try 219 call TearDown() 220 catch 221 call add(v:errors, 'Caught exception in TearDown() after ' . a:test . ': ' . v:exception . ' @ ' . v:throwpoint) 222 endtry 223 endif 224 225 " Clear any autocommands and put back the catch-all for SwapExists. 226 au! 227 au SwapExists * call HandleSwapExists() 228 229 " Check for and close any stray popup windows. 230 if has('popupwin') 231 call assert_equal([], popup_list()) 232 call popup_clear(1) 233 endif 234 235 " Close any extra tab pages and windows and make the current one not modified. 236 while tabpagenr('$') > 1 237 let winid = win_getid() 238 quit! 239 if winid == win_getid() 240 echoerr 'Could not quit window' 241 break 242 endif 243 endwhile 244 245 while 1 246 let wincount = winnr('$') 247 if wincount == 1 248 break 249 endif 250 bwipe! 251 if wincount == winnr('$') 252 " Did not manage to close a window. 253 only! 254 break 255 endif 256 endwhile 257 258 exe 'cd ' . save_cwd 259 260 let message = 'Executed ' . a:test 261 if has('reltime') 262 let message ..= repeat(' ', 50 - len(message)) 263 let time = reltime(func_start) 264 if has('float') && reltimefloat(time) > 0.1 265 let message = s:t_bold .. message 266 endif 267 let message ..= ' in ' .. reltimestr(time) .. ' seconds' 268 if has('float') && reltimefloat(time) > 0.1 269 let message ..= s:t_normal 270 endif 271 endif 272 call add(s:messages, message) 273 let s:done += 1 274endfunc 275 276func AfterTheTest(func_name) 277 if len(v:errors) > 0 278 if match(s:may_fail_list, '^' .. a:func_name) >= 0 279 let s:fail_expected += 1 280 call add(s:errors_expected, 'Found errors in ' . g:testfunc . ':') 281 call extend(s:errors_expected, v:errors) 282 else 283 let s:fail += 1 284 call add(s:errors, 'Found errors in ' . g:testfunc . ':') 285 call extend(s:errors, v:errors) 286 endif 287 let v:errors = [] 288 endif 289endfunc 290 291func EarlyExit(test) 292 " It's OK for the test we use to test the quit detection. 293 if a:test != 'Test_zz_quit_detected()' 294 call add(v:errors, 'Test caused Vim to exit: ' . a:test) 295 endif 296 297 call FinishTesting() 298endfunc 299 300" This function can be called by a test if it wants to abort testing. 301func FinishTesting() 302 call AfterTheTest('') 303 304 " Don't write viminfo on exit. 305 set viminfo= 306 307 " Clean up files created by setup.vim 308 call delete('XfakeHOME', 'rf') 309 310 if s:fail == 0 && s:fail_expected == 0 311 " Success, create the .res file so that make knows it's done. 312 exe 'split ' . fnamemodify(g:testname, ':r') . '.res' 313 write 314 endif 315 316 if len(s:errors) > 0 317 " Append errors to test.log 318 split test.log 319 call append(line('$'), '') 320 call append(line('$'), 'From ' . g:testname . ':') 321 call append(line('$'), s:errors) 322 write 323 endif 324 325 if s:done == 0 326 if s:filtered > 0 327 let message = "NO tests match $TEST_FILTER: '" .. $TEST_FILTER .. "'" 328 else 329 let message = 'NO tests executed' 330 endif 331 else 332 if s:filtered > 0 333 call add(s:messages, "Filtered " .. s:filtered .. " tests with $TEST_FILTER") 334 endif 335 let message = 'Executed ' . s:done . (s:done > 1 ? ' tests' : ' test') 336 endif 337 if s:done > 0 && has('reltime') 338 let message = s:t_bold .. message .. repeat(' ', 40 - len(message)) 339 let message ..= ' in ' .. reltimestr(reltime(s:start_time)) .. ' seconds' 340 let message ..= s:t_normal 341 endif 342 echo message 343 call add(s:messages, message) 344 if s:fail > 0 345 let message = s:fail . ' FAILED:' 346 echo message 347 call add(s:messages, message) 348 call extend(s:messages, s:errors) 349 endif 350 if s:fail_expected > 0 351 let message = s:fail_expected . ' FAILED (matching $TEST_MAY_FAIL):' 352 echo message 353 call add(s:messages, message) 354 call extend(s:messages, s:errors_expected) 355 endif 356 357 " Add SKIPPED messages 358 call extend(s:messages, s:skipped) 359 360 " Append messages to the file "messages" 361 split messages 362 call append(line('$'), '') 363 call append(line('$'), 'From ' . g:testname . ':') 364 call append(line('$'), s:messages) 365 write 366 367 qall! 368endfunc 369 370" Source the test script. First grab the file name, in case the script 371" navigates away. g:testname can be used by the tests. 372let g:testname = expand('%') 373let s:done = 0 374let s:fail = 0 375let s:fail_expected = 0 376let s:errors = [] 377let s:errors_expected = [] 378let s:messages = [] 379let s:skipped = [] 380if expand('%') =~ 'test_vimscript.vim' 381 " this test has intentional errors, don't use try/catch. 382 source % 383else 384 try 385 source % 386 catch /^\cskipped/ 387 call add(s:messages, ' Skipped') 388 call add(s:skipped, 'SKIPPED ' . expand('%') . ': ' . substitute(v:exception, '^\S*\s\+', '', '')) 389 catch 390 let s:fail += 1 391 call add(s:errors, 'Caught exception: ' . v:exception . ' @ ' . v:throwpoint) 392 endtry 393endif 394 395" Names of flaky tests. 396let s:flaky_tests = [ 397 \ 'Test_BufWrite_lockmarks()', 398 \ 'Test_autocmd_SafeState()', 399 \ 'Test_bufunload_all()', 400 \ 'Test_client_server()', 401 \ 'Test_close_and_exit_cb()', 402 \ 'Test_close_output_buffer()', 403 \ 'Test_collapse_buffers()', 404 \ 'Test_cwd()', 405 \ 'Test_diff_screen()', 406 \ 'Test_exit_callback_interval()', 407 \ 'Test_map_timeout_with_timer_interrupt()', 408 \ 'Test_out_cb()', 409 \ 'Test_pipe_through_sort_all()', 410 \ 'Test_pipe_through_sort_some()', 411 \ 'Test_popup_and_window_resize()', 412 \ 'Test_quoteplus()', 413 \ 'Test_quotestar()', 414 \ 'Test_reltime()', 415 \ 'Test_state()', 416 \ 'Test_terminal_composing_unicode()', 417 \ 'Test_terminal_does_not_truncate_last_newlines()', 418 \ 'Test_terminal_no_cmd()', 419 \ 'Test_terminal_noblock()', 420 \ 'Test_terminal_redir_file()', 421 \ 'Test_termwinscroll()', 422 \ 'Test_timer_oneshot()', 423 \ 'Test_timer_paused()', 424 \ 'Test_timer_repeat_many()', 425 \ 'Test_timer_repeat_three()', 426 \ 'Test_timer_stop_all_in_callback()', 427 \ 'Test_timer_stop_in_callback()', 428 \ 'Test_timer_with_partial_callback()', 429 \ ] 430 431" Locate Test_ functions and execute them. 432redir @q 433silent function /^Test_ 434redir END 435let s:tests = split(substitute(@q, '\(function\|def\) \(\k*()\)', '\2', 'g')) 436 437" If there is an extra argument filter the function names against it. 438if argc() > 1 439 let s:tests = filter(s:tests, 'v:val =~ argv(1)') 440endif 441 442" If the environment variable $TEST_FILTER is set then filter the function 443" names against it. 444let s:filtered = 0 445if $TEST_FILTER != '' 446 let s:filtered = len(s:tests) 447 let s:tests = filter(s:tests, 'v:val =~ $TEST_FILTER') 448 let s:filtered -= len(s:tests) 449endif 450 451let s:may_fail_list = [] 452if $TEST_MAY_FAIL != '' 453 " Split the list at commas and add () to make it match g:testfunc. 454 let s:may_fail_list = split($TEST_MAY_FAIL, ',')->map({i, v -> v .. '()'}) 455endif 456 457" Execute the tests in alphabetical order. 458for g:testfunc in sort(s:tests) 459 " Silence, please! 460 set belloff=all 461 let prev_error = '' 462 let total_errors = [] 463 let g:run_nr = 1 464 465 " A test can set g:test_is_flaky to retry running the test. 466 let g:test_is_flaky = 0 467 468 call RunTheTest(g:testfunc) 469 470 " Repeat a flaky test. Give up when: 471 " - $TEST_NO_RETRY is not empty 472 " - it fails again with the same message 473 " - it fails five times (with a different message) 474 if len(v:errors) > 0 475 \ && $TEST_NO_RETRY == '' 476 \ && (index(s:flaky_tests, g:testfunc) >= 0 477 \ || g:test_is_flaky) 478 while 1 479 call add(s:messages, 'Found errors in ' . g:testfunc . ':') 480 call extend(s:messages, v:errors) 481 482 call add(total_errors, 'Run ' . g:run_nr . ':') 483 call extend(total_errors, v:errors) 484 485 if g:run_nr == 5 || prev_error == v:errors[0] 486 call add(total_errors, 'Flaky test failed too often, giving up') 487 let v:errors = total_errors 488 break 489 endif 490 491 call add(s:messages, 'Flaky test failed, running it again') 492 493 " Flakiness is often caused by the system being very busy. Sleep a 494 " couple of seconds to have a higher chance of succeeding the second 495 " time. 496 sleep 2 497 498 let prev_error = v:errors[0] 499 let v:errors = [] 500 let g:run_nr += 1 501 502 call RunTheTest(g:testfunc) 503 504 if len(v:errors) == 0 505 " Test passed on rerun. 506 break 507 endif 508 endwhile 509 endif 510 511 call AfterTheTest(g:testfunc) 512endfor 513 514call FinishTesting() 515 516" vim: shiftwidth=2 sts=2 expandtab 517