Merge branch 'breakpoints-list' into remote-bp

This commit is contained in:
Ben Jackson 2019-03-30 10:13:26 +00:00
commit 88bac786ba
17 changed files with 1325 additions and 207 deletions

7
.gitignore vendored
View file

@ -1,3 +1,10 @@
*.un~
*.sw[op]
__pycache__
.vscode
tests/*.res
tests/messages
tests/debuglog
test.log
gadgets/
*.pyc

6
.gitmodules vendored
View file

@ -0,0 +1,6 @@
[submodule "vendor/vader.vim"]
path = vendor/vader.vim
url = https://github.com/junegunn/vader.vim
[submodule "vendor/vim-themis"]
path = vendor/vim-themis
url = https://github.com/thinca/vim-themis

View file

@ -102,6 +102,10 @@ function! vimspector#ShowOutput( category ) abort
py3 _vimspector_session.ShowOutput( vim.eval( 'a:category' ) )
endfunction
function! vimspector#ListBreakpoints() abort
py3 _vimspector_session.ListBreakpoints()
endfunction
" Boilerplate {{{
let &cpo=s:save_cpo
unlet s:save_cpo

35
azure-pipelines.yml Normal file
View file

@ -0,0 +1,35 @@
# Starter pipeline
# Start with a minimal pipeline that you can customize to build and deploy your code.
# Add steps that build, run tests, deploy, and more:
# https://aka.ms/yaml
jobs:
- job: 'linax'
pool:
vimImage: 'ubuntu-16.04'
container: 'puremourning/vimspector:test'
steps:
- bash: python3 install_gadget.py
displayName: 'Install gadgets'
- bash: vim --version
displayName: 'Print vim version information'
- bash: ./run_tests
displayName: 'Run the tests'
- job: 'macos'
pool:
vmImage: 'macOS-10.13'
steps:
- bash: brew install macvim
displayName: 'Install vim'
- bash: python3 install_gadget.py
displayName: 'Install gadgets'
- bash: vim --version
displayName: 'Print vim version information'
- bash: ./run_tests
displayName: 'Run the tests'

378
install_gadget.py Executable file
View file

@ -0,0 +1,378 @@
#!/usr/bin/env python
# vimspector - A multi-language debugging system for Vim
# Copyright 2019 Ben Jackson
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
try:
import urllib.request as urllib2
except ImportError:
import urllib2
import contextlib
import os
import collections
import string
import zipfile
import shutil
import subprocess
import traceback
import tarfile
import hashlib
import sys
import json
# Include vimspector source, for utils
sys.path.insert( 1, os.path.join( os.path.dirname( __file__ ),
'python3' ) )
from vimspector import install
GADGETS = {
'vscode-cpptools': {
'download': {
'url': ( 'https://github.com/Microsoft/vscode-cpptools/releases/download/'
'${version}/${file_name}' ),
},
'do': lambda name, root: InstallCppTools( name, root ),
'all': {
'version': '0.21.0',
},
'linux': {
'file_name': 'cpptools-linux.vsix',
'checksum': None,
},
'macos': {
'file_name': 'cpptools-osx.vsix',
'checksum': '4c149df241f8a548f928d824565aa9bb3ccaaa426c07aac3b47db3a51ebbb1f4',
},
'windows': {
'file_name': 'cpptools-win32.vsix',
'checksum': None,
},
"adapters": {
"vscode-cpptools": {
"name": "cppdbg",
"command": [
"${gadgetDir}/vscode-cpptools/debugAdapters/OpenDebugAD7"
],
"attach": {
"pidProperty": "processId",
"pidSelect": "ask"
},
},
},
},
'vscode-python': {
'download': {
'url': ( 'https://github.com/Microsoft/vscode-python/releases/download/'
'${version}/${file_name}' ),
},
'all': {
'version': '2018.12.1',
'file_name': 'ms-python-release.vsix',
'checksum': '0406028b7d2fbb86ffd6cda18a36638a95111fd35b19cc198d343a2828f5c3b1',
},
},
'tclpro': {
'repo': {
'url': 'https://github.com/puremourning/TclProDebug',
'ref': 'master',
},
'do': lambda name, root: InstallTclProDebug( name, root )
},
'vscode-mono-debug': {
'enabled': False,
'download': {
'url': 'https://marketplace.visualstudio.com/_apis/public/gallery/'
'publishers/ms-vscode/vsextensions/mono-debug/${version}/'
'vspackage',
'target': 'vscode-mono-debug.tar.gz',
'format': 'tar',
},
'all': {
'file_name': 'vscode-mono-debug.vsix',
'version': '0.15.8',
'checksum': '723eb2b621b99d65a24f215cb64b45f5fe694105613a900a03c859a62a810470',
}
},
'vscode-bash-debug': {
'download': {
'url': ( 'https://github.com/rogalmic/vscode-bash-debug/releases/'
'download/${version}/${file_name}' ),
},
'all': {
'file_name': 'bash-debug-0.3.3.vsix',
'version': 'untagged-3c529a47de44a70c9c76',
'checksum': '',
}
}
}
@contextlib.contextmanager
def CurrentWorkingDir( d ):
cur_d = os.getcwd()
try:
os.chdir( d )
yield
finally:
os.chdir( cur_d )
def MakeExecutable( file_path ):
# TODO: import stat and use them by _just_ adding the X bit.
print( 'Making executable: {}'.format( file_path ) )
os.chmod( file_path, 0o755 )
def InstallCppTools( name, root ):
extension = os.path.join( root, 'extension' )
# It's hilarious, but the execute bits aren't set in the vsix. So they
# actually have javascript code which does this. It's just a horrible horrible
# hoke that really is not funny.
MakeExecutable( os.path.join( extension, 'debugAdapters', 'OpenDebugAD7' ) )
with open( os.path.join( extension, 'package.json' ) ) as f:
package = json.load( f )
runtime_dependencies = package[ 'runtimeDependencies' ]
for dependency in runtime_dependencies:
for binary in dependency.get( 'binaries' ):
file_path = os.path.abspath( os.path.join( extension, binary ) )
if os.path.exists( file_path ):
MakeExecutable( os.path.join( extension, binary ) )
MakeExtensionSymlink( name, root )
def InstallTclProDebug( name, root ):
configure = [ './configure' ]
if OS == 'macos':
# Apple removed the headers from system frameworks because they are
# determined to make life difficult. And the TCL configure scripts are super
# old so don't know about this. So we do their job for them and try and find
# a tclConfig.sh.
#
# NOTE however that in Apple's infinite wisdom, installing the "headers" in
# the other location is actually broken because the paths in the
# tclConfig.sh are pointing at the _old_ location. You actually do have to
# run the package installation which puts the headers back in order to work.
# This is why the below list is does not contain stuff from
# /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform
# '/Applications/Xcode.app/Contents/Developer/Platforms'
# '/MacOSX.platform/Developer/SDKs/MacOSX.sdk/System'
# '/Library/Frameworks/Tcl.framework',
# '/Applications/Xcode.app/Contents/Developer/Platforms'
# '/MacOSX.platform/Developer/SDKs/MacOSX.sdk/System'
# '/Library/Frameworks/Tcl.framework/Versions'
# '/Current',
for p in [ '/System/Library/Frameworks/Tcl.framework/',
'/usr/local/opt/tcl-tk/lib' ]:
if os.path.exists( os.path.join( p, 'tclConfig.sh' ) ):
configure.append( '--with-tcl=' + p )
break
with CurrentWorkingDir( os.path.join( root, 'lib', 'tclparser' ) ):
subprocess.check_call( configure )
subprocess.check_call( [ 'make' ] )
MakeSymlink( gadget_dir, name, root )
def DownloadFileTo( url, destination, file_name = None, checksum = None ):
if not file_name:
file_name = url.split( '/' )[ -1 ]
file_path = os.path.abspath( os.path.join( destination, file_name ) )
if not os.path.isdir( destination ):
os.makedirs( destination )
if os.path.exists( file_path ):
if checksum:
if ValidateCheckSumSHA256( file_path, checksum ):
print( "Checksum matches for {}, using it".format( file_path ) )
return file_path
else:
print( "Checksum doesn't match for {}, removing it".format(
file_path ) )
print( "Removing existing {}".format( file_path ) )
os.remove( file_path )
r = urllib2.Request( url, headers = { 'User-Agent': 'Vimspector' } )
print( "Downloading {} to {}/{}".format( url, destination, file_name ) )
with contextlib.closing( urllib2.urlopen( r ) ) as u:
with open( file_path, 'wb' ) as f:
f.write( u.read() )
if checksum:
if not ValidateCheckSumSHA256( file_path, checksum ):
raise RuntimeError(
'Checksum for {} ({}) does not match expected {}'.format(
file_path,
GetChecksumSHA254( file_path ),
checksum ) )
else:
print( "Checksum for {}: {}".format( file_path,
GetChecksumSHA254( file_path ) ) )
return file_path
def GetChecksumSHA254( file_path ):
with open( file_path, 'rb' ) as existing_file:
return hashlib.sha256( existing_file.read() ).hexdigest()
def ValidateCheckSumSHA256( file_path, checksum ):
existing_sha256 = GetChecksumSHA254( file_path )
return existing_sha256 == checksum
def RemoveIfExists( destination ):
if os.path.exists( destination ) or os.path.islink( destination ):
if os.path.islink( destination ):
print( "Removing file {}".format( destination ) )
os.remove( destination )
else:
print( "Removing dir {}".format( destination ) )
shutil.rmtree( destination )
# Python's ZipFile module strips execute bits from files, for no good reason
# other than crappy code. Let's do it's job for it.
class ModePreservingZipFile( zipfile.ZipFile ):
def extract( self, member, path = None, pwd = None ):
if not isinstance(member, zipfile.ZipInfo):
member = self.getinfo(member)
if path is None:
path = os.getcwd()
ret_val = self._extract_member(member, path, pwd)
attr = member.external_attr >> 16
os.chmod(ret_val, attr)
return ret_val
def ExtractZipTo( file_path, destination, format ):
print( "Extracting {} to {}".format( file_path, destination ) )
RemoveIfExists( destination )
if format == 'zip':
with ModePreservingZipFile( file_path ) as f:
f.extractall( path = destination )
return
elif format == 'tar':
try:
with tarfile.open( file_path ) as f:
f.extractall( path = destination )
except Exception:
# There seems to a bug in python's tarfile that means it can't read some
# windows-generated tar files
os.makedirs( destination )
with CurrentWorkingDir( destination ):
subprocess.check_call( [ 'tar', 'zxvf', file_path ] )
def MakeExtensionSymlink( name, root ):
MakeSymlink( gadget_dir, name, os.path.join( root, 'extension' ) ),
def MakeSymlink( in_folder, link, pointing_to ):
RemoveIfExists( os.path.join( in_folder, link ) )
in_folder = os.path.abspath( in_folder )
pointing_to = os.path.relpath( os.path.abspath( pointing_to ),
in_folder )
os.symlink( pointing_to, os.path.join( in_folder, link ) )
def CloneRepoTo( url, ref, destination ):
RemoveIfExists( destination )
subprocess.check_call( [ 'git', 'clone', url, destination ] )
subprocess.check_call( [ 'git', '-C', destination, 'checkout', ref ] )
OS = install.GetOS()
gadget_dir = install.GetGadgetDir( os.path.dirname( __file__ ), OS )
print( 'OS = ' + OS )
print( 'gadget_dir = ' + gadget_dir )
failed = []
all_adapters = {}
for name, gadget in GADGETS.items():
if not gadget.get( 'enabled', True ):
continue
try:
v = {}
v.update( gadget.get( 'all', {} ) )
v.update( gadget.get( OS, {} ) )
if 'download' in gadget:
if 'file_name' not in v:
raise RuntimeError( "Unsupported OS {} for gadget {}".format( OS,
name ) )
destination = os.path.join( gadget_dir, 'download', name, v[ 'version' ] )
url = string.Template( gadget[ 'download' ][ 'url' ] ).substitute( v )
file_path = DownloadFileTo(
url,
destination,
file_name = gadget[ 'download' ].get( 'target' ),
checksum = v.get( 'checksum' ) )
root = os.path.join( destination, 'root' )
ExtractZipTo( file_path,
root,
format = gadget[ 'download' ].get( 'format', 'zip' ) )
elif 'repo' in gadget:
url = string.Template( gadget[ 'repo' ][ 'url' ] ).substitute( v )
ref = string.Template( gadget[ 'repo' ][ 'ref' ] ).substitute( v )
destination = os.path.join( gadget_dir, 'download', name )
CloneRepoTo( url, ref, destination )
root = destination
if 'do' in gadget:
gadget[ 'do' ]( name, root )
else:
MakeExtensionSymlink( name, root )
all_adapters.update( gadget.get( 'adapters', {} ) )
print( "Done installing {}".format( name ) )
except Exception as e:
traceback.print_exc()
failed.append( name )
print( "FAILED installing {}: {}".format( name, e ) )
with open( install.GetGadgetConfigFile( os.path.dirname( __file__ ) ),
'w' ) as f:
json.dump( { 'adapters': all_adapters }, f, indent=2, sort_keys=True )
if failed:
raise RuntimeError( 'Failed to install gadgets: {}'.format(
','.join( failed ) ) )

View file

@ -0,0 +1,265 @@
# vimspector - A multi-language debugging system for Vim
# Copyright 2019 Ben Jackson
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from collections import defaultdict
import vim
import os
class ProjectBreakpoints( object ):
def __init__( self ):
self._connection = None
# These are the user-entered breakpoints.
self._line_breakpoints = defaultdict( list )
self._func_breakpoints = []
# FIXME: Remove this. Remove breakpoints nonesense from code.py
self._breakpoints_handler = None
self._exceptionBreakpoints = None
self._server_capabilities = {}
self._next_sign_id = 1
# TODO: Change to sign_define ?
vim.command( 'sign define vimspectorBP text==> texthl=Error' )
vim.command( 'sign define vimspectorBPDisabled text=!> texthl=Warning' )
def ConnectionUp( self, connection ):
self._connection = connection
def SetServerCapabilities( self, server_capabilities ):
self._server_capabilities = server_capabilities
def ConnectionClosed( self ):
self._breakpoints_handler = None
self._exceptionBreakpoints = None
self._server_capabilities = {}
self._connection = None
# for each breakpoint:
# clear its resolved status
def ListBreakpoints( self ):
# FIXME: Handling of breakpoints is a mess, split between _codeView and this
# object. This makes no sense and should be centralised so that we don't
# have this duplication and bug factory.
qf = []
if self._connection and self._codeView:
qf = self._codeView.BreakpointsAsQuickFix()
else:
for file_name, breakpoints in self._line_breakpoints.items():
for bp in breakpoints:
qf.append( {
'filename': file_name,
'lnum': bp[ 'line' ],
'col': 1,
'type': 'L',
'valid': 1 if bp[ 'state' ] == 'ENABLED' else 0,
'text': "Line breakpoint - {}".format(
bp[ 'state' ] )
} )
# I think this shows that the qf list is not right for this.
for bp in self._func_breakpoints:
qf.append( {
'filename': '',
'lnum': 1,
'col': 1,
'type': 'F',
'valid': 1,
'text': "Function breakpoint: {}".format( bp[ 'function' ] ),
} )
vim.eval( 'setqflist( {} )'.format( json.dumps( qf ) ) )
def ToggleBreakpoint( self ):
line, column = vim.current.window.cursor
file_name = vim.current.buffer.name
if not file_name:
return
found_bp = False
for index, bp in enumerate( self._line_breakpoints[ file_name] ):
if bp[ 'line' ] == line:
found_bp = True
if bp[ 'state' ] == 'ENABLED':
bp[ 'state' ] = 'DISABLED'
else:
if 'sign_id' in bp:
vim.command( 'sign unplace {0} group=VimspectorBP'.format(
bp[ 'sign_id' ] ) )
del self._line_breakpoints[ file_name ][ index ]
if not found_bp:
self._line_breakpoints[ file_name ].append( {
'state': 'ENABLED',
'line': line,
# 'sign_id': <filled in when placed>,
#
# Used by other breakpoint types:
# 'condition': ...,
# 'hitCondition': ...,
# 'logMessage': ...
} )
self.UpdateUI()
def AddFunctionBreakpoint( self, function ):
self._func_breakpoints.append( {
'state': 'ENABLED',
'function': function,
} )
# TODO: We don't really have aanything to update here, but if we're going to
# have a UI list of them we should update that at this point
self.UpdateUI()
def UpdateUI( self ):
if self._connection:
self.SendBreakpoints()
else:
self._ShowBreakpoints()
# FIXME: Remove this temporary compat .layer
def SetBreakpointsHandler( self, handler ):
self._breakpoints_handler = handler
def SendBreakpoints( self ):
if not self._breakpoints_handler:
handler = lambda source, msg: self._ShowBreakpoints()
else:
handler = self._breakpoints_handler
for file_name, line_breakpoints in self._line_breakpoints.items():
breakpoints = []
for bp in line_breakpoints:
if bp[ 'state' ] != 'ENABLED':
continue
if 'sign_id' in bp:
vim.command( 'sign unplace {0} group=VimspectorBP'.format(
bp[ 'sign_id' ] ) )
del bp[ 'sign_id' ]
breakpoints.append( { 'line': bp[ 'line' ] } )
source = {
'name': os.path.basename( file_name ),
'path': file_name,
}
self._connection.DoRequest(
lambda msg: handler( source, msg ),
{
'command': 'setBreakpoints',
'arguments': {
'source': source,
'breakpoints': breakpoints,
},
'sourceModified': False, # TODO: We can actually check this
}
)
if self._server_capabilities.get( 'supportsFunctionBreakpoints' ):
self._connection.DoRequest(
lambda msg: handler( None, msg ),
{
'command': 'setFunctionBreakpoints',
'arguments': {
'breakpoints': [
{ 'name': bp[ 'function' ] }
for bp in self._func_breakpoints if bp[ 'state' ] == 'ENABLED'
],
}
}
)
if self._exceptionBreakpoints is None:
self._SetUpExceptionBreakpoints()
if self._exceptionBreakpoints:
self._connection.DoRequest(
None, # There is nothing on the response to this
{
'command': 'setExceptionBreakpoints',
'arguments': self._exceptionBreakpoints
}
)
def _SetUpExceptionBreakpoints( self ):
exceptionBreakpointFilters = self._server_capabilities.get(
'exceptionBreakpointFilters',
[] )
if exceptionBreakpointFilters or not self._server_capabilities.get(
'supportsConfigurationDoneRequest' ):
exceptionFilters = []
if exceptionBreakpointFilters:
for f in exceptionBreakpointFilters:
response = utils.AskForInput(
"Enable exception filter '{}'? (Y/N)".format( f[ 'label' ] ) )
if response == 'Y':
exceptionFilters.append( f[ 'filter' ] )
elif not response and f.get( 'default' ):
exceptionFilters.append( f[ 'filter' ] )
self._exceptionBreakpoints = {
'filters': exceptionFilters
}
if self._server_capabilities.get( 'supportsExceptionOptions' ):
# FIXME Sigh. The python debug adapter requires this
# key to exist. Even though it is optional.
break_mode = utils.SelectFromList( 'When to break on exception?',
[ 'never',
'always',
'unhandled',
'userHandled' ] )
if not break_mode:
break_mode = 'unhandled'
path = [ { 'nagate': True, 'names': [ 'DO_NOT_MATCH' ] } ]
self._exceptionBreakpoints[ 'exceptionOptions' ] = [ {
'path': path,
'breakMode': break_mode
} ]
def _ShowBreakpoints( self ):
for file_name, line_breakpoints in self._line_breakpoints.items():
for bp in line_breakpoints:
if 'sign_id' in bp:
vim.command( 'sign unplace {0} group=VimspectorBP '.format(
bp[ 'sign_id' ] ) )
else:
bp[ 'sign_id' ] = self._next_sign_id
self._next_sign_id += 1
vim.command(
'sign place {0} group=VimspectorBP line={1} name={2} file={3}'.format(
bp[ 'sign_id' ] ,
bp[ 'line' ],
'vimspectorBP' if bp[ 'state' ] == 'ENABLED'
else 'vimspectorBPDisabled',
file_name ) )

View file

@ -20,8 +20,6 @@ from collections import defaultdict
from vimspector import utils
SIGN_ID_OFFSET = 10000000
class CodeView( object ):
def __init__( self, window ):
@ -30,7 +28,7 @@ class CodeView( object ):
self._logger = logging.getLogger( __name__ )
utils.SetUpLogging( self._logger )
self._next_sign_id = SIGN_ID_OFFSET
self._next_sign_id = 1
self._breakpoints = defaultdict( list )
self._signs = {
'vimspectorPC': None,
@ -53,7 +51,8 @@ class CodeView( object ):
def SetCurrentFrame( self, frame ):
if self._signs[ 'vimspectorPC' ]:
vim.command( 'sign unplace {0}'.format( self._signs[ 'vimspectorPC' ] ) )
vim.command( 'sign unplace {} group=VimspectorCode'.format(
self._signs[ 'vimspectorPC' ] ) )
self._signs[ 'vimspectorPC' ] = None
if not frame or not frame.get( 'source' ):
@ -76,7 +75,9 @@ class CodeView( object ):
self._signs[ 'vimspectorPC' ] = self._next_sign_id
self._next_sign_id += 1
vim.command( 'sign place {0} line={1} name=vimspectorPC file={2}'.format(
vim.command( 'sign place {0} group=VimspectorCode priority=20 '
'line={1} name=vimspectorPC '
'file={2}'.format(
self._signs[ 'vimspectorPC' ],
frame[ 'line' ],
frame[ 'source' ][ 'path' ] ) )
@ -85,7 +86,8 @@ class CodeView( object ):
def Clear( self ):
if self._signs[ 'vimspectorPC' ]:
vim.command( 'sign unplace {0}'.format( self._signs[ 'vimspectorPC' ] ) )
vim.command( 'sign unplace {} group=VimspectorCode'.format(
self._signs[ 'vimspectorPC' ] ) )
self._signs[ 'vimspectorPC' ] = None
self._UndisplaySigns()
@ -140,7 +142,7 @@ class CodeView( object ):
def _UndisplaySigns( self ):
for sign_id in self._signs[ 'breakpoints' ]:
vim.command( 'sign unplace {0}'.format( sign_id ) )
vim.command( 'sign unplace {} group=VimspectorCode'.format( sign_id ) )
self._signs[ 'breakpoints' ].clear()
@ -160,7 +162,10 @@ class CodeView( object ):
self._next_sign_id += 1
self._signs[ 'breakpoints' ].append( sign_id )
vim.command(
'sign place {0} line={1} name={2} file={3}'.format(
'sign place {0} group=VimspectorCode priority=9 '
'line={1} '
'name={2} '
'file={3}'.format(
sign_id,
breakpoint[ 'line' ],
'vimspectorBP' if breakpoint[ 'verified' ]
@ -168,6 +173,22 @@ class CodeView( object ):
file_name ) )
def BreakpointsAsQuickFix( self ):
qf = []
for file_name, breakpoints in self._breakpoints.items():
for breakpoint in breakpoints:
qf.append( {
'filename': file_name,
'lnum': breakpoint.get( 'line', 1 ),
'col': 1,
'type': 'L',
'valid': 1 if breakpoint.get( 'verified' ) else 0,
'text': "Line breakpoint - {}".format(
'VERIFIED' if breakpoint.get( 'verified' ) else 'INVALID' )
} )
return qf
def LaunchTerminal( self, params ):
# kind = params.get( 'kind', 'integrated' )

View file

@ -23,14 +23,18 @@ import shlex
from collections import defaultdict
from vimspector import ( code,
from vimspector import ( breakpoints,
code,
debug_adapter_connection,
install,
output,
stack_trace,
utils,
variables )
SIGN_ID_OFFSET = 10005000
VIMSPECTOR_HOME = os.path.abspath( os.path.join( os.path.dirname( __file__ ),
'..',
'..' ) )
class DebugSession( object ):
@ -38,85 +42,28 @@ class DebugSession( object ):
self._logger = logging.getLogger( __name__ )
utils.SetUpLogging( self._logger )
self._logger.info( 'VIMSPECTOR_HOME = %s', VIMSPECTOR_HOME )
self._logger.info( 'gadgetDir = %s',
install.GetGadgetDir( VIMSPECTOR_HOME,
install.GetOS() ) )
self._uiTab = None
self._stackTraceView = None
self._variablesView = None
self._outputView = None
self._breakpoints = breakpoints.ProjectBreakpoints()
self._run_on_server_exit = None
self._next_sign_id = SIGN_ID_OFFSET
# FIXME: This needs redesigning. There are a number of problems:
# - breakpoints don't have to be line-wise (e.g. method/exception)
# - when the server moves/changes a breakpoint, this is not updated,
# leading to them getting out of sync
# - the split of responsibility between this object and the CodeView is
# messy and ill-defined.
self._line_breakpoints = defaultdict( list )
self._func_breakpoints = []
self._ResetServerState()
vim.command( 'sign define vimspectorBP text==> texthl=Error' )
vim.command( 'sign define vimspectorBPDisabled text=!> texthl=Warning' )
def _ResetServerState( self ):
self._connection = None
self._configuration = None
self._exceptionBreakpoints = None
self._init_complete = False
self._launch_complete = False
self._server_capabilities = {}
def ToggleBreakpoint( self ):
line, column = vim.current.window.cursor
file_name = vim.current.buffer.name
if not file_name:
return
found_bp = False
for index, bp in enumerate( self._line_breakpoints[ file_name] ):
if bp[ 'line' ] == line:
found_bp = True
if bp[ 'state' ] == 'ENABLED':
bp[ 'state' ] = 'DISABLED'
else:
if 'sign_id' in bp:
vim.command( 'sign unplace {0}'.format( bp[ 'sign_id' ] ) )
del self._line_breakpoints[ file_name ][ index ]
if not found_bp:
self._line_breakpoints[ file_name ].append( {
'state': 'ENABLED',
'line': line,
# 'sign_id': <filled in when placed>,
#
# Used by other breakpoint types:
# 'condition': ...,
# 'hitCondition': ...,
# 'logMessage': ...
} )
self._UpdateUIBreakpoints()
def _UpdateUIBreakpoints( self ):
if self._connection:
self._SendBreakpoints()
else:
self._ShowBreakpoints()
def AddFunctionBreakpoint( self, function ):
self._func_breakpoints.append( {
'state': 'ENABLED',
'function': function,
} )
# TODO: We don't really have aanything to update here, but if we're going to
# have a UI list of them we should update that at this point
self._UpdateUIBreakpoints()
def Start( self, launch_variables = {} ):
self._configuration = None
self._adapter = None
@ -132,7 +79,15 @@ class DebugSession( object ):
database = json.load( f )
configurations = database.get( 'configurations' )
adapters = database.get( 'adapters' )
adapters = {}
for gadget_config_file in [ install.GetGadgetConfigFile( VIMSPECTOR_HOME ),
utils.PathToConfigFile( '.gadgets.json' ) ]:
if gadget_config_file and os.path.exists( gadget_config_file ):
with open( gadget_config_file, 'r' ) as f:
adapters.update( json.load( f ).get( 'adapters' ) or {} )
adapters.update( database.get( 'adapters' ) or {} )
if len( configurations ) == 1:
configuration_name = next( iter( configurations.keys() ) )
@ -156,7 +111,8 @@ class DebugSession( object ):
# way to load .vimspector.local.json which just sets variables
self._variables = {
'dollar': '$', # HACK
'workspaceRoot': self._workspace_root
'workspaceRoot': self._workspace_root,
'gadgetDir': install.GetGadgetDir( VIMSPECTOR_HOME, install.GetOS() )
}
self._variables.update(
utils.ParseVariables( adapter.get( 'variables', {} ) ) )
@ -195,6 +151,16 @@ class DebugSession( object ):
self._stackTraceView.ConnectionUp( self._connection )
self._variablesView.ConnectionUp( self._connection )
self._outputView.ConnectionUp( self._connection )
self._breakpoints.ConnectionUp( self._connection )
def update_breakpoints( source, message ):
if 'body' not in message:
return
self._codeView.AddBreakpoints( source,
message[ 'body' ][ 'breakpoints' ] )
self._codeView.ShowBreakpoints()
self._breakpoints.SetBreakpointsHandler( update_breakpoints )
if self._connection:
self._StopDebugAdapter( start )
@ -250,7 +216,7 @@ class DebugSession( object ):
self._uiTab = None
# make sure that we're displaying signs in any still-open buffers
self._UpdateUIBreakpoints()
self._breakpoints.UpdateUI()
def StepOver( self ):
if self._stackTraceView.GetCurrentThreadId() is None:
@ -557,6 +523,7 @@ class DebugSession( object ):
def _Initialise( self ):
def handle_initialize_response( msg ):
self._server_capabilities = msg.get( 'body' ) or {}
self._breakpoints.SetServerCapabilities( self._server_capabilities )
self._Launch()
self._connection.DoRequest( handle_initialize_response, {
@ -612,12 +579,6 @@ class DebugSession( object ):
)
def _UpdateBreakpoints( self, source, message ):
if 'body' not in message:
return
self._codeView.AddBreakpoints( source, message[ 'body' ][ 'breakpoints' ] )
self._codeView.ShowBreakpoints()
def _OnLaunchComplete( self ):
self._launch_complete = True
self._LoadThreadsIfReady()
@ -649,7 +610,8 @@ class DebugSession( object ):
def OnEvent_initialized( self, message ):
self._SendBreakpoints()
self._codeView.ClearBreakpoints()
self._breakpoints.SendBreakpoints()
if self._server_capabilities.get( 'supportsConfigurationDoneRequest' ):
self._connection.DoRequest(
@ -661,7 +623,6 @@ class DebugSession( object ):
else:
self._OnInitializeComplete()
def OnEvent_thread( self, message ):
self._stackTraceView.OnThreadEvent( message[ 'body' ] )
@ -721,6 +682,7 @@ class DebugSession( object ):
self._stackTraceView.ConnectionClosed()
self._variablesView.ConnectionClosed()
self._outputView.ConnectionClosed()
self._breakpoints.ConnectionClosed()
self._ResetServerState()
@ -731,128 +693,6 @@ class DebugSession( object ):
# We will handle this when the server actually exists
utils.UserMessage( "Debugging was terminated." )
def _RemoveBreakpoints( self ):
for breakpoints in self._line_breakpoints.values():
for bp in breakpoints:
if 'sign_id' in bp:
vim.command( 'sign unplace {0}'.format( bp[ 'sign_id' ] ) )
del bp[ 'sign_id' ]
def _SendBreakpoints( self ):
self._codeView.ClearBreakpoints()
for file_name, line_breakpoints in self._line_breakpoints.items():
breakpoints = []
for bp in line_breakpoints:
if bp[ 'state' ] != 'ENABLED':
continue
if 'sign_id' in bp:
vim.command( 'sign unplace {0}'.format( bp[ 'sign_id' ] ) )
del bp[ 'sign_id' ]
breakpoints.append( { 'line': bp[ 'line' ] } )
source = {
'name': os.path.basename( file_name ),
'path': file_name,
}
self._connection.DoRequest(
functools.partial( self._UpdateBreakpoints, source ),
{
'command': 'setBreakpoints',
'arguments': {
'source': source,
'breakpoints': breakpoints,
},
'sourceModified': False, # TODO: We can actually check this
}
)
if self._server_capabilities.get( 'supportsFunctionBreakpoints' ):
self._connection.DoRequest(
functools.partial( self._UpdateBreakpoints, None ),
{
'command': 'setFunctionBreakpoints',
'arguments': {
'breakpoints': [
{ 'name': bp[ 'function' ] }
for bp in self._func_breakpoints if bp[ 'state' ] == 'ENABLED'
],
}
}
)
if self._exceptionBreakpoints is None:
self._SetUpExceptionBreakpoints()
if self._exceptionBreakpoints:
self._connection.DoRequest(
None, # There is nothing on the response to this
{
'command': 'setExceptionBreakpoints',
'arguments': self._exceptionBreakpoints
}
)
def _SetUpExceptionBreakpoints( self ):
exceptionBreakpointFilters = self._server_capabilities.get(
'exceptionBreakpointFilters',
[] )
if exceptionBreakpointFilters or not self._server_capabilities.get(
'supportsConfigurationDoneRequest' ):
exceptionFilters = []
if exceptionBreakpointFilters:
for f in exceptionBreakpointFilters:
response = utils.AskForInput(
"Enable exception filter '{}'? (Y/N)".format( f[ 'label' ] ) )
if response == 'Y':
exceptionFilters.append( f[ 'filter' ] )
elif not response and f.get( 'default' ):
exceptionFilters.append( f[ 'filter' ] )
self._exceptionBreakpoints = {
'filters': exceptionFilters
}
if self._server_capabilities.get( 'supportsExceptionOptions' ):
# FIXME Sigh. The python debug adapter requires this
# key to exist. Even though it is optional.
break_mode = utils.SelectFromList( 'When to break on exception?',
[ 'never',
'always',
'unhandled',
'userHandled' ] )
if not break_mode:
break_mode = 'unhandled'
path = [ { 'nagate': True, 'names': [ 'DO_NOT_MATCH' ] } ]
self._exceptionBreakpoints[ 'exceptionOptions' ] = [ {
'path': path,
'breakMode': break_mode
} ]
def _ShowBreakpoints( self ):
for file_name, line_breakpoints in self._line_breakpoints.items():
for bp in line_breakpoints:
if 'sign_id' in bp:
vim.command( 'sign unplace {0}'.format( bp[ 'sign_id' ] ) )
else:
bp[ 'sign_id' ] = self._next_sign_id
self._next_sign_id += 1
vim.command(
'sign place {0} line={1} name={2} file={3}'.format(
bp[ 'sign_id' ] ,
bp[ 'line' ],
'vimspectorBP' if bp[ 'state' ] == 'ENABLED'
else 'vimspectorBPDisabled',
file_name ) )
def OnEvent_output( self, message ):
if self._outputView:
self._outputView.OnOutput( message[ 'body' ] )
@ -880,3 +720,12 @@ class DebugSession( object ):
self._outputView.Print( 'server', msg )
self._stackTraceView.OnStopped( event )
def ListBreakpoints( self ):
return self._breakpoints.ListBreakpoints()
def ToggleBreakpoint( self ):
return self._breakpoints.ToggleBreakpoint()
def AddFunctionBreakpoint( self, function ):
return self._breakpoints.AddFunctionBreakpoint( function )

View file

@ -0,0 +1,32 @@
# vimspector - A multi-language debugging system for Vim
# Copyright 2019 Ben Jackson
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import platform
import os
def GetOS():
if platform.system() == 'Darwin':
return 'macos'
elif platform.system() == 'Winwdows':
return 'windows'
else:
return 'linux'
def GetGadgetDir( vimspector_base, OS ):
return os.path.join( os.path.abspath( vimspector_base ), 'gadgets', OS )
def GetGadgetConfigFile( vimspector_base ):
return os.path.join( GetGadgetDir( vimspector_base, GetOS() ),
'.gadgets.json' )

View file

@ -262,7 +262,7 @@ def AppendToBuffer( buf, line_or_lines, modified=False ):
line = 1
buf[:] = line_or_lines
except:
# There seem to be a lot of Vim bugs that lead to E351, whose help says that
# There seem to be a lot of Vim bugs that lead to E315, whose help says that
# this is an internal error. Ignore the error, but write a trace to the log.
logging.getLogger( __name__ ).exception(
'Internal error while updating buffer %s (%s)', buf.name, buf.number )

8
run_test_vim Executable file
View file

@ -0,0 +1,8 @@
#!/usr/bin/env bash
RUN_VIM="vim --noplugin --clean --not-a-term -Nu vimrc"
RUN_TEST="${RUN_VIM} -S run_test.vim"
pushd tests > /dev/null
exec $RUN_VIM "$@"

31
run_tests Executable file
View file

@ -0,0 +1,31 @@
#!/usr/bin/env bash
RUN_VIM="vim --noplugin --clean --not-a-term"
RUN_TEST="${RUN_VIM} -S run_test.vim"
pushd tests > /dev/null
echo "Running Vimspector Vim tests"
RESULT=0
for t in *.test.vim; do
echo ""
echo "%RUN: $t"
rm -f messages debuglog
if ${RUN_TEST} $t --cmd 'au SwapExists * let v:swapchoice = "e"'; then
echo "%PASS: $t PASSED"
else
cat messages
echo "%FAIL: $t FAILED"
RESULT=1
fi
done
popd > /dev/null
echo ""
echo "All done."
exit $RESULT

View file

@ -0,0 +1,93 @@
function! SetUp()
if exists ( 'g:loaded_vimpector' )
unlet g:loaded_vimpector
endif
source vimrc
" This is a bit of a hack
runtime! plugin/**/*.vim
endfunction
function! ClearDown()
if exists( '*vimspector#internal#state#Reset' )
call vimspector#internal#state#Reset()
endif
endfunction
function! SetUp_Test_Mappings_Are_Added_HUMAN()
let g:vimspector_enable_mappings = 'HUMAN'
endfunction
function! Test_Mappings_Are_Added_HUMAN()
call assert_true( hasmapto( 'vimspector#Continue()' ) )
call assert_false( hasmapto( 'vimspector#Launch()' ) )
call assert_true( hasmapto( 'vimspector#Stop()' ) )
call assert_true( hasmapto( 'vimspector#Restart()' ) )
call assert_true( hasmapto( 'vimspector#ToggleBreakpoint()' ) )
call assert_true( hasmapto( 'vimspector#AddFunctionBreakpoint' ) )
call assert_true( hasmapto( 'vimspector#StepOver()' ) )
call assert_true( hasmapto( 'vimspector#StepInto()' ) )
call assert_true( hasmapto( 'vimspector#StepOut()' ) )
endfunction
function! Test_Mappings_Are_Added_VISUAL_STUDIO()
call assert_true( hasmapto( 'vimspector#Continue()' ) )
call assert_false( hasmapto( 'vimspector#Launch()' ) )
call assert_true( hasmapto( 'vimspector#Stop()' ) )
call assert_true( hasmapto( 'vimspector#Restart()' ) )
call assert_true( hasmapto( 'vimspector#ToggleBreakpoint()' ) )
call assert_true( hasmapto( 'vimspector#AddFunctionBreakpoint' ) )
call assert_true( hasmapto( 'vimspector#StepOver()' ) )
call assert_true( hasmapto( 'vimspector#StepInto()' ) )
call assert_true( hasmapto( 'vimspector#StepOut()' ) )
endfunction
function! SetUp_Test_Signs_Placed_Using_API_Are_Shown()
let g:vimspector_enable_mappings = 'VISUAL_STUDIO'
endfunction
function! Test_Signs_Placed_Using_API_Are_Shown()
" We need a real file
edit testdata/cpp/simple.cpp
call feedkeys( '/printf<CR>' )
" Set breakpoint
call vimspector#ToggleBreakpoint()
call assert_true( exists( '*vimspector#ToggleBreakpoint' ) )
let signs = sign_getplaced( '.', {
\ 'group': 'VimspectorBP',
\ 'line': line( '.' )
\ } )
call assert_true( len( signs ) == 1 )
call assert_true( len( signs[ 0 ].signs ) == 1 )
call assert_true( signs[ 0 ].signs[ 0 ].name == 'vimspectorBP' )
" Disable breakpoint
call vimspector#ToggleBreakpoint()
let signs = sign_getplaced( '.', {
\ 'group': 'VimspectorBP',
\ 'line': line( '.' )
\ } )
call assert_true( len( signs ) == 1 )
call assert_true( len( signs[ 0 ].signs ) == 1 )
call assert_true( signs[ 0 ].signs[ 0 ].name == 'vimspectorBPDisabled' )
" Remove breakpoint
call vimspector#ToggleBreakpoint()
let signs = sign_getplaced( '.', {
\ 'group': 'VimspectorBP',
\ 'line': line( '.' )
\ } )
call assert_true( len( signs ) == 1 )
call assert_true( len( signs[ 0 ].signs ) == 0 )
" TODO: Use the screen dump test ?
endfunction

29
tests/ci/image/Dockerfile Normal file
View file

@ -0,0 +1,29 @@
FROM ubuntu:18.04
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && \
apt-get -y dist-upgrade && \
apt-get -y install python3-dev \
ca-cacert \
libncurses5-dev libncursesw5-dev \
git \
tcl-dev \
tcllib && \
apt-get -y autoremove
RUN ln -fs /usr/share/zoneinfo/Europe/London /etc/localtime && \
dpkg-reconfigure --frontend noninteractive tzdata
RUN mkdir -p $HOME/vim && \
cd $HOME/vim && \
git clone https://github.com/vim/vim && \
cd vim && \
git checkout v8.1.0958 && \
./configure --with-features=huge \
--enable-python3interp \
--enable-terminal \
--enable-multibyte \
--enable-fail-if-missing && \
make -j 8 install

347
tests/run_test.vim Normal file
View file

@ -0,0 +1,347 @@
" This script is sourced while editing the .vim file with the tests.
" When the script is successful the .res file will be created.
" Errors are appended to the test.log file.
"
" To execute only specific test functions, add a second argument. It will be
" matched against the names of the Test_ funtion. E.g.:
" ../vim -u NONE -S runtest.vim test_channel.vim open_delay
" The output can be found in the "messages" file.
"
" The test script may contain anything, only functions that start with
" "Test_" are special. These will be invoked and should contain assert
" functions. See test_assert.vim for an example.
"
" It is possible to source other files that contain "Test_" functions. This
" can speed up testing, since Vim does not need to restart. But be careful
" that the tests do not interfere with each other.
"
" If an error cannot be detected properly with an assert function add the
" error to the v:errors list:
" call add(v:errors, 'test foo failed: Cannot find xyz')
"
" If preparation for each Test_ function is needed, define a SetUp function.
" It will be called before each Test_ function.
"
" If cleanup after each Test_ function is needed, define a TearDown function.
" It will be called after each Test_ function.
"
" When debugging a test it can be useful to add messages to v:errors:
" call add(v:errors, "this happened")
set rtp=$VIM/vimfiles,$VIMRUNTIME,$VIM/vimfiles/after
if has('packages')
let &packpath = &rtp
endif
call ch_logfile( 'debuglog', 'w' )
" For consistency run all tests with 'nocompatible' set.
" This also enables use of line continuation.
set nocp viminfo+=nviminfo
" Use utf-8 by default, instead of whatever the system default happens to be.
" Individual tests can overrule this at the top of the file.
set encoding=utf-8
" Avoid stopping at the "hit enter" prompt
set nomore
" Output all messages in English.
lang mess C
" Always use forward slashes.
set shellslash
func RunTheTest(test)
echo 'Executing ' . a:test
" Avoid stopping at the "hit enter" prompt
set nomore
" Avoid a three second wait when a message is about to be overwritten by the
" mode message.
set noshowmode
" Clear any overrides.
call test_override('ALL', 0)
" Some tests wipe out buffers. To be consistent, always wipe out all
" buffers.
%bwipe!
" The test may change the current directory. Save and restore the
" directory after executing the test.
let save_cwd = getcwd()
if exists("*SetUp_" . a:test)
try
exe 'call SetUp_' . a:test
catch
call add(v:errors,
\ 'Caught exception in SetUp_' . a:test . ' before '
\ . a:test
\ . ': '
\ . v:exception
\ . ' @ '
\ . v:throwpoint)
endtry
endif
if exists("*SetUp")
try
call SetUp()
catch
call add(v:errors,
\ 'Caught exception in SetUp() before '
\ . a:test
\ . ': '
\ . v:exception
\ . ' @ '
\ . v:throwpoint)
endtry
endif
call add(s:messages, 'Executing ' . a:test)
let s:done += 1
if a:test =~ 'Test_nocatch_'
" Function handles errors itself. This avoids skipping commands after the
" error.
exe 'call ' . a:test
else
try
let s:test = a:test
au VimLeavePre * call EarlyExit(s:test)
exe 'call ' . a:test
au! VimLeavePre
catch /^\cskipped/
call add(s:messages, ' Skipped')
call add(s:skipped,
\ 'SKIPPED ' . a:test
\ . ': '
\ . substitute(v:exception, '^\S*\s\+', '', ''))
catch
call add(v:errors,
\ 'Caught exception in ' . a:test
\ . ': '
\ . v:exception
\ . ' @ '
\ . v:throwpoint)
endtry
endif
" In case 'insertmode' was set and something went wrong, make sure it is
" reset to avoid trouble with anything else.
set noinsertmode
if exists("*TearDown")
try
call TearDown()
catch
call add(v:errors,
\ 'Caught exception in TearDown() after ' . a:test
\ . ': '
\ . v:exception
\ . ' @ '
\ . v:throwpoint)
endtry
endif
if exists("*TearDown_" . a:test)
try
exe 'call TearDown_' . a:test
catch
call add(v:errors,
\ 'Caught exception in TearDown_' . a:test . ' after ' . a:test
\ . ': '
\ . v:exception
\ . ' @ '
\ . v:throwpoint)
endtry
endif
" Clear any autocommands
au!
" Close any extra tab pages and windows and make the current one not modified.
while tabpagenr('$') > 1
quit!
endwhile
while 1
let wincount = winnr('$')
if wincount == 1
break
endif
bwipe!
if wincount == winnr('$')
" Did not manage to close a window.
only!
break
endif
endwhile
exe 'cd ' . save_cwd
endfunc
func AfterTheTest()
if len(v:errors) > 0
let s:fail += 1
call add(s:errors, 'Found errors in ' . s:test . ':')
call extend(s:errors, v:errors)
let v:errors = []
endif
endfunc
func EarlyExit(test)
" It's OK for the test we use to test the quit detection.
if a:test != 'Test_zz_quit_detected()'
call add(v:errors, 'Test caused Vim to exit: ' . a:test)
endif
call FinishTesting()
endfunc
" This function can be called by a test if it wants to abort testing.
func FinishTesting()
call AfterTheTest()
" Don't write viminfo on exit.
set viminfo=
if s:fail == 0
" Success, create the .res file so that make knows it's done.
exe 'split ' . fnamemodify(g:testname, ':r') . '.res'
write
endif
if len(s:errors) > 0
" Append errors to test.log
split test.log
call append(line('$'), '')
call append(line('$'), 'From ' . g:testname . ':')
call append(line('$'), s:errors)
write
endif
if s:done == 0
let message = 'NO tests executed'
else
let message = 'Executed ' . s:done . (s:done > 1 ? ' tests' : ' test')
endif
echo message
call add(s:messages, message)
if s:fail > 0
let message = s:fail . ' FAILED:'
echo message
call add(s:messages, message)
call extend(s:messages, s:errors)
endif
" Add SKIPPED messages
call extend(s:messages, s:skipped)
" Append messages to the file "messages"
split messages
call append(line('$'), '')
call append(line('$'), 'From ' . g:testname . ':')
call append(line('$'), s:messages)
write
if s:fail > 0
cquit!
else
qall!
endif
endfunc
" Source the test script. First grab the file name, in case the script
" navigates away. g:testname can be used by the tests.
let g:testname = expand('%')
let s:done = 0
let s:fail = 0
let s:errors = []
let s:messages = []
let s:skipped = []
try
source %
catch
let s:fail += 1
call add(s:errors,
\ 'Caught exception: ' .
\ v:exception .
\ ' @ ' . v:throwpoint)
endtry
" Names of flaky tests.
let s:flaky_tests = []
" Pattern indicating a common flaky test failure.
let s:flaky_errors_re = '__does_not_match__'
" Locate Test_ functions and execute them.
redir @q
silent function /^Test_
redir END
let s:tests = split(substitute(@q, 'function \(\k*()\)', '\1', 'g'))
" If there is an extra argument filter the function names against it.
if argc() > 1
let s:tests = filter(s:tests, 'v:val =~ argv(1)')
endif
" Execute the tests in alphabetical order.
for s:test in sort(s:tests)
" Silence, please!
set belloff=all
let prev_error = ''
let total_errors = []
let run_nr = 1
call RunTheTest(s:test)
" Repeat a flaky test. Give up when:
" - it fails again with the same message
" - it fails five times (with a different mesage)
if len(v:errors) > 0
\ && (index(s:flaky_tests, s:test) >= 0
\ || v:errors[0] =~ s:flaky_errors_re)
while 1
call add(s:messages, 'Found errors in ' . s:test . ':')
call extend(s:messages, v:errors)
call add(total_errors, 'Run ' . run_nr . ':')
call extend(total_errors, v:errors)
if run_nr == 5 || prev_error == v:errors[0]
call add(total_errors, 'Flaky test failed too often, giving up')
let v:errors = total_errors
break
endif
call add(s:messages, 'Flaky test failed, running it again')
" Flakiness is often caused by the system being very busy. Sleep a
" couple of seconds to have a higher chance of succeeding the second
" time.
sleep 2
let prev_error = v:errors[0]
let v:errors = []
let run_nr += 1
call RunTheTest(s:test)
if len(v:errors) == 0
" Test passed on rerun.
break
endif
endwhile
endif
call AfterTheTest()
endfor
call FinishTesting()
" vim: shiftwidth=2 sts=2 expandtab

7
tests/testdata/cpp/simple.cpp vendored Normal file
View file

@ -0,0 +1,7 @@
#include <iostream>
int main( int argc, char ** )
{
printf( "this is a test %d", argc );
return 0;
}

6
tests/vimrc Normal file
View file

@ -0,0 +1,6 @@
let g:vimspector_test_plugin_path = expand( '<sfile>:h:h' )
let &rtp = &rtp . ',' . g:vimspector_test_plugin_path
filetype plugin indent on
syntax enable