]> Sergey Matveev's repositories - vim-lsp.git/commitdiff
Add a screen dump test for completion menu
authorYegappan Lakshmanan <yegappan@yahoo.com>
Wed, 22 Mar 2023 05:08:13 +0000 (22:08 -0700)
committerYegappan Lakshmanan <yegappan@yahoo.com>
Wed, 22 Mar 2023 05:08:13 +0000 (22:08 -0700)
test/clangd_tests.vim
test/common.vim [new file with mode: 0644]
test/runner.vim
test/screendump.vim [new file with mode: 0644]
test/start_tsserver.vim [new file with mode: 0644]
test/term_util.vim [new file with mode: 0644]
test/tsserver_tests.vim

index 3625923f1c19b4d449eda1c6e7ef047de277e737..e38d2339a3a05f42c094f7c46d94570b07a47fa0 100644 (file)
@@ -1,6 +1,8 @@
 vim9script
 # Unit tests for Vim Language Server Protocol (LSP) clangd client
 
+source common.vim
+
 var lspServers = [{
       filetype: ['c', 'cpp'],
       path: (exepath('clangd-14') ?? exepath('clangd')),
@@ -952,20 +954,17 @@ def g:Test_ScanFindIdent()
   bw!
 enddef
 
+# TODO:
+# 1. Add a test for autocompletion with a single match while ignoring case.
+#    After the full matched name is typed, the completion popup should still
+#    be displayed. e.g.
+#
+#      int MyVar = 1;
+#      int abc = myvar<C-N><C-Y>
+
 # Start the C language server.  Returns true on success and false on failure.
 def g:StartLangServer(): bool
-  # Edit a dummy C file to start the LSP server
-  :edit Xtest.c
-  # Wait for the LSP server to become ready (max 10 seconds)
-  var maxcount = 100
-  while maxcount > 0 && !g:LspServerReady()
-    :sleep 100m
-    maxcount -= 1
-  endwhile
-  var serverStatus: bool = g:LspServerReady()
-  :%bw!
-
-  return serverStatus
+  return g:StartLangServerWithFile('Xtest.c')
 enddef
 
 # vim: shiftwidth=2 softtabstop=2 noexpandtab
diff --git a/test/common.vim b/test/common.vim
new file mode 100644 (file)
index 0000000..5a3e9aa
--- /dev/null
@@ -0,0 +1,139 @@
+vim9script
+# Common routines used for running the unit tests
+
+# Load the LSP plugin.  Also enable syntax, file type detection.
+def g:LoadLspPlugin()
+  syntax on
+  filetype on
+  filetype plugin on
+  filetype indent on
+
+  # Set the $LSP_PROFILE environment variable to profile the LSP plugin
+  var do_profile: bool = false
+  if exists('$LSP_PROFILE')
+      do_profile = true
+  endif
+
+  if do_profile
+      # profile the LSP plugin
+      profile start lsp_profile.txt
+      profile! file */lsp/*
+  endif
+
+  source ../plugin/lsp.vim
+
+  g:LSPTest = true
+enddef
+
+# The WaitFor*() functions are reused from the Vim test suite.
+#
+# Wait for up to five seconds for "assert" to return zero.  "assert" must be a
+# (lambda) function containing one assert function.  Example:
+#      call WaitForAssert({-> assert_equal("dead", job_status(job)})
+#
+# A second argument can be used to specify a different timeout in msec.
+#
+# Return zero for success, one for failure (like the assert function).
+func g:WaitForAssert(assert, ...)
+  let timeout = get(a:000, 0, 5000)
+  if g:WaitForCommon(v:null, a:assert, timeout) < 0
+    return 1
+  endif
+  return 0
+endfunc
+
+# Either "expr" or "assert" is not v:null
+# Return the waiting time for success, -1 for failure.
+func g:WaitForCommon(expr, assert, timeout)
+  " using reltime() is more accurate, but not always available
+  let slept = 0
+  if exists('*reltimefloat')
+    let start = reltime()
+  endif
+
+  while 1
+    if type(a:expr) == v:t_func
+      let success = a:expr()
+    elseif type(a:assert) == v:t_func
+      let success = a:assert() == 0
+    else
+      let success = eval(a:expr)
+    endif
+    if success
+      return slept
+    endif
+
+    if slept >= a:timeout
+      break
+    endif
+    if type(a:assert) == v:t_func
+      " Remove the error added by the assert function.
+      call remove(v:errors, -1)
+    endif
+
+    sleep 10m
+    if exists('*reltimefloat')
+      let slept = float2nr(reltimefloat(reltime(start)) * 1000)
+    else
+      let slept += 10
+    endif
+  endwhile
+
+  return -1  " timed out
+endfunc
+
+# Wait for up to five seconds for "expr" to become true.  "expr" can be a
+# stringified expression to evaluate, or a funcref without arguments.
+# Using a lambda works best.  Example:
+#      call WaitFor({-> status == "ok"})
+#
+# A second argument can be used to specify a different timeout in msec.
+#
+# When successful the time slept is returned.
+# When running into the timeout an exception is thrown, thus the function does
+# not return.
+func g:WaitFor(expr, ...)
+  let timeout = get(a:000, 0, 5000)
+  let slept = g:WaitForCommon(a:expr, v:null, timeout)
+  if slept < 0
+    throw 'WaitFor() timed out after ' .. timeout .. ' msec'
+  endif
+  return slept
+endfunc
+
+# Wait for diagnostic messages from the LSP server
+def g:WaitForDiags(errCount: number)
+  var retries = 0
+  while retries < 150
+    var d = lsp#lsp#ErrorCount()
+    if d.Error == errCount
+      break
+    endif
+    retries += 1
+    :sleep 100m
+  endwhile
+enddef
+
+# Start the language server.  Returns true on success and false on failure.
+# 'fname' is the name of a dummy file to start the server.
+def g:StartLangServerWithFile(fname: string): bool
+  # Edit a dummy file to start the LSP server
+  exe ':silent! edit ' .. fname
+  # Wait for the LSP server to become ready (max 10 seconds)
+  var maxcount = 100
+  while maxcount > 0 && !g:LspServerReady()
+    :sleep 100m
+    maxcount -= 1
+  endwhile
+  var serverStatus: bool = g:LspServerReady()
+  :bw!
+
+  if !serverStatus
+    writefile(['FAIL: Not able to start the language server'], 'results.txt')
+    qall!
+  endif
+
+  return serverStatus
+enddef
+
+# vim: shiftwidth=2 softtabstop=2 noexpandtab
index 797b0d2074263ed7a5e9a81ff4f8b07ba83544ce..9593622380ef86726c04f25e3b9795e946755897 100644 (file)
@@ -3,96 +3,9 @@ vim9script
 # The global variable TestName should be set to the name of the file
 # containing the tests.
 
-syntax on
-filetype on
-filetype plugin on
-filetype indent on
+source common.vim
 
-# Set the $LSP_PROFILE environment variable to profile the LSP plugin
-var do_profile: bool = false
-if exists('$LSP_PROFILE')
-  do_profile = true
-endif
-
-if do_profile
-  # profile the LSP plugin
-  profile start lsp_profile.txt
-  profile! file */lsp/*
-endif
-
-source ../plugin/lsp.vim
-
-g:LSPTest = true
-
-# The WaitFor*() functions are reused from the Vim test suite.
-#
-# Wait for up to five seconds for "assert" to return zero.  "assert" must be a
-# (lambda) function containing one assert function.  Example:
-#      call WaitForAssert({-> assert_equal("dead", job_status(job)})
-#
-# A second argument can be used to specify a different timeout in msec.
-#
-# Return zero for success, one for failure (like the assert function).
-func g:WaitForAssert(assert, ...)
-  let timeout = get(a:000, 0, 5000)
-  if g:WaitForCommon(v:null, a:assert, timeout) < 0
-    return 1
-  endif
-  return 0
-endfunc
-
-# Either "expr" or "assert" is not v:null
-# Return the waiting time for success, -1 for failure.
-func g:WaitForCommon(expr, assert, timeout)
-  " using reltime() is more accurate, but not always available
-  let slept = 0
-  if exists('*reltimefloat')
-    let start = reltime()
-  endif
-
-  while 1
-    if type(a:expr) == v:t_func
-      let success = a:expr()
-    elseif type(a:assert) == v:t_func
-      let success = a:assert() == 0
-    else
-      let success = eval(a:expr)
-    endif
-    if success
-      return slept
-    endif
-
-    if slept >= a:timeout
-      break
-    endif
-    if type(a:assert) == v:t_func
-      " Remove the error added by the assert function.
-      call remove(v:errors, -1)
-    endif
-
-    sleep 10m
-    if exists('*reltimefloat')
-      let slept = float2nr(reltimefloat(reltime(start)) * 1000)
-    else
-      let slept += 10
-    endif
-  endwhile
-
-  return -1  " timed out
-endfunc
-
-# Wait for diagnostic messages from the LSP server
-def g:WaitForDiags(errCount: number)
-  var retries = 0
-  while retries < 150
-    var d = lsp#lsp#ErrorCount()
-    if d.Error == errCount
-      break
-    endif
-    retries += 1
-    :sleep 100m
-  endwhile
-enddef
+g:LoadLspPlugin()
 
 def LspRunTests()
   :set nomore
@@ -126,10 +39,7 @@ enddef
 
 exe 'source ' .. g:TestName
 
-if !g:StartLangServer()
-  writefile(['FAIL: Not able to start the language server'], 'results.txt')
-  qall!
-endif
+g:StartLangServer()
 
 LspRunTests()
 qall!
diff --git a/test/screendump.vim b/test/screendump.vim
new file mode 100644 (file)
index 0000000..68d3c3f
--- /dev/null
@@ -0,0 +1,117 @@
+" Functions shared by tests making screen dumps.
+
+source term_util.vim
+
+" 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
+
+
+" Verify that Vim running in terminal buffer "buf" matches the screen dump.
+" "options" is passed to term_dumpwrite().
+" Additionally, the "wait" entry can specify the maximum time to wait for the
+" screen dump to match in msec (default 1000 msec).
+" The file name used is "dumps/{filename}.dump".
+"
+" To ignore part of the dump, provide a "dumps/{filename}.vim" file with
+" Vim 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, ...)
+  let reference = 'dumps/' . a:filename . '.dump'
+  let filter = 'dumps/' . a:filename . '.vim'
+  let testfile = 'failed/' . a:filename . '.dump'
+
+  let max_loops = get(a:options, 'wait', 1000) / 10
+
+  " Starting a terminal to make a screendump is always considered flaky.
+  let g:test_is_flaky = 1
+
+  " wait for the pending updates to be handled.
+  call TermWait(a:buf)
+
+  " Redraw to execute the code that updates the screen.  Otherwise we get the
+  " text and attributes only from the internal buffer.
+  redraw
+
+  if filereadable(reference)
+    let refdump = ReadAndFilter(reference, filter)
+  else
+    " Must be a new screendump, always fail
+    let refdump = []
+  endif
+
+  let did_mkdir = 0
+  if !isdirectory('failed')
+    let did_mkdir = 1
+    call mkdir('failed')
+  endif
+
+  let i = 0
+  while 1
+    " leave some time for updating the original window
+    sleep 10m
+    call delete(testfile)
+    call term_dumpwrite(a:buf, testfile, a:options)
+    let testdump = ReadAndFilter(testfile, filter)
+    if refdump == testdump
+      call delete(testfile)
+      if did_mkdir
+       call delete('failed', 'd')
+      endif
+      break
+    endif
+    if i == max_loops
+      " Leave the failed dump around for inspection.
+      if filereadable(reference)
+       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
+      else
+       let msg = 'See new dump file: call term_dumpload("testdir/' .. testfile .. '")'
+       " no point in retrying
+       let g:run_nr = 10
+      endif
+      for i in range(len(refdump))
+       if i >= len(testdump)
+         break
+       endif
+       if testdump[i] != refdump[i]
+         let msg = msg . '; difference in line ' . (i + 1) . ': "' . testdump[i] . '"'
+       endif
+      endfor
+      call assert_report(msg)
+      return 1
+    endif
+    let i += 1
+  endwhile
+  return 0
+endfunc
diff --git a/test/start_tsserver.vim b/test/start_tsserver.vim
new file mode 100644 (file)
index 0000000..3896c70
--- /dev/null
@@ -0,0 +1,10 @@
+vim9script
+source common.vim
+g:LoadLspPlugin()
+var lspServers = [{
+filetype: ['typescript', 'javascript'],
+         path: exepath('typescript-language-server'),
+         args: ['--stdio']
+}]
+g:LspAddServer(lspServers)
+g:StartLangServerWithFile('Xtest.ts')
diff --git a/test/term_util.vim b/test/term_util.vim
new file mode 100644 (file)
index 0000000..7b1779f
--- /dev/null
@@ -0,0 +1,125 @@
+" Functions about terminal shared by several tests
+
+" Wrapper around term_wait().
+" The second argument is the minimum time to wait in msec, 10 if omitted.
+func TermWait(buf, ...)
+  let wait_time = a:0 ? a:1 : 10
+  call term_wait(a:buf, wait_time)
+endfunc
+
+" Run Vim with "arguments" in a new terminal window.
+" By default uses a size of 20 lines and 75 columns.
+" Returns the buffer number of the terminal.
+"
+" Options is a dictionary, these items are recognized:
+" "keep_t_u7" - when 1 do not make t_u7 empty (resetting t_u7 avoids clearing
+"               parts of line 2 and 3 on the display)
+" "rows" - height of the terminal window (max. 20)
+" "cols" - width of the terminal window (max. 78)
+" "statusoff" - number of lines the status is offset from default
+" "wait_for_ruler" - if zero then don't wait for ruler to show
+" "no_clean" - if non-zero then remove "--clean" from the command
+func RunVimInTerminal(arguments, options)
+  " If Vim doesn't exit a swap file remains, causing other tests to fail.
+  " Remove it here.
+  call delete(".swp")
+
+  if exists('$COLORFGBG')
+    " Clear $COLORFGBG to avoid 'background' being set to "dark", which will
+    " only be corrected if the response to t_RB is received, which may be too
+    " late.
+    let $COLORFGBG = ''
+  endif
+
+  " Make a horizontal and vertical split, so that we can get exactly the right
+  " size terminal window.  Works only when the current window is full width.
+  call assert_equal(&columns, winwidth(0))
+  split
+  vsplit
+
+  " Always do this with 256 colors and a light background.
+  set t_Co=256 background=light
+  hi Normal ctermfg=NONE ctermbg=NONE
+
+  " Make the window 20 lines high and 75 columns, unless told otherwise or
+  " 'termwinsize' is set.
+  let rows = get(a:options, 'rows', 20)
+  let cols = get(a:options, 'cols', 75)
+  let statusoff = get(a:options, 'statusoff', 1)
+
+  if get(a:options, 'keep_t_u7', 0)
+    let reset_u7 = ''
+  else
+    let reset_u7 = ' --cmd "set t_u7=" '
+  endif
+
+  let cmd = exepath('vim') .. ' -u NONE --clean --not-a-term --cmd "set enc=utf8"'.. reset_u7 .. a:arguments
+
+  if get(a:options, 'no_clean', 0)
+    let cmd = substitute(cmd, '--clean', '', '')
+  endif
+
+  let options = #{curwin: 1}
+  if &termwinsize == ''
+    let options.term_rows = rows
+    let options.term_cols = cols
+  endif
+
+  " Accept other options whose name starts with 'term_'.
+  call extend(options, filter(copy(a:options), 'v:key =~# "^term_"'))
+
+  let buf = term_start(cmd, options)
+
+  if &termwinsize == ''
+    " in the GUI we may end up with a different size, try to set it.
+    if term_getsize(buf) != [rows, cols]
+      call term_setsize(buf, rows, cols)
+    endif
+    call assert_equal([rows, cols], term_getsize(buf))
+  else
+    let rows = term_getsize(buf)[0]
+    let cols = term_getsize(buf)[1]
+  endif
+
+  call TermWait(buf)
+
+  if get(a:options, 'wait_for_ruler', 1)
+    " Wait for "All" or "Top" of the ruler to be shown in the last line or in
+    " the status line of the last window. This can be quite slow (e.g. when
+    " using valgrind).
+    " If it fails then show the terminal contents for debugging.
+    try
+      call g:WaitFor({-> len(term_getline(buf, rows)) >= cols - 1 || len(term_getline(buf, rows - statusoff)) >= cols - 1})
+    catch /timed out after/
+      let lines = map(range(1, rows), {key, val -> term_getline(buf, val)})
+      call assert_report('RunVimInTerminal() failed, screen contents: ' . join(lines, "<NL>"))
+    endtry
+  endif
+
+  return buf
+endfunc
+
+" Stop a Vim running in terminal buffer "buf".
+func StopVimInTerminal(buf, kill = 1)
+  call assert_equal("running", term_getstatus(a:buf))
+
+  " Wait for all the pending updates to terminal to complete
+  call TermWait(a:buf)
+
+  " CTRL-O : works both in Normal mode and Insert mode to start a command line.
+  " In Command-line it's inserted, the CTRL-U removes it again.
+  call term_sendkeys(a:buf, "\<C-O>:\<C-U>qa!\<cr>")
+
+  " Wait for all the pending updates to terminal to complete
+  call TermWait(a:buf)
+
+  " Wait for the terminal to end.
+  call WaitForAssert({-> assert_equal("finished", term_getstatus(a:buf))})
+
+  " If the buffer still exists forcefully wipe it.
+  if a:kill && bufexists(a:buf)
+    exe a:buf .. 'bwipe!'
+  endif
+endfunc
+
+" vim: shiftwidth=2 sts=2 expandtab
index d0b86959a96535b2092f4327a65fdc28fa089e75..74a7431490f273d3b9fd6060672a584f8fabb3a5 100644 (file)
@@ -1,6 +1,10 @@
 vim9script
 # Unit tests for Vim Language Server Protocol (LSP) typescript client
 
+source common.vim
+source term_util.vim
+source screendump.vim
+
 var lspServers = [{
       filetype: ['typescript', 'javascript'],
       path: exepath('typescript-language-server'),
@@ -26,7 +30,7 @@ def g:Test_LspDiag()
   ]
 
   setline(1, lines)
-  :sleep 1
+  :sleep 3
   g:WaitForDiags(2)
   :redraw!
   :LspDiagShow
@@ -76,7 +80,7 @@ def g:Test_LspGoto()
   sleep 200m
 
   var lines: list<string> = [
-    'function B(val: number): void;'
+    'function B(val: number): void;',
     'function B(val: string): void;',
     'function B(val: string | number) {',
     '  console.log(val);',
@@ -88,7 +92,7 @@ def g:Test_LspGoto()
   ]
 
   setline(1, lines)
-  :sleep 1
+  :sleep 3
 
   cursor(8, 1)
   assert_equal('', execute('LspGotoDefinition'))
@@ -133,25 +137,32 @@ def g:Test_LspGoto()
   assert_equal(3, line('.'))
   assert_equal([], popup_list())
   popup_clear()
+enddef
 
-  :%bw!
+# Test for auto-completion.  Make sure that only keywords that matches with the
+# keyword before the cursor are shown.
+def g:Test_LspCompletion1()
+  var lines =<< trim END
+    const http = require('http')
+    http.cr
+  END
+  writefile(lines, 'Xcompletion1.js', 'D')
+  var buf = g:RunVimInTerminal('--cmd "silent so start_tsserver.vim" Xcompletion1.js', {rows: 10, wait_for_ruler: 1})
+  sleep 5
+  term_sendkeys(buf, "GAe")
+  g:TermWait(buf)
+  g:VerifyScreenDump(buf, 'Test_tsserver_completion_1', {})
+  term_sendkeys(buf, "\<BS>")
+  g:TermWait(buf)
+  g:VerifyScreenDump(buf, 'Test_tsserver_completion_2', {})
+
+  g:StopVimInTerminal(buf)
 enddef
 
 # Start the typescript language server.  Returns true on success and false on
 # failure.
 def g:StartLangServer(): bool
-  # Edit a dummy .ts file to start the LSP server
-  :edit Xtest.ts
-  # Wait for the LSP server to become ready (max 10 seconds)
-  var maxcount = 100
-  while maxcount > 0 && !g:LspServerReady()
-    :sleep 100m
-    maxcount -= 1
-  endwhile
-  var serverStatus: bool = g:LspServerReady()
-  :%bw!
-
-  return serverStatus
+  return g:StartLangServerWithFile('Xtest.ts')
 enddef
 
 # vim: shiftwidth=2 softtabstop=2 noexpandtab