summaryrefslogtreecommitdiff
path: root/mnv/src/testdir/util/screendump.mnv
blob: 2f28c759a81aa9fd0aa064a1ce4848afbdc40b06 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
" Functions shared by tests making screen dumps.

" Only load this script once.
if exists('*VerifyScreenDump')
  finish
endif

" Skip the rest if there is no terminal feature at all.
if !has('terminal')
  finish
endif

" Read a dump file "fname" and if "filter" exists apply it to the text.
def ReadAndFilter(fname: string, filter: string): list<string>
  var contents = readfile(fname)

  if filereadable(filter)
    # do this in the bottom window so that the terminal window is unaffected
    wincmd j
    enew
    setline(1, contents)
    exe "source " .. filter
    contents = getline(1, '$')
    enew!
    wincmd k
    redraw
  endif

  return contents
enddef

" Accommodate rendering idiosyncrasies (see #16559).  For details, refer to
" "VerifyScreenDump()" and the "options" dictionary passed to it: this is
" an implementation of its "FileComparisonPreAction" entry.  (This function
" runs in couples with "g:ScreenDumpLookForFFFDChars()".)
def g:ScreenDumpDiscardFFFDChars(
	state: dict<number>,
	testdump: list<string>,
	refdump: list<string>)
  if empty(state) || len(testdump) != len(refdump)
    return
  endif
  for lstr: string in keys(state)
    const lnum: number = str2nr(lstr)
    const fst_fffd_idx: number = stridx(testdump[lnum], "\xef\xbf\xbd")
    # Retroactively discard non-equal line suffixes.  It is assumed that no
    # runs of U+EFU+BFU+BD and no U+FFFDs are present in "refdump".
    if fst_fffd_idx >= 0
      # Mask the "||" character cells and the cursor cell ">.".
      const masked_part: string = substitute(
	  substitute(
	      strpart(testdump[lnum], 0, (fst_fffd_idx - 1)),
	      '[>|]|', '|.', 'g'),
	  '|\@<!>', '|', 'g')
      const prev_cell_idx: number = strridx(masked_part, '|')
      # A series of repeated characters will be found recorded in shorthand;
      # e.g. "|α@3" stands for a cell of four "α"s.  Replacing any repeated
      # multibyte character of a series with a U+FFFD character will split the
      # series and its shorthand record will reflect this fact: "|α@2|�".
      # Therefore, a common prefix to share for two corresponding lines can
      # extend to either an ASCII character(s) cell before the leftmost U+FFFD
      # character cell; or, a last-but-one arbitrary cell before the leftmost
      # U+FFFD character cell; or, an empty string.
      const prefix: number = (prev_cell_idx >= 0)
	  ? (char2nr(strpart(masked_part, (prev_cell_idx + 1), 1), true) < 128)
	      ? fst_fffd_idx - 1
	      : (strridx(masked_part, '|', (prev_cell_idx - 1)) >= 0)
		  ? prev_cell_idx
		  : 0
	  : 0
      refdump[lnum] = strpart(refdump[lnum], 0, prefix)
      testdump[lnum] = strpart(testdump[lnum], 0, prefix)
    endif
  endfor
enddef

" Accommodate rendering idiosyncrasies (see #16559).  For details, refer to
" "VerifyScreenDump()" and the "options" dictionary passed to it: this is
" an implementation of its "NonEqualLineComparisonPostAction" entry.  (This
" function runs in couples with "g:ScreenDumpDiscardFFFDChars()".)
def g:ScreenDumpLookForFFFDChars(
	state: dict<number>,
	testdump: list<string>,
	lnum: number)
  if stridx(testdump[lnum], "\xef\xbf\xbd") >= 0
    state[string(lnum)] = 1
  endif
enddef

" Verify that MNV running in terminal buffer "buf" matches the screen dump.
"
" A copy of "options" is passed to "term_dumpwrite()".  For convenience, this
" dictionary supports other optional entries:
"   "wait", (default to 1000 msec at least)
"	the maximum time to wait for the screen dump to match in msec.
"   "FileComparisonPreAction", (default to a no-op action)
"	some Funcref to call, passing the following three arguments, each time
"	before the file contents of two screen dumps are compared:
"	    some dictionary with some state entries;
"	    the file contents of the newly generated screen dump;
"	    the file contents of the reference screen dump.
"   "NonEqualLineComparisonPostAction", (default to a no-op action)
"	some Funcref to call, passing the following three arguments, each time
"	after a corresponding pair of lines is found not equal:
"	    some dictionary with some state entries;
"	    the file contents of the newly generated screen dump;
"	    the zero-based number of the line whose copies are not equal.
"
" The file name used is "dumps/{filename}.dump".
"
" To ignore part of the dump, provide a "dumps/{filename}.mnv" file with
" MNV commands to be applied to both the reference and the current dump, so
" that parts that are irrelevant are not used for the comparison.  The result
" is NOT written, thus "term_dumpdiff()" shows the difference anyway.
"
" Optionally an extra argument can be passed which is prepended to the error
" message.  Use this when using the same dump file with different options.
" Returns non-zero when verification fails.
func VerifyScreenDump(buf, filename, options, ...)
  if has('gui_running') && exists("g:check_screendump_called") && g:check_screendump_called == v:false
    echoerr "VerifyScreenDump() called from a test that lacks a CheckScreendump guard."
    return 1
  endif
  let reference = 'dumps/' . a:filename . '.dump'
  let filter = 'dumps/' . a:filename . '.mnv'
  let testfile = 'failed/' . a:filename . '.dump'

  let options_copy = copy(a:options)
  if has_key(options_copy, 'wait')
    let max_loops = max([0, remove(options_copy, 'wait')])
  else
    let max_loops = 1000
  endif
  if has_key(options_copy, 'FileComparisonPreAction')
    let FileComparisonPreAction = remove(options_copy, 'FileComparisonPreAction')
    let CopyStringList = {_refdump -> copy(_refdump)}
  else
    let FileComparisonPreAction = {_state, _testdump, _refdump -> 0}
    let CopyStringList = {_refdump -> _refdump}
  endif
  if has_key(options_copy, 'NonEqualLineComparisonPostAction')
    let NonEqualLineComparisonPostAction = remove(options_copy, 'NonEqualLineComparisonPostAction')
  else
    let NonEqualLineComparisonPostAction = {_state, _testdump, _lnum -> 0}
  endif

  " Starting a terminal to make a screendump is always considered flaky.
  let g:test_is_flaky = 1
  let g:giveup_same_error = 0

  " wait for the pending updates to be handled.
  call TermWait(a:buf, 0)

  " Redraw to execute the code that updates the screen.  Otherwise we get the
  " text and attributes only from the internal buffer.
  redraw

  let did_mkdir = 0
  if !isdirectory('failed')
    let did_mkdir = 1
    call mkdir('failed')
  endif

  if !filereadable(reference)
    " Leave a bit of time for updating the original window while we spin wait.
    sleep 10m
    call delete(testfile)
    call term_dumpwrite(a:buf, testfile, options_copy)
    call assert_report('See new dump file: call term_dumpload("testdir/' .. testfile .. '")')
    " No point in retrying.
    let g:run_nr = 10
    return 1
  endif

  let refdump_orig = ReadAndFilter(reference, filter)
  let state = {}
  let i = 0
  while 1
    " Leave a bit of time for updating the original window while we spin wait.
    sleep 1m
    call delete(testfile)
    call term_dumpwrite(a:buf, testfile, options_copy)
    " Filtering done with "FileComparisonPreAction()" may change "refdump*".
    let refdump = CopyStringList(refdump_orig)
    let testdump = ReadAndFilter(testfile, filter)
    call FileComparisonPreAction(state, testdump, refdump)
    if refdump == testdump
      call delete(testfile)
      if did_mkdir
	call delete('failed', 'd')
      endif
      if i > 0
	call remove(v:errors, -1)
      endif
      break
    endif

    " Leave the failed dump around for inspection.
    let msg = 'See dump file difference: call term_dumpdiff("testdir/' .. testfile .. '", "testdir/' .. reference .. '")'
    if a:0 == 1
      let msg = a:1 . ': ' . msg
    endif
    if len(testdump) != len(refdump)
      let msg = msg . '; line count is ' . len(testdump) . ' instead of ' . len(refdump)
    endif
    for j in range(len(refdump))
      if j >= len(testdump)
	break
      endif
      if testdump[j] != refdump[j]
	let msg = msg . '; difference in line ' . (j + 1) . ': "' . testdump[j] . '"'
	call NonEqualLineComparisonPostAction(state, testdump, j)
      endif
    endfor

    " Always add the last error so that it is displayed on timeout.
    " See TestTimeout() in runtest.mnv.
    if i > 0
      call remove(v:errors, -1)
    endif
    call assert_report(msg)

    let i += 1
    if i >= max_loops
      return 1
    endif
  endwhile
  return 0
endfunc

" mnv:sw=2:ts=8:noet: