Merge pull request #1265 from ZyX-I/fix-1264

Fix tmux bindings and add functional test for tmux
This commit is contained in:
Nikolai Aleksandrovich Pavlov 2015-01-17 00:55:49 +03:00
commit e02e6dc1c7
10 changed files with 497 additions and 17 deletions

View File

@ -65,8 +65,6 @@ the absolute path to the Powerline installation directory:
set fish_function_path $fish_function_path "{repository_root}/powerline/bindings/fish"
powerline-setup
.. _tmux-statusline:
Rcsh prompt
===========
@ -77,7 +75,7 @@ many \*nix distributions. To use it add
. {repository_root}/powerline/bindings/rc/powerline.rc
to :file:`rcrc` file (usually :file:`~/.rcrc`) and make sure ``rc`` is startead
to :file:`rcrc` file (usually :file:`~/.rcrc`) and make sure ``rc`` is started
as a login shell (with ``-l`` argument): otherwise this configuration file is
not read.

View File

@ -80,17 +80,17 @@ def source_tmux_files(pl, args):
class EmptyArgs(object):
def __init__(self, ext, config_path):
self.ext = ext
self.ext = [ext]
self.config_path = None
def __getattr__(self, attr):
return None
def init_environment(pl, args):
def init_tmux_environment(pl, args):
'''Initialize tmux environment from tmux configuration
'''
powerline = ShellPowerline(finish_args(EmptyArgs('tmux', args.config_path)))
powerline = ShellPowerline(finish_args(os.environ, EmptyArgs('tmux', args.config_path)))
# TODO Move configuration files loading out of Powerline object and use it
# directly
powerline.update_renderer()
@ -148,7 +148,12 @@ def init_environment(pl, args):
if attr == 'attrs':
attrs = attrs_to_tmux_attrs(get_highlighting(group)[attr])
set_tmux_environment(varname, ']#['.join(attrs))
set_tmux_environment(varname + '_LEGACY', ','.join(attrs))
set_tmux_environment(varname + '_LEGACY', (','.join(
# Tmux-1.6 does not accept no… attributes in
# window-status-…-attr options.
(attr for attr in attrs if not attr.startswith('no')))
# But it does not support empty attributes as well.
or 'none'))
else:
set_tmux_environment(varname, 'colour' + str(get_highlighting(group)[attr][0]))
@ -159,6 +164,11 @@ def init_environment(pl, args):
' ' * powerline.renderer.strwidth(left_dividers['hard'])))
def tmux_setup(pl, args):
init_tmux_environment(pl, args)
source_tmux_files(pl, args)
def get_main_config(args):
find_config_files = generate_config_finder()
config_loader = ConfigLoader(run_once=True)

View File

@ -1,9 +1,2 @@
if-shell 'test -z "$POWERLINE_CONFIG_COMMAND"' 'set-environment -g POWERLINE_CONFIG_COMMAND powerline-config'
run-shell 'eval $POWERLINE_CONFIG_COMMAND tmux setenv'
# Simplify tmux version checking by using multiple config files. Source these
# config files based on the version in which tmux features were added and/or
# deprecated. By splitting these configuration options into separate files,
run-shell 'eval $POWERLINE_CONFIG_COMMAND tmux source'
if-shell 'test -z "$POWERLINE_CONFIG_COMMAND"' 'set-environment -g POWERLINE_CONFIG_COMMAND powerline-config; run-shell "env \"\$POWERLINE_CONFIG_COMMAND\" tmux setup"' 'run-shell "env \"\$POWERLINE_CONFIG_COMMAND\" tmux setup"'
# vim: ft=tmux

View File

@ -20,7 +20,8 @@ class StrFunction(object):
TMUX_ACTIONS = {
'source': StrFunction(config.source_tmux_files, 'source'),
'setenv': StrFunction(config.init_environment, 'setenv'),
'setenv': StrFunction(config.init_tmux_environment, 'setenv'),
'setup': StrFunction(config.tmux_setup, 'setup'),
}

View File

@ -5,7 +5,7 @@ git clone --depth=1 git://github.com/powerline/deps tests/bot-ci/deps
. tests/bot-ci/scripts/common/main.sh
sudo apt-get install -qq libssl1.0.0
sudo apt-get install -qq screen zsh tcsh mksh busybox socat realpath bc rc
sudo apt-get install -qq screen zsh tcsh mksh busybox socat realpath bc rc tmux
if test -n "$USE_UCS2_PYTHON" ; then
pip install virtualenvwrapper

59
tests/lib/terminal.py Normal file
View File

@ -0,0 +1,59 @@
# vim:fileencoding=utf-8:noet
from __future__ import (unicode_literals, division, absolute_import, print_function)
import threading
from time import sleep
import pexpect
from tests.lib.vterm import VTerm
class ExpectProcess(threading.Thread):
def __init__(self, lib, rows, cols, cmd, args, cwd=None, env=None):
super(ExpectProcess, self).__init__()
self.vterm = VTerm(lib, rows, cols)
self.lock = threading.Lock()
self.rows = rows
self.cols = cols
self.cmd = cmd
self.args = args
self.cwd = cwd
self.env = env
self.buffer = []
self.child_lock = threading.Lock()
def run(self):
child = pexpect.spawn(self.cmd, self.args, cwd=self.cwd, env=self.env)
sleep(0.5)
child.setwinsize(self.rows, self.cols)
self.child = child
status = None
while status is None:
try:
with self.child_lock:
s = child.read_nonblocking(size=1024, timeout=0)
status = child.status
except pexpect.TIMEOUT:
pass
except pexpect.EOF:
break
else:
with self.lock:
self.vterm.push(s)
self.buffer.append(s)
def __getitem__(self, position):
with self.lock:
return self.vterm.vtscreen[position]
def read(self):
with self.lock:
ret = b''.join(self.buffer)
del self.buffer[:]
return ret
def send(self, data):
with self.child_lock:
self.child.send(data)

178
tests/lib/vterm.py Normal file
View File

@ -0,0 +1,178 @@
# vim:fileencoding=utf-8:noet
from __future__ import (unicode_literals, division, absolute_import, print_function)
import ctypes
from powerline.lib.unicode import unicode, unichr, tointiter
class CTypesFunction(object):
def __init__(self, library, name, rettype, args):
self.name = name
self.prototype = ctypes.CFUNCTYPE(rettype, *[
arg[1] for arg in args
])
self.args = args
self.func = self.prototype((name, library), tuple((
(1, arg[0]) for arg in args
)))
def __call__(self, *args, **kwargs):
return self.func(*args, **kwargs)
def __repr__(self):
return '{cls}(<library>, {name!r}, {rettype!r}, {args!r})'.format(
cls=self.__class__.__name__,
**self.__dict__
)
class CTypesLibraryFuncsCollection(object):
def __init__(self, lib, **kwargs):
self.lib = lib
library_loader = ctypes.LibraryLoader(ctypes.CDLL)
library = library_loader.LoadLibrary(lib)
self.library = library
for name, args in kwargs.items():
self.__dict__[name] = CTypesFunction(library, name, *args)
class VTermPos_s(ctypes.Structure):
_fields_ = (
('row', ctypes.c_int),
('col', ctypes.c_int),
)
class VTermColor_s(ctypes.Structure):
_fields_ = (
('red', ctypes.c_uint8),
('green', ctypes.c_uint8),
('blue', ctypes.c_uint8),
)
class VTermScreenCellAttrs_s(ctypes.Structure):
_fields_ = (
('bold', ctypes.c_uint, 1),
('underline', ctypes.c_uint, 2),
('italic', ctypes.c_uint, 1),
('blink', ctypes.c_uint, 1),
('reverse', ctypes.c_uint, 1),
('strike', ctypes.c_uint, 1),
('font', ctypes.c_uint, 4),
('dwl', ctypes.c_uint, 1),
('dhl', ctypes.c_uint, 2),
)
VTERM_MAX_CHARS_PER_CELL = 6
class VTermScreenCell_s(ctypes.Structure):
_fields_ = (
('chars', ctypes.ARRAY(ctypes.c_uint32, VTERM_MAX_CHARS_PER_CELL)),
('width', ctypes.c_char),
('attrs', VTermScreenCellAttrs_s),
('fg', VTermColor_s),
('bg', VTermColor_s),
)
VTerm_p = ctypes.c_void_p
VTermScreen_p = ctypes.c_void_p
def get_functions(lib):
return CTypesLibraryFuncsCollection(
lib,
vterm_new=(VTerm_p, (
('rows', ctypes.c_int),
('cols', ctypes.c_int)
)),
vterm_obtain_screen=(VTermScreen_p, (('vt', VTerm_p),)),
vterm_screen_reset=(None, (
('screen', VTermScreen_p),
('hard', ctypes.c_int)
)),
vterm_input_write=(ctypes.c_size_t, (
('vt', VTerm_p),
('bytes', ctypes.POINTER(ctypes.c_char)),
('size', ctypes.c_size_t),
)),
vterm_screen_get_cell=(ctypes.c_int, (
('screen', VTermScreen_p),
('pos', VTermPos_s),
('cell', ctypes.POINTER(VTermScreenCell_s))
)),
vterm_free=(None, (('vt', VTerm_p),)),
)
class VTermColor(object):
__slots__ = ('red', 'green', 'blue')
def __init__(self, color):
self.red = color.red
self.green = color.green
self.blue = color.blue
@property
def color_key(self):
return (self.red, self.green, self.blue)
class VTermScreenCell(object):
def __init__(self, vtsc):
for field in VTermScreenCellAttrs_s._fields_:
field_name = field[0]
setattr(self, field_name, getattr(vtsc.attrs, field_name))
self.text = ''.join((
unichr(vtsc.chars[i]) for i in range(VTERM_MAX_CHARS_PER_CELL)
)).rstrip('\x00')
self.width = next(tointiter(vtsc.width))
self.fg = VTermColor(vtsc.fg)
self.bg = VTermColor(vtsc.bg)
self.cell_properties_key = (
self.fg.color_key,
self.bg.color_key,
self.bold,
self.underline,
self.italic,
)
class VTermScreen(object):
def __init__(self, functions, screen):
self.functions = functions
self.screen = screen
def __getitem__(self, position):
pos = VTermPos_s(*position)
cell = VTermScreenCell_s()
ret = self.functions.vterm_screen_get_cell(self.screen, pos, cell)
if ret != 1:
raise ValueError('vterm_screen_get_cell returned {0}'.format(ret))
return VTermScreenCell(cell)
def reset(self, hard):
self.functions.vterm_screen_reset(self.screen, int(bool(hard)))
class VTerm(object):
def __init__(self, lib, rows, cols):
self.functions = get_functions(lib)
self.vt = self.functions.vterm_new(rows, cols)
self.vtscreen = VTermScreen(self.functions, self.functions.vterm_obtain_screen(self.vt))
self.vtscreen.reset(True)
def push(self, data):
if isinstance(data, unicode):
data = data.encode('utf-8')
return self.functions.vterm_input_write(self.vt, data, len(data))
def __del__(self):
try:
self.functions.vterm_free(self.vt)
except AttributeError:
pass

8
tests/run_vterm_tests.sh Executable file
View File

@ -0,0 +1,8 @@
#!/bin/sh
set -e
FAILED=0
if ! sh tests/test_in_vterm/test.sh ; then
echo "Failed vterm"
FAILED=1
fi
exit $FAILED

53
tests/test_in_vterm/test.sh Executable file
View File

@ -0,0 +1,53 @@
#!/bin/sh
. tests/bot-ci/scripts/common/main.sh
set +x
: ${POWERLINE_TMUX_EXE:=tmux}
FAILED=0
rm -rf tests/vterm
mkdir tests/vterm
mkdir tests/vterm/path
ln -s "$(which "${PYTHON}")" tests/vterm/path/python
ln -s "$(which bash)" tests/vterm/path
ln -s "$(which env)" tests/vterm/path
ln -s "$PWD/scripts/powerline-render" tests/vterm/path
ln -s "$PWD/scripts/powerline-config" tests/vterm/path
FAIL_SUMMARY=""
test_tmux() {
if test "$PYTHON_IMPLEMENTATION" = PyPy && test "$PYTHON_VERSION_MAJOR" -eq 3 ; then
# FIXME PyPy3 segfaults for some reason
return 0
fi
if ! which "${POWERLINE_TMUX_EXE}" ; then
return 0
fi
ln -s "$(which "${POWERLINE_TMUX_EXE}")" tests/vterm/path
if ! "${PYTHON}" tests/test_in_vterm/test_tmux.py; then
echo "Failed vterm test $f"
FAILED=1
FAIL_SUMMARY="$FAIL_SUMMARY${NL}F $f"
for file in tests/vterm/*.log ; do
if ! test -e "$file" ; then
break
fi
echo '____________________________________________________________'
echo "$file:"
echo '============================================================'
cat -v $file
done
fi
}
test_tmux || true
if test $FAILED -eq 0 ; then
echo "$FAIL_SUMMARY"
rm -rf tests/vterm
fi
exit $FAILED

180
tests/test_in_vterm/test_tmux.py Executable file
View File

@ -0,0 +1,180 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8:noet
from __future__ import (unicode_literals, division, absolute_import, print_function)
import os
from time import sleep
from subprocess import check_call
from itertools import groupby
from difflib import ndiff
from powerline.lib.unicode import u
from powerline.bindings.tmux import get_tmux_version
from powerline import get_fallback_logger
from tests.lib.terminal import ExpectProcess
def cell_properties_key_to_shell_escape(cell_properties_key):
fg, bg, bold, underline, italic = cell_properties_key
return('\x1b[38;2;{0};48;2;{1}{bold}{underline}{italic}m'.format(
';'.join((str(i) for i in fg)),
';'.join((str(i) for i in bg)),
bold=(';1' if bold else ''),
underline=(';4' if underline else ''),
italic=(';3' if italic else ''),
))
def main():
VTERM_TEST_DIR = os.path.abspath('tests/vterm')
vterm_path = os.path.join(VTERM_TEST_DIR, 'path')
socket_path = os.path.join(VTERM_TEST_DIR, 'tmux-socket')
rows = 50
cols = 200
tmux_exe = os.path.join(vterm_path, 'tmux')
try:
p = ExpectProcess(
lib='tests/bot-ci/deps/libvterm/libvterm.so',
rows=rows,
cols=cols,
cmd=tmux_exe,
args=[
# Specify full path to tmux socket (testing tmux instance must
# not interfere with user one)
'-S', socket_path,
# Force 256-color mode
'-2',
# Request verbose logging just in case
'-v',
# Specify configuration file
'-f', os.path.abspath('powerline/bindings/tmux/powerline.conf'),
# Run bash three times
'new-session', 'bash --norc --noprofile -i', ';',
'new-window', 'bash --norc --noprofile -i', ';',
'new-window', 'bash --norc --noprofile -i', ';',
],
cwd=VTERM_TEST_DIR,
env={
'TERM': 'vt100',
'PATH': vterm_path,
'SHELL': os.path.join(''),
'POWERLINE_CONFIG_PATHS': os.path.abspath('powerline/config_files'),
'POWERLINE_COMMAND': 'powerline-render',
'POWERLINE_THEME_OVERRIDES': (
'default.segments.right=[{"type":"string","name":"s1","highlight_groups":["cwd"]}];'
'default.segments.left=[{"type":"string","name":"s2","highlight_groups":["background"]}];'
'default.segment_data.s1.contents=S1 string here;'
'default.segment_data.s2.contents=S2 string here;'
),
'LD_LIBRARY_PATH': os.environ.get('LD_LIBRARY_PATH', ''),
'PYTHONPATH': os.environ.get('PYTHONPATH', ''),
},
)
p.start()
sleep(10)
last_line = []
for col in range(cols):
last_line.append(p[rows - 1, col])
result = tuple((
(key, ''.join((i.text for i in subline)))
for key, subline in groupby(last_line, lambda i: i.cell_properties_key)
))
expected_result_new = (
(((0, 0, 0), (243, 243, 243), 1, 0, 0), ' 0 '),
(((243, 243, 243), (11, 11, 11), 0, 0, 0), ' '),
(((255, 255, 255), (11, 11, 11), 0, 0, 0), ' S2 string here '),
(((133, 133, 133), (11, 11, 11), 0, 0, 0), ' 0 '),
(((88, 88, 88), (11, 11, 11), 0, 0, 0), '| '),
(((188, 188, 188), (11, 11, 11), 0, 0, 0), 'bash '),
(((255, 255, 255), (11, 11, 11), 0, 0, 0), ' '),
(((133, 133, 133), (11, 11, 11), 0, 0, 0), ' 1 '),
(((88, 88, 88), (11, 11, 11), 0, 0, 0), '| '),
(((0, 102, 153), (11, 11, 11), 0, 0, 0), 'bash '),
(((255, 255, 255), (11, 11, 11), 0, 0, 0), ' '),
(((11, 11, 11), (0, 102, 153), 0, 0, 0), ' '),
(((102, 204, 255), (0, 102, 153), 0, 0, 0), '2 | '),
(((255, 255, 255), (0, 102, 153), 1, 0, 0), 'bash '),
(((0, 102, 153), (11, 11, 11), 0, 0, 0), ' '),
(((255, 255, 255), (11, 11, 11), 0, 0, 0), ' '),
(((88, 88, 88), (11, 11, 11), 0, 0, 0), ' '),
(((199, 199, 199), (88, 88, 88), 0, 0, 0), ' S1 string here '),
)
expected_result_old = (
(((0, 0, 0), (243, 243, 243), 1, 0, 0), ' 0 '),
(((243, 243, 243), (11, 11, 11), 0, 0, 0), ' '),
(((255, 255, 255), (11, 11, 11), 0, 0, 0), ' S2 string here '),
(((133, 133, 133), (11, 11, 11), 0, 0, 0), ' 0 '),
(((88, 88, 88), (11, 11, 11), 0, 0, 0), '| '),
(((188, 188, 188), (11, 11, 11), 0, 0, 0), 'bash '),
(((255, 255, 255), (11, 11, 11), 0, 0, 0), ' '),
(((133, 133, 133), (11, 11, 11), 0, 0, 0), ' 1 '),
(((88, 88, 88), (11, 11, 11), 0, 0, 0), '| '),
(((188, 188, 188), (11, 11, 11), 0, 0, 0), 'bash '),
(((255, 255, 255), (11, 11, 11), 0, 0, 0), ' '),
(((11, 11, 11), (0, 102, 153), 0, 0, 0), ' '),
(((102, 204, 255), (0, 102, 153), 0, 0, 0), '2 | '),
(((255, 255, 255), (0, 102, 153), 1, 0, 0), 'bash '),
(((0, 102, 153), (11, 11, 11), 0, 0, 0), ' '),
(((255, 255, 255), (11, 11, 11), 0, 0, 0), ' '),
(((88, 88, 88), (11, 11, 11), 0, 0, 0), ' '),
(((199, 199, 199), (88, 88, 88), 0, 0, 0), ' S1 string here '),
)
print('Result:')
shesc_result = ''.join((
'{0}{1}\x1b[m'.format(cell_properties_key_to_shell_escape(key), text)
for key, text in result
))
print(shesc_result)
print('Expected:')
tmux_version = get_tmux_version(get_fallback_logger())
if tmux_version < (1, 8):
expected_result = expected_result_old
else:
expected_result = expected_result_new
shesc_expected_result = ''.join((
'{0}{1}\x1b[m'.format(cell_properties_key_to_shell_escape(key), text)
for key, text in expected_result
))
print(shesc_expected_result)
if result == expected_result:
return True
else:
p.send(b'powerline-config tmux setup\n')
sleep(5)
print('Screen:')
screen = []
for i in range(rows):
screen.append([])
for j in range(cols):
screen[-1].append(p[i, j])
print('\n'.join(
''.join((
'{0}{1}\x1b[m'.format(
cell_properties_key_to_shell_escape(i.cell_properties_key),
i.text
) for i in line
))
for line in screen
))
a = shesc_result.replace('\x1b', '\\e') + '\n'
b = shesc_expected_result.replace('\x1b', '\\e') + '\n'
print('_' * 80)
print('Diff:')
print('=' * 80)
print(''.join((u(line) for line in ndiff([a], [b]))))
return False
finally:
check_call([tmux_exe, '-S', socket_path, 'kill-server'], env={
'PATH': vterm_path,
})
if __name__ == '__main__':
if main():
raise SystemExit(0)
else:
raise SystemExit(1)