UltiSnips/test/vim_test_case.py

467 lines
15 KiB
Python
Raw Normal View History

# encoding: utf-8
# pylint: skip-file
import os
import re
import shutil
import subprocess
import tempfile
import textwrap
import time
import unittest
2015-02-08 14:34:38 -05:00
from test.constant import (ARR_D, ARR_L, ARR_R, ARR_U, BS, ESC, PYTHON3,
SEQUENCES)
2015-01-20 15:26:03 -05:00
def is_process_running(pid):
"""Returns true if a process with pid is running, false otherwise."""
2015-01-20 15:26:03 -05:00
# from
# http://stackoverflow.com/questions/568271/how-to-check-if-there-exists-a-process-with-a-given-pid
try:
os.kill(pid, 0)
except OSError:
return False
else:
return True
2015-01-20 15:26:03 -05:00
def silent_call(cmd):
"""Calls 'cmd' and returns the exit value."""
return subprocess.call(cmd, stderr=subprocess.PIPE, stdout=subprocess.PIPE)
2015-01-20 15:26:03 -05:00
def create_directory(dirname):
"""Creates 'dirname' and its parents if it does not exist."""
try:
os.makedirs(dirname)
except OSError:
pass
2015-01-20 15:26:03 -05:00
def plugin_cache_dir():
"""The directory that we check out our bundles to."""
2015-01-20 15:26:03 -05:00
return os.path.join(tempfile.gettempdir(), 'UltiSnips_test_vim_plugins')
def read_text_file(filename):
"""Reads the contens of a text file."""
if PYTHON3:
2015-01-20 15:26:03 -05:00
return open(filename, 'r', encoding='utf-8').read()
else:
2015-01-20 15:26:03 -05:00
return open(filename, 'r').read()
def wait_until_file_exists(file_path, times=None, interval=0.01):
while times is None or times:
if os.path.exists(file_path):
return True
time.sleep(interval)
if times is not None:
times -= 1
return False
2015-01-20 15:26:03 -05:00
class TempFileManager(object):
2015-01-20 15:26:03 -05:00
"""A TempFileManager keeps a unique prefix path for temp files.
A temp file, or a name for a temp file generate by a TempFileManager
always belongs to the same directory.
"""
2015-01-20 15:26:03 -05:00
def __init__(self, name=''):
"""The unique prefix path is UltiSnipsTest_{name}XXXXXX."""
self._temp_dir = tempfile.mkdtemp(prefix='UltiSnipsTest_' + name)
def name_temp(self, file_path):
2015-01-20 15:26:03 -05:00
"""Get the absolute path of a temp file by given file path."""
return os.path.join(self._temp_dir, file_path)
def write_temp(self, file_path, content):
2015-01-20 15:26:03 -05:00
"""Write the content to a temp file by given file path inside the
_temp_dir, and return the absolute path of that file."""
abs_path = self.name_temp(file_path)
create_directory(os.path.dirname(abs_path))
if PYTHON3:
2015-01-20 15:26:03 -05:00
with open(abs_path, 'w', encoding='utf-8') as f:
f.write(content)
else:
2015-01-20 15:26:03 -05:00
with open(abs_path, 'w') as f:
f.write(content)
return abs_path
2015-01-20 15:26:03 -05:00
def unique_name_temp(self, suffix='', prefix=''):
"""Generate a unique name for a temp file with given suffix and prefix,
and return full absolute path."""
file_handler, abspath = tempfile.mkstemp(
suffix, prefix, self._temp_dir)
os.close(file_handler)
os.remove(abspath)
return abspath
def clear_temp(self):
2015-01-20 15:26:03 -05:00
"""Clear the temp file directory, but the directory itself is not
removed."""
shutil.rmtree(self._temp_dir)
create_directory(self._temp_dir)
class VimInterface(TempFileManager):
2015-01-20 15:26:03 -05:00
def __init__(self, name=''):
TempFileManager.__init__(self, name)
def get_buffer_data(self):
2015-01-20 15:26:03 -05:00
buffer_path = self.unique_name_temp(prefix='buffer_')
self.send(ESC + ':w! %s\n' % buffer_path)
if wait_until_file_exists(buffer_path, 50):
return read_text_file(buffer_path)[:-1]
def send(self, s):
raise NotImplementedError()
def launch(self, config=[]):
2015-01-20 15:26:03 -05:00
pid_file = self.name_temp('vim.pid')
done_file = self.name_temp('loading_done')
if os.path.exists(done_file):
os.remove(done_file)
post_config = []
2015-01-20 15:26:03 -05:00
post_config.append('%s << EOF' % ('py3' if PYTHON3 else 'py'))
post_config.append('import vim')
post_config.append(
"with open('%s', 'w') as pid_file: pid_file.write(vim.eval('getpid()'))" %
pid_file)
post_config.append(
"with open('%s', 'w') as done_file: pass" %
done_file)
post_config.append('EOF')
config_path = self.write_temp('vim_config.vim',
textwrap.dedent(os.linesep.join(config + post_config) + '\n'))
# Note the space to exclude it from shell history.
self.send(""" vim -u %s\r\n""" % config_path)
wait_until_file_exists(done_file)
2015-01-20 15:26:03 -05:00
self._vim_pid = int(open(pid_file, 'r').read())
def leave_with_wait(self):
2015-01-20 15:26:03 -05:00
self.send(3 * ESC + ':qa!\n')
while is_process_running(self._vim_pid):
time.sleep(.05)
class VimInterfaceScreen(VimInterface):
2015-01-20 15:26:03 -05:00
def __init__(self, session):
2015-01-20 15:26:03 -05:00
VimInterface.__init__(self, 'Screen')
self.session = session
self.need_screen_escapes = 0
self.detect_parsing()
def send(self, s):
if self.need_screen_escapes:
# escape characters that are special to some versions of screen
repl = lambda m: '\\' + m.group(0)
2015-01-20 15:26:03 -05:00
s = re.sub(r"[$^#\\']", repl, s)
if PYTHON3:
2015-01-20 15:26:03 -05:00
s = s.encode('utf-8')
while True:
rv = 0
if len(s) > 30:
2015-01-20 15:26:03 -05:00
rv |= silent_call(
['screen', '-x', self.session, '-X', 'register', 'S', s])
rv |= silent_call(
['screen', '-x', self.session, '-X', 'paste', 'S'])
else:
2015-01-20 15:26:03 -05:00
rv |= silent_call(
['screen', '-x', self.session, '-X', 'stuff', s])
if not rv:
break
time.sleep(.2)
def detect_parsing(self):
self.launch()
2015-01-20 15:26:03 -05:00
# Send a string where the interpretation will depend on version of
# screen
string = '$TERM'
self.send('i' + string + ESC)
output = self.get_buffer_data()
# If the output doesn't match the input, need to do additional escaping
if output != string:
self.need_screen_escapes = 1
self.leave_with_wait()
2015-01-20 15:26:03 -05:00
class VimInterfaceTmux(VimInterface):
2015-01-20 15:26:03 -05:00
def __init__(self, session):
self.session = session
self._check_version()
def send(self, s):
# I did not find any documentation on what needs escaping when sending
# to tmux, but it seems like this is all that is needed for now.
s = s.replace(';', r'\;')
if PYTHON3:
2015-01-20 15:26:03 -05:00
s = s.encode('utf-8')
silent_call(['tmux', 'send-keys', '-t', self.session, '-l', s])
def _check_version(self):
2015-01-20 15:26:03 -05:00
stdout, _ = subprocess.Popen(['tmux', '-V'],
stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
if PYTHON3:
2015-01-20 15:26:03 -05:00
stdout = stdout.decode('utf-8')
m = re.match(r"tmux (\d+).(\d+)", stdout)
if not m or not (int(m.group(1)), int(m.group(2))) >= (1, 9):
2015-01-20 15:26:03 -05:00
raise RuntimeError(
'Need at least tmux 1.9, you have %s.' %
stdout.strip())
class VimInterfaceWindows(VimInterface):
2015-01-20 15:26:03 -05:00
BRACES = re.compile('([}{])')
WIN_ESCAPES = ['+', '^', '%', '~', '[', ']', '<', '>', '(', ')']
WIN_REPLACES = [
2015-01-20 15:26:03 -05:00
(BS, '{BS}'),
(ARR_L, '{LEFT}'),
(ARR_R, '{RIGHT}'),
(ARR_U, '{UP}'),
(ARR_D, '{DOWN}'),
('\t', '{TAB}'),
('\n', '~'),
(ESC, '{ESC}'),
# On my system ` waits for a second keystroke, so `+SPACE = "`". On
# most systems, `+Space = "` ". I work around this, by sending the host
# ` as `+_+BS. Awkward, but the only way I found to get this working.
('`', '`_{BS}'),
('´', '´_{BS}'),
('{^}', '{^}_{BS}'),
]
def __init__(self):
self.seq_buf = []
# import windows specific modules
2015-01-20 15:26:03 -05:00
import win32com.client
import win32gui
self.win32gui = win32gui
2015-01-20 15:26:03 -05:00
self.shell = win32com.client.Dispatch('WScript.Shell')
def is_focused(self, title=None):
2015-01-20 15:26:03 -05:00
cur_title = self.win32gui.GetWindowText(
self.win32gui.GetForegroundWindow())
if (title or '- GVIM') in cur_title:
return True
return False
def focus(self, title=None):
2015-01-20 15:26:03 -05:00
if not self.shell.AppActivate(title or '- GVIM'):
raise Exception('Failed to switch to GVim window')
time.sleep(1)
def convert_keys(self, keys):
keys = self.BRACES.sub(r"{\1}", keys)
for k in self.WIN_ESCAPES:
2015-01-20 15:26:03 -05:00
keys = keys.replace(k, '{%s}' % k)
for f, r in self.WIN_REPLACES:
keys = keys.replace(f, r)
return keys
def send(self, keys):
self.seq_buf.append(keys)
2015-01-20 15:26:03 -05:00
seq = ''.join(self.seq_buf)
for f in SEQUENCES:
if f.startswith(seq) and f != seq:
return
self.seq_buf = []
seq = self.convert_keys(seq)
if not self.is_focused():
time.sleep(2)
self.focus()
if not self.is_focused():
# This is the only way I can find to stop test execution
2015-01-20 15:26:03 -05:00
raise KeyboardInterrupt('Failed to focus GVIM')
self.shell.SendKeys(seq)
class VimTestCase(unittest.TestCase, TempFileManager):
snippets = ()
files = {}
2015-01-20 15:26:03 -05:00
text_before = ' --- some text before --- \n\n'
text_after = '\n\n --- some text after --- '
expected_error = ''
wanted = ''
keys = ''
sleeptime = 0.00
2015-01-20 15:26:03 -05:00
output = ''
plugins = []
# Skip this test for the given reason or None for not skipping it.
skip_if = lambda self: None
version = None # Will be set to vim --version output
def __init__(self, *args, **kwargs):
unittest.TestCase.__init__(self, *args, **kwargs)
2015-01-20 15:26:03 -05:00
TempFileManager.__init__(self, 'Case')
def runTest(self):
# Only checks the output. All work is done in setUp().
wanted = self.text_before + self.wanted + self.text_after
if self.expected_error:
self.assertRegexpMatches(self.output, self.expected_error)
return
for i in range(self.retries):
if self.output != wanted:
# Redo this, but slower
self.sleeptime += 0.02
self.tearDown()
self.setUp()
self.assertEqual(self.output, wanted)
def _extra_options_pre_init(self, vim_config):
"""Adds extra lines to the vim_config list."""
def _extra_options_post_init(self, vim_config):
"""Adds extra lines to the vim_config list."""
def _before_test(self):
2015-01-20 15:26:03 -05:00
"""Send these keys before the test runs.
Used for buffer local variables and other options.
"""
def _create_file(self, file_path, content):
"""Creates a file in the runtimepath that is created for this test.
2015-01-20 15:26:03 -05:00
Returns the absolute path to the file.
"""
return self.write_temp(file_path, textwrap.dedent(content + '\n'))
def _link_file(self, source, relative_destination):
2015-01-20 15:26:03 -05:00
"""Creates a link from 'source' to the 'relative_destination' in our
temp dir."""
absdir = self.name_temp(relative_destination)
create_directory(absdir)
os.symlink(source, os.path.join(absdir, os.path.basename(source)))
def setUp(self):
if not VimTestCase.version:
2015-01-20 15:26:03 -05:00
VimTestCase.version, _ = subprocess.Popen(['vim', '--version'],
stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
if PYTHON3:
2015-01-20 15:26:03 -05:00
VimTestCase.version = VimTestCase.version.decode('utf-8')
if self.plugins and not self.test_plugins:
2015-01-20 15:26:03 -05:00
return self.skipTest('Not testing integration with other plugins.')
reason_for_skipping = self.skip_if()
if reason_for_skipping is not None:
return self.skipTest(reason_for_skipping)
vim_config = []
vim_config.append('set nocompatible')
vim_config.append('set runtimepath=$VIMRUNTIME,%s,%s' % (
os.path.dirname(os.path.dirname(__file__)), self._temp_dir))
if self.plugins:
2015-01-20 15:26:03 -05:00
self._link_file(
os.path.join(
plugin_cache_dir(),
'vim-pathogen',
'autoload'),
'.')
for plugin in self.plugins:
2015-01-20 15:26:03 -05:00
self._link_file(
os.path.join(
plugin_cache_dir(),
os.path.basename(plugin)),
'bundle')
vim_config.append('execute pathogen#infect()')
# Vim parameters.
vim_config.append('syntax on')
vim_config.append('filetype plugin indent on')
vim_config.append('set clipboard=""')
vim_config.append('set encoding=utf-8')
vim_config.append('set fileencoding=utf-8')
vim_config.append('set buftype=nofile')
vim_config.append('set shortmess=at')
vim_config.append('let @" = ""')
vim_config.append('let g:UltiSnipsExpandTrigger="<tab>"')
vim_config.append('let g:UltiSnipsJumpForwardTrigger="?"')
vim_config.append('let g:UltiSnipsJumpBackwardTrigger="+"')
vim_config.append('let g:UltiSnipsListSnippets="@"')
2015-01-20 15:26:03 -05:00
vim_config.append(
'let g:UltiSnipsUsePythonVersion="%i"' %
(3 if PYTHON3 else 2))
vim_config.append('let g:UltiSnipsSnippetDirectories=["us"]')
self._extra_options_pre_init(vim_config)
# Now activate UltiSnips.
vim_config.append('call UltiSnips#bootstrap#Bootstrap()')
self._extra_options_post_init(vim_config)
# Finally, add the snippets and some configuration for the test.
2015-01-20 15:26:03 -05:00
vim_config.append('%s << EOF' % ('py3' if PYTHON3 else 'py'))
2015-01-20 15:26:03 -05:00
if len(self.snippets) and not isinstance(self.snippets[0], tuple):
self.snippets = (self.snippets, )
for s in self.snippets:
sv, content = s[:2]
2015-01-20 15:26:03 -05:00
description = ''
options = ''
priority = 0
if len(s) > 2:
description = s[2]
if len(s) > 3:
options = s[3]
if len(s) > 4:
priority = s[4]
2015-01-20 15:26:03 -05:00
vim_config.append('UltiSnips_Manager.add_snippet(%r, %r, %r, %r, priority=%i)' % (
sv, content, description, options, priority))
# fill buffer with default text and place cursor in between.
prefilled_text = (self.text_before + self.text_after).splitlines()
2015-01-20 15:26:03 -05:00
vim_config.append('vim.current.buffer[:] = %r\n' % prefilled_text)
vim_config.append(
'vim.current.window.cursor = (max(len(vim.current.buffer)//2, 1), 0)')
# End of python stuff.
2015-01-20 15:26:03 -05:00
vim_config.append('EOF')
for name, content in self.files.items():
self._create_file(name, content)
self.vim.launch(vim_config)
self._before_test()
if not self.interrupt:
# Go into insert mode and type the keys but leave Vim some time to
# react.
for c in 'i' + self.keys:
self.vim.send(c)
time.sleep(self.sleeptime)
self.output = self.vim.get_buffer_data()
def tearDown(self):
if self.interrupt:
2015-01-20 15:26:03 -05:00
print('Working directory: %s' % (self._temp_dir))
return
self.vim.leave_with_wait()
self.clear_temp()
# vim:fileencoding=utf-8:foldmarker={{{#,#}}}: