" Runs all the syntax tests for which there is no "done/name" file. " " Current directory must be runtime/syntax. " needed because of line-continuation lines set cpo&vim " Only do this with the +eval feature if 1 " Remember the directory where we started. Will change to "testdir" below. let syntaxDir = getcwd() let s:messagesFname = fnameescape(syntaxDir .. '/testdir/messages') let s:messages = [] " Erase the cursor line and do not advance the cursor. (Call the function " after each passing test report.) def EraseLineAndReturnCarriage(line: string) const full_width: number = winwidth(0) const half_width: number = full_width - (full_width + 1) / 2 if strlen(line) > half_width echon "\r" .. repeat("\x20", full_width) .. "\r" else echon repeat("\x20", half_width) .. "\r" endif enddef " Add one message to the list of messages func Message(msg) echomsg a:msg call add(s:messages, a:msg) endfunc " Report a fatal message and exit func Fatal(msg) echoerr a:msg call AppendMessages(a:msg) qall! endfunc " Append s:messages to the messages file and make it empty. func AppendMessages(header) silent exe 'split ' .. s:messagesFname call append(line('$'), '') call append(line('$'), a:header) call append(line('$'), s:messages) let s:messages = [] silent wq endfunc " Relevant messages are written to the "messages" file. " If the file already exists it is appended to. silent exe 'split ' .. s:messagesFname call append(line('$'), repeat('=-', 70)) call append(line('$'), '') let s:test_run_message = 'Test run on ' .. strftime("%Y %b %d %H:%M:%S") call append(line('$'), s:test_run_message) silent wq if syntaxDir !~ '[/\\]runtime[/\\]syntax\>' call Fatal('Current directory must be "runtime/syntax"') endif if !isdirectory('testdir') call Fatal('"testdir" directory not found') endif " Use the script for source code screendump testing. It sources other scripts, " therefore we must "cd" there. cd ../../src/testdir source screendump.vim exe 'cd ' .. fnameescape(syntaxDir) " For these tests we need to be able to run terminal Vim with 256 colors. On " MS-Windows the console only has 16 colors and the GUI can't run in a " terminal. if !CanRunVimInTerminal() call Fatal('Cannot make screendumps, aborting') endif cd testdir if !isdirectory('done') call mkdir('done') endif if !isdirectory('failed') call mkdir('failed') endif set nocp set nowrapscan set report=9999 set modeline set debug=throw set nomore au! SwapExists * call HandleSwapExists() func HandleSwapExists() " Ignore finding a swap file for the test input, the user might be editing " it and that's OK. if expand('') =~ 'input[/\\].*\..*' let v:swapchoice = 'e' endif endfunc " Trace ruler liveness on demand. if !empty($VIM_SYNTAX_TEST_LOG) && filewritable($VIM_SYNTAX_TEST_LOG) def s:TraceRulerLiveness(context: string, times: number, tail: string) writefile([printf('%s: %4d: %s', context, times, tail)], $VIM_SYNTAX_TEST_LOG, 'a') enddef else def s:TraceRulerLiveness(_: string, _: number, _: string) enddef endif " See ":help 'ruler'". def s:CannotSeeLastLine(ruler: list): bool return !(get(ruler, -1, '') ==# 'All' || get(ruler, -1, '') ==# 'Bot') enddef def s:CannotDumpNextPage(buf: number, prev_ruler: list, ruler: list): bool return !(ruler !=# prev_ruler && len(ruler) == 2 && ruler[1] =~# '\%(\d%\|\, ruler: list): bool return !(len(ruler) == 2 && ruler[1] =~# '\%(\, ruler: list): bool return !(len(ruler) > 3 && get(ruler, -1, '') =~# '\%(\ # Expect defaults from "term_util#RunVimInTerminal()". if winwidth(1) != 75 || winheight(1) != 20 ch_log(printf('Aborting for %s: (75 x 20) != (%d x %d)', in_name_and_out_name, winwidth(1), winheight(1))) return ['0,0-1', 'All'] endif # A two-fold role for redrawing: # (*) in case the terminal buffer cannot redraw itself just yet; # (*) to avoid extra "real estate" checks. redraw # The contents of "ruler". var ruler: list = [] # Attempts at most, targeting ASan-instrumented Vim builds. var times: number = 2048 # Check "real estate" of the terminal buffer. Read and compare its ruler # line and let "Xtestscript#s:AssertCursorForwardProgress()" do the rest. # Note that the cursor ought to be advanced after each successive call of # this function yet its relative position need not be changed (e.g. "0%"). while CannotDumpPage(ruler) && times > 0 ruler = split(term_getline(buf, 20)) sleep 1m times -= 1 if times % 8 == 0 redraw endif endwhile TraceRulerLiveness('P', (2048 - times), in_name_and_out_name) return ruler enddef " Prevent "s:TermPollRuler()" from prematurely reading the cursor position, " which is available at ":edit", after outracing the loading of syntax etc. in " the terminal buffer. (Call the function before calling "VerifyScreenDump()" " for the first time.) def s:TermWaitAndPollRuler(buf: number, in_name_and_out_name: string): list # Expect defaults from "term_util#RunVimInTerminal()". if winwidth(1) != 75 || winheight(1) != 20 ch_log(printf('Aborting for %s: (75 x 20) != (%d x %d)', in_name_and_out_name, winwidth(1), winheight(1))) return ['0,0-1', 'All'] endif # The contents of "ruler". var ruler: string = '' # Attempts at most, targeting ASan-instrumented Vim builds. var times: number = 32768 # Check "real estate" of the terminal buffer. Expect a known token to be # rendered in the terminal buffer; its prefix must be "is_" so that buffer # variables from "sh.vim" can be matched (see "Xtestscript#ShellInfo()"). # Verify that the whole line is available! while ruler !~# '^is_.\+\s\%(All\|Top\)$' && times > 0 ruler = term_getline(buf, 20) sleep 1m times -= 1 if times % 16 == 0 redraw endif endwhile TraceRulerLiveness('W', (32768 - times), in_name_and_out_name) if strpart(ruler, 0, 8) !=# 'is_nonce' # Retain any of "b:is_(bash|dash|kornshell|posix|sh)" entries and let # "CannotDumpShellFirstPage()" win the cursor race. return TermPollRuler( function(CannotDumpShellFirstPage, [buf, []]), buf, in_name_and_out_name) else # Clear the "is_nonce" token and let "CannotDumpFirstPage()" win any # race. term_sendkeys(buf, ":redraw!\") endif return TermPollRuler( function(CannotDumpFirstPage, [buf, []]), buf, in_name_and_out_name) enddef func RunTest() let XTESTSCRIPT =<< trim END " Track the cursor progress through a syntax test file so that any " degenerate input can be reported. Each file will have its own cursor. let s:cursor = 1 " extra info for shell variables func ShellInfo() let msg = '' for [key, val] in items(b:) if key =~ '^is_' let msg ..= key .. ': ' .. val .. ', ' endif endfor if msg != '' echomsg msg endif endfunc au! SwapExists * call HandleSwapExists() func HandleSwapExists() " Ignore finding a swap file for the test input, the user might be " editing it and that's OK. if expand('') =~ 'input[/\\].*\..*' let v:swapchoice = 'e' endif endfunc func LoadFiletype(type) for file in glob("ftplugin/" .. a:type .. "*.vim", 1, 1) exe "source " .. file endfor redraw! endfunc func SetUpVim() call cursor(1, 1) " Defend against rogue VIM_TEST_SETUP commands. for _ in range(20) let lnum = search('\C\', 'eW', 20) if lnum < 1 break endif exe substitute(getline(lnum), '\C.*\', '', '') endfor call cursor(1, 1) " BEGIN [runtime/defaults.vim] " Also, disable italic highlighting to avoid issues on some terminals. set display=lastline ruler scrolloff=5 t_ZH= t_ZR= syntax on " END [runtime/defaults.vim] redraw! endfunc def s:AssertCursorForwardProgress(): bool const curnum: number = line('.') if curnum <= cursor # Use "actions/upload-artifact@v4" of ci.yml for delivery. writefile([printf('No cursor progress: %d <= %d (%s). Please file an issue.', curnum, cursor, bufname('%'))], 'failed/00-FIXME', 'a') bwipeout! endif cursor = curnum return true enddef def ScrollToSecondPage(estate: number, op_wh: number, op_so: number): bool if line('.') != 1 || line('w$') >= line('$') return AssertCursorForwardProgress() endif try set scrolloff=0 # Advance mark "c"[ursor] along with the cursor. norm! Lmc if foldclosed('.') < 0 && (strdisplaywidth(getline('.')) + &l:fdc * winheight(1)) >= estate # Make for an exit for a screenful long line. norm! j^ return AssertCursorForwardProgress() else # Place the cursor on the actually last visible line. while winline() < op_wh const lastnum: number = winline() norm! gjmc if lastnum > winline() break endif endwhile norm! zt endif finally # COMPATIBILITY: Scroll up around "scrolloff" lines. &scrolloff = max([1, op_so]) endtry norm! ^ return AssertCursorForwardProgress() enddef def ScrollToNextPage(estate: number, op_wh: number, op_so: number): bool if line('.') == 1 || line('w$') >= line('$') return AssertCursorForwardProgress() endif try set scrolloff=0 # Advance mark "c"[ursor] along with the cursor. norm! Lmc if foldclosed('.') < 0 && (strdisplaywidth(getline('.')) + &l:fdc * winheight(1)) >= estate # Make for an exit for a screenful long line. norm! j^ return AssertCursorForwardProgress() else # Place the cursor on the actually last visible line. while winline() < op_wh const lastnum: number = winline() norm! gjmc if lastnum > winline() break endif endwhile endif finally # COMPATIBILITY: Scroll up/down around "scrolloff" lines. &scrolloff = max([1, op_so]) endtry norm! zt const marknum: number = line("'c") # Eschew &smoothscroll since line("`c") is not supported. # Remember that "w0" can point to the first line of a _closed_ fold # whereas the last line of a _closed_ fold can be marked. if line('w0') > marknum while line('w0') > marknum exe "norm! \" endwhile if line('w0') != marknum exe "norm! \H" endif # Handle non-wrapped lines. elseif line('w0') < marknum while line('w0') < marknum exe "norm! \" endwhile if line('w0') != marknum exe "norm! \H" endif endif norm! ^ return AssertCursorForwardProgress() enddef END let MAX_FAILED_COUNT = 5 let DUMP_OPTS = exists("$VIM_SYNTAX_TEST_WAIT_TIME") && \ !empty($VIM_SYNTAX_TEST_WAIT_TIME) \ ? {'wait': max([1, str2nr($VIM_SYNTAX_TEST_WAIT_TIME)])} \ : {} lockvar DUMP_OPTS MAX_FAILED_COUNT XTESTSCRIPT let ok_count = 0 let failed_tests = [] let skipped_count = 0 let last_test_status = 'invalid' " Create a map of setup configuration filenames with their basenames as keys. let setup = glob('input/setup/*.vim', 1, 1) \ ->reduce({d, f -> extend(d, {fnamemodify(f, ':t:r'): f})}, {}) " Turn a subset of filenames etc. requested for testing into a pattern. let filter = filereadable('../testdir/Xfilter') \ ? readfile('../testdir/Xfilter') \ ->map({_, v -> '^' .. substitute(v, '_$', '', '')}) \ ->join('\|') \ : '' " Treat "^self-testing" as a string NOT as a regexp. if filter ==# '^self-testing' let dirpath = 'input/selftestdir/' let fnames = readdir(dirpath, {fname -> fname !~ '^README\.txt$'}) else let dirpath = 'input/' let filter ..= exists("$VIM_SYNTAX_TEST_FILTER") && \ !empty($VIM_SYNTAX_TEST_FILTER) \ ? (empty(filter) ? '' : '\|') .. $VIM_SYNTAX_TEST_FILTER \ : '' let fnames = readdir(dirpath, \ {subset -> {fname -> fname !~ '\~$' && fname =~# subset}}( \ empty(filter) ? '^.\+\..\+$' : filter)) endif for fname in fnames let root = fnamemodify(fname, ':r') let fname = dirpath .. fname " Execute the test if the "done" file does not exist or when the input file " is newer. let in_time = getftime(fname) let out_time = getftime('done/' .. root) if out_time < 0 || in_time > out_time call ch_log('running tests for: ' .. fname) let filetype = substitute(root, '\([^_.]*\)[_.].*', '\1', '') let failed_root = 'failed/' .. root for dumpname in glob(failed_root .. '_\d*\.dump', 1, 1) call delete(dumpname) endfor call delete('done/' .. root) call writefile(XTESTSCRIPT, 'Xtestscript') " close all but the last window while winnr('$') > 1 close endwhile " Redraw to make sure that messages are cleared and there is enough space " for the terminal window. redraw " Let "Xtestscript#SetUpVim()" turn the syntax on. let prefix = '-Nu NONE -S Xtestscript' let path = get(setup, root, '') " Source the found setup configuration file. let args = !empty(path) \ ? prefix .. ' -S ' .. path \ : prefix let buf = RunVimInTerminal(args, {}) " edit the file only after catching the SwapExists event call term_sendkeys(buf, ":edit " .. fname .. "\") " set up the testing environment call term_sendkeys(buf, ":call SetUpVim()\") " load filetype specific settings call term_sendkeys(buf, ":call LoadFiletype('" .. filetype .. "')\") " Make a synchronisation point between buffers by requesting to echo " a known token in the terminal buffer and asserting its availability " with "s:TermWaitAndPollRuler()". if filetype == 'sh' call term_sendkeys(buf, ":call ShellInfo()\") else call term_sendkeys(buf, ":echo 'is_nonce'\") endif let root_00 = root .. '_00' let in_name_and_out_name = fname .. ': failed/' .. root_00 .. '.dump' " Queue up all "term_sendkeys()"es and let them finish before returning " from "s:TermWaitAndPollRuler()". let ruler = s:TermWaitAndPollRuler(buf, in_name_and_out_name) call ch_log('First screendump for ' .. in_name_and_out_name) " Make a screendump at the start of the file: failed/root_00.dump let fail = VerifyScreenDump(buf, root_00, DUMP_OPTS) " Accommodate the next code block to "buf"'s contingency for self " wipe-out. try let nr = 0 let keys_a = ":call ScrollToSecondPage((18 * 75 + 1), 19, 5) | redraw!\" let keys_b = ":call ScrollToNextPage((18 * 75 + 1), 19, 5) | redraw!\" while s:CannotSeeLastLine(ruler) call term_sendkeys(buf, keys_a) let keys_a = keys_b let nr += 1 let root_next = printf('%s_%02d', root, nr) let in_name_and_out_name = fname .. ': failed/' .. root_next .. '.dump' let ruler = s:TermPollRuler( \ function('s:CannotDumpNextPage', [buf, ruler]), \ buf, \ in_name_and_out_name) call ch_log('Next screendump for ' .. in_name_and_out_name) " Make a screendump of every 18 lines of the file: failed/root_NN.dump let fail += VerifyScreenDump(buf, root_next, DUMP_OPTS) endwhile call StopVimInTerminal(buf) finally call delete('Xtestscript') endtry " redraw here to avoid the following messages to get mixed up with screen " output. redraw " Add any assert errors to s:messages. if len(v:errors) > 0 call extend(s:messages, v:errors) if last_test_status == 'passed' call EraseLineAndReturnCarriage('Test ' .. root .. ' OK') else echon "\n" endif " Echo the errors here, in case the script aborts or the "messages" file " is not displayed later. echomsg v:errors let v:errors = [] let fail += 1 endif if fail == 0 if last_test_status == 'skipped' echon "\n" endif let last_test_status = 'passed' let msg = "Test " .. root .. " OK" call Message(msg) call EraseLineAndReturnCarriage(msg) call writefile(['OK'], 'done/' .. root) let ok_count += 1 else let last_test_status = 'failed' call Message("Test " .. root .. " FAILED") echon "\n" call delete('done/' .. root) eval failed_tests->add(root) if len(failed_tests) > MAX_FAILED_COUNT call Message('') call Message('Too many errors, aborting') endif endif else if last_test_status == 'passed' call EraseLineAndReturnCarriage('Test ' .. root .. ' OK') endif let last_test_status = 'skipped' call Message("Test " .. root .. " skipped") let skipped_count += 1 endif " Append messages to the file "testdir/messages" call AppendMessages('Input file ' .. fname .. ':') if len(failed_tests) > MAX_FAILED_COUNT break endif endfor if last_test_status == 'passed' && exists('root') call EraseLineAndReturnCarriage('Test ' .. root .. ' OK') endif call Message(s:test_run_message) call Message('OK: ' .. ok_count) call Message('FAILED: ' .. len(failed_tests) .. ': ' .. string(failed_tests)) call Message('skipped: ' .. skipped_count) if !empty(failed_tests) call Message('') call Message('View generated screendumps with "../../src/vim --clean -S testdir/viewdumps.vim"') endif call AppendMessages('== SUMMARY SYNTAX TESTS ==') if len(failed_tests) > 0 " have make report an error cquit endif endfunc call RunTest() " Matching "if 1" at the start. endif qall! " vim:sw=2:ts=8:noet: