From b371f3db0efde04b98e15bf5d1668fbcc615d55a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 4 Jul 2026 01:07:11 +0000 Subject: [PATCH] Refactor kitty_tests: move __init__.py code to base.py, update all imports --- kitty_tests/__init__.py | 515 ----------------------------- kitty_tests/atexit.py | 2 +- kitty_tests/base.py | 515 +++++++++++++++++++++++++++++ kitty_tests/check_build.py | 2 +- kitty_tests/clipboard.py | 2 +- kitty_tests/command_palette.py | 2 +- kitty_tests/completion.py | 2 +- kitty_tests/crypto.py | 2 +- kitty_tests/datatypes.py | 2 +- kitty_tests/dnd.py | 2 +- kitty_tests/dnd_kitten.py | 2 +- kitty_tests/file_transmission.py | 2 +- kitty_tests/fonts.py | 2 +- kitty_tests/glfw.py | 2 +- kitty_tests/graphics.py | 2 +- kitty_tests/keys.py | 2 +- kitty_tests/layout.py | 2 +- kitty_tests/main.py | 2 +- kitty_tests/mouse.py | 2 +- kitty_tests/multicell.py | 4 +- kitty_tests/notifications.py | 2 +- kitty_tests/open_actions.py | 2 +- kitty_tests/options.py | 2 +- kitty_tests/parser.py | 2 +- kitty_tests/screen.py | 2 +- kitty_tests/search_query_parser.py | 2 +- kitty_tests/shell_integration.py | 2 +- kitty_tests/shm.py | 2 +- kitty_tests/slang.py | 2 +- kitty_tests/ssh.py | 2 +- kitty_tests/tab_bar.py | 2 +- kitty_tests/tui.py | 2 +- kitty_tests/utmp.py | 2 +- 33 files changed, 547 insertions(+), 547 deletions(-) create mode 100644 kitty_tests/base.py diff --git a/kitty_tests/__init__.py b/kitty_tests/__init__.py index 8c2c4a602..e69de29bb 100644 --- a/kitty_tests/__init__.py +++ b/kitty_tests/__init__.py @@ -1,515 +0,0 @@ -#!/usr/bin/env python -# License: GPL v3 Copyright: 2016, Kovid Goyal - -import fcntl -import io -import os -import select -import shlex -import shutil -import signal -import struct -import sys -import termios -import time -from contextlib import contextmanager, suppress -from functools import wraps -from pty import CHILD, STDIN_FILENO, STDOUT_FILENO, fork -from unittest import TestCase - -from kitty.config import finalize_keys, finalize_mouse_mappings -from kitty.fast_data_types import TEXT_SIZE_CODE, Cursor, HistoryBuf, LineBuf, Screen, get_options, monotonic, set_options -from kitty.options.parse import merge_result_dicts -from kitty.options.types import Options, defaults -from kitty.rgb import to_color -from kitty.types import MouseEvent -from kitty.utils import read_screen_size -from kitty.window import da1, decode_cmdline, process_remote_print, process_title_from_child - - -def parse_bytes(screen, data, dump_callback=None): - data = memoryview(data) - while data: - dest = screen.test_create_write_buffer() - s = screen.test_commit_write_buffer(data, dest) - data = data[s:] - screen.test_parse_written_data(dump_callback) - - -def draw_multicell( - screen: Screen, text: str, width: int = 0, scale: int = 1, subscale_n: int = 0, subscale_d: int = 0, vertical_align: int = 0, horizontal_align: int = 0 - ) -> None: - cmd = f'\x1b]{TEXT_SIZE_CODE};w={width}:s={scale}:n={subscale_n}:d={subscale_d}:v={vertical_align}:h={horizontal_align};{text}\a' - parse_bytes(screen, cmd.encode()) - - -class Callbacks: - - def __init__(self, pty=None) -> None: - self.clear() - self.pty = pty - self.ftc = None - self.set_pointer_shape = lambda data: None - self.last_cmd_at = 0 - self.last_cmd_cmdline = '' - self.last_cmd_exit_status = sys.maxsize - - def write(self, data) -> None: - self.wtcbuf += bytes(data) - - def notify_child_of_resize(self): - self.num_of_resize_events += 1 - - def on_reset(self) -> None: - if self.pty is not None: - self.pty.reset_termios_state() - - def color_control(self, code, data) -> None: - from kitty.window import color_control - response = color_control(self.color_profile, code, data) - if response: - def p(x): - if '@' in x: - return (to_color(x.partition('@')[0]), int(255 * float(x.partition('@')[2]))) - ans = to_color(x) - if ans is None: - ans = x - return ans - parts = {x.partition('=')[0]:p(x.partition('=')[2]) for x in response.split(';')[1:]} - self.color_control_responses.append(parts) - - def title_changed(self, data, is_base64=False) -> None: - self.titlebuf.append(process_title_from_child(data, is_base64, '')) - - def osc_context(self, data): - pass - - def icon_changed(self, data) -> None: - self.iconbuf += str(data, 'utf-8') - - def set_dynamic_color(self, code, data='') -> None: - if code == 22: - self.set_pointer_shape(data) - else: - self.colorbuf += str(data or b'', 'utf-8') - - def set_color_table_color(self, code, data='') -> None: - self.ctbuf += '' - - def color_profile_popped(self, x) -> None: - pass - - def cmd_output_marking(self, is_start: bool | None, data: str = '') -> None: - if is_start: - self.last_cmd_at = monotonic() - self.last_cmd_cmdline = decode_cmdline(data) if data else data - else: - if self.last_cmd_at != 0: - self.last_cmd_at = 0 - with suppress(Exception): - self.last_cmd_exit_status = int(data) - - def request_capabilities(self, q) -> None: - from kitty.terminfo import get_capabilities - for c in get_capabilities(q, None): - self.write(c.encode('ascii')) - - def desktop_notify(self, osc_code: int, raw_data: memoryview) -> None: - self.notifications.append((osc_code, str(raw_data, 'utf-8'))) - - def open_url(self, url: str, hyperlink_id: int) -> None: - self.open_urls.append((url, hyperlink_id)) - - def clipboard_control(self, data: memoryview, is_partial: bool = False) -> None: - self.cc_buf.append((str(data, 'utf-8'), is_partial)) - - def clear(self) -> None: - self.wtcbuf = b'' - self.iconbuf = self.colorbuf = self.ctbuf = '' - self.titlebuf = [] - self.printbuf = [] - self.color_control_responses = [] - self.notifications = [] - self.open_urls = [] - self.cc_buf = [] - self.bell_count = 0 - self.clone_cmds = [] - self.current_clone_data = '' - self.last_cmd_exit_status = sys.maxsize - self.last_cmd_cmdline = '' - self.last_cmd_at = 0 - self.num_of_resize_events = 0 - self.da1 = [] - - def on_bell(self) -> None: - self.bell_count += 1 - - def on_da1(self) -> None: - opts = None - with suppress(RuntimeError): - opts = get_options() - if opts is None: - opts = defaults - payload = da1(opts) - self.da1.append(payload) - if self.pty and self.pty.needs_da1: - self.pty.send_da1_response(payload) - - def on_activity_since_last_focus(self) -> None: - pass - - def finish_scroll_animation(self) -> None: - pass - - def on_mouse_event(self, event): - ev = MouseEvent(**event) - opts = get_options() - action_def = opts.mousemap.get(ev) - if not action_def: - return False - self.current_mouse_button = ev.button - for action in opts.alias_map.resolve_aliases(action_def, 'mouse_map'): - getattr(self, action.func)(*action.args) - self.current_mouse_button = 0 - return True - - def handle_remote_print(self, msg): - text = process_remote_print(msg) - self.printbuf.append(text) - - def handle_remote_cmd(self, msg): - pass - - def handle_remote_clone(self, msg): - msg = str(msg, 'utf-8') - if not msg: - if self.current_clone_data: - cdata, self.current_clone_data = self.current_clone_data, '' - from kitty.launch import CloneCmd - self.clone_cmds.append(CloneCmd(cdata)) - self.current_clone_data = '' - return - num, rest = msg.split(':', 1) - if num == '0' or len(self.current_clone_data) > 1024 * 1024: - self.current_clone_data = '' - self.current_clone_data += rest - - def handle_remote_ssh(self, msg): - from kittens.ssh.utils import get_ssh_data - if self.pty: - for line in get_ssh_data(msg, "testing"): - self.pty.write_to_child(line) - - def handle_remote_echo(self, msg): - from base64 import standard_b64decode - if self.pty: - data = standard_b64decode(msg) - self.pty.write_to_child(data) - - def file_transmission(self, data): - if self.ftc: - self.ftc.handle_serialized_command(data) - - -def filled_line_buf(ynum=5, xnum=5, cursor=Cursor()): - ans = LineBuf(ynum, xnum) - cursor.x = 0 - for i in range(ynum): - t = (f'{i}') * xnum - ans.line(i).set_text(t, 0, xnum, cursor) - return ans - - -def filled_cursor(): - ans = Cursor() - ans.bold = ans.italic = ans.reverse = ans.strikethrough = ans.dim = True - ans.fg = 0x101 - ans.bg = 0x201 - ans.decoration_fg = 0x301 - return ans - - -def filled_history_buf(ynum=5, xnum=5, cursor=Cursor()): - lb = filled_line_buf(ynum, xnum, cursor) - ans = HistoryBuf(ynum, xnum) - for i in range(ynum): - ans.push(lb.line(i)) - return ans - - -is_ci = os.environ.get('CI') == 'true' -max_attempts = 4 if is_ci else 2 -sleep_duration = 4 if is_ci else 2 - - -def retry_on_failure(max_attempts=max_attempts, sleep_duration=sleep_duration): - def decorator(func): - @wraps(func) - def wrapper(*args, **kwargs): - for attempt in range(max_attempts): - try: - return func(*args, **kwargs) - except Exception: - if attempt < max_attempts - 1: # Don't sleep on the last attempt - time.sleep(sleep_duration) - print(f'{func.__name__} failed, retrying in {sleep_duration} seconds', file=sys.stderr) - else: - raise # Re-raise the last exception - return wrapper - return decorator - - -class BaseTest(TestCase): - - ae = TestCase.assertEqual - maxDiff = 2048 - is_ci = is_ci - - def rmtree_ignoring_errors(self, tdir): - try: - shutil.rmtree(tdir) - except FileNotFoundError as err: - print('Failed to delete the directory:', tdir, 'with error:', err, file=sys.stderr) - - def tearDown(self): - set_options(None) - - def set_options(self, options=None): - final_options = {'scrollback_pager_history_size': 1024, 'click_interval': 0.5} - if options: - final_options.update(options) - options = Options(merge_result_dicts(defaults._asdict(), final_options)) - finalize_keys(options, {}) - finalize_mouse_mappings(options, {}) - set_options(options) - return options - - def cmd_to_run_python_code(self, code): - from kitty.constants import kitty_exe - return [kitty_exe(), '+runpy', code] - - def create_screen(self, cols=5, lines=5, scrollback=5, cell_width=10, cell_height=20, options=None): - self.set_options(options) - c = Callbacks() - s = Screen(c, lines, cols, scrollback, cell_width, cell_height, 0, c) - c.color_profile = s.color_profile - return s - - def create_pty( - self, argv=None, cols=80, lines=100, scrollback=100, cell_width=10, cell_height=20, - options=None, cwd=None, env=None, stdin_fd=None, stdout_fd=None, needs_da1=False, - ): - self.set_options(options) - return PTY( - argv, lines, cols, scrollback, cell_width, cell_height, cwd, env, stdin_fd=stdin_fd, stdout_fd=stdout_fd, - needs_da1=needs_da1, - ) - - def assertEqualAttributes(self, c1, c2): - x1, y1, c1.x, c1.y = c1.x, c1.y, 0, 0 - x2, y2, c2.x, c2.y = c2.x, c2.y, 0, 0 - try: - self.assertEqual(c1, c2) - finally: - c1.x, c1.y, c2.x, c2.y = x1, y1, x2, y2 - - -debug_stdout = debug_stderr = -1 - - -@contextmanager -def forwardable_stdio(): - global debug_stderr, debug_stdout - debug_stdout = fd = os.dup(sys.stdout.fileno()) - os.set_inheritable(fd, True) - debug_stderr = fd = os.dup(sys.stderr.fileno()) - os.set_inheritable(fd, True) - try: - yield - finally: - os.close(debug_stderr) - os.close(debug_stdout) - debug_stderr = debug_stdout = -1 - - -class PTY: - - def __init__( - self, argv=None, rows=25, columns=80, scrollback=100, cell_width=10, cell_height=20, - cwd=None, env=None, stdin_fd=None, stdout_fd=None, needs_da1=True, window_id=0, - log_data_flow=False, - ): - self.is_child = False - self.log_data_flow = log_data_flow - if isinstance(argv, str): - argv = shlex.split(argv) - self.write_buf = b'' - if argv is None: - from kitty.child import openpty - self.master_fd, self.slave_fd = openpty() - self.child_pid = 0 - self.initial_termios_state = termios.tcgetattr(self.master_fd) - else: - self.child_pid, self.master_fd = fork() - self.is_child = self.child_pid == CHILD - self.child_waited_for = False - if self.is_child: - while read_screen_size().width != columns * cell_width: - time.sleep(0.01) - if cwd: - os.chdir(cwd) - if stdin_fd is not None: - os.dup2(stdin_fd, STDIN_FILENO) - os.close(stdin_fd) - if stdout_fd is not None: - os.dup2(stdout_fd, STDOUT_FILENO) - os.close(stdout_fd) - signal.pthread_sigmask(signal.SIG_SETMASK, ()) - env = os.environ if env is None else env - if debug_stdout > -1: - env['KITTY_STDIO_FORWARDED'] = str(debug_stdout) - os.execvpe(argv[0], argv, env) - if stdin_fd is not None: - os.close(stdin_fd) - if stdout_fd is not None: - os.close(stdout_fd) - os.set_blocking(self.master_fd, False) - self.cell_width = cell_width - self.cell_height = cell_height - self.set_window_size(rows=rows, columns=columns) - self.needs_da1 = needs_da1 - self.callbacks = Callbacks(self) - self.screen = Screen(self.callbacks, rows, columns, scrollback, cell_width, cell_height, window_id, self.callbacks) - self.received_bytes = b'' - - def reset_termios_state(self): - if s := getattr(self, 'initial_termios_state', None): - termios.tcsetattr(self.master_fd, termios.TCSANOW, s) - - def turn_off_echo(self): - s = termios.tcgetattr(self.master_fd) - s[3] &= ~termios.ECHO - termios.tcsetattr(self.master_fd, termios.TCSANOW, s) - - def is_echo_on(self): - s = termios.tcgetattr(self.master_fd) - return True if s[3] & termios.ECHO else False - - def __enter__(self): - return self - - def __exit__(self, *a): - self.__del__() - - def __del__(self): - if not self.is_child: - if hasattr(self, 'master_fd'): - os.close(self.master_fd) - del self.master_fd - if hasattr(self, 'slave_fd'): - os.close(self.slave_fd) - del self.slave_fd - if self.child_pid > 0 and not self.child_waited_for: - st = time.monotonic() - while time.monotonic() - st < 2: - pid, ec = os.waitpid(self.child_pid, os.WNOHANG) - if pid == self.child_pid: - self.child_waited_for = True - break - time.sleep(0.1) - if not self.child_waited_for: - os.kill(self.child_pid, signal.SIGKILL) - os.waitpid(self.child_pid, 0) - self.child_waited_for = True - - def write_to_child(self, data, flush=False): - if isinstance(data, str): - data = data.encode('utf-8') - if self.log_data_flow: - print('t -> c:', bytes(data)) - self.write_buf += data - if flush: - self.process_input_from_child(0) - - def send_da1_response(self, data: str) -> None: - self.write_to_child('\x1b[' + data, flush=False) # ]]]]]] - - def send_cmd_to_child(self, cmd, flush=False): - self.callbacks.last_cmd_exit_status = sys.maxsize - self.last_cmd = cmd - self.write_to_child(cmd + '\r', flush=flush) - - def process_input_from_child(self, timeout=10): - rd, wd, _ = select.select([self.master_fd], [self.master_fd] if self.write_buf else [], [], max(0, timeout)) - if wd: - n = os.write(self.master_fd, self.write_buf) - self.write_buf = self.write_buf[n:] - - bytes_read = 0 - if rd: - data = os.read(self.master_fd, io.DEFAULT_BUFFER_SIZE) - bytes_read += len(data) - self.received_bytes += data - if self.log_data_flow: - print('c -> t:', data) - parse_bytes(self.screen, data) - return bytes_read - - def wait_till(self, q, timeout=10, timeout_msg=None): - end_time = time.monotonic() + timeout - while not q() and time.monotonic() <= end_time: - try: - self.process_input_from_child(timeout=end_time - time.monotonic()) - except OSError as e: - if not q(): - raise Exception(f'Failed to read from pty with error: {e}. {self.screen_contents_for_error()}') from e - return - if not q(): - msg = 'The condition was not met' - if timeout_msg is not None: - msg = timeout_msg() - if not msg.endswith('\n'): - msg += '. ' - raise TimeoutError(f'Timed out after {timeout} seconds: {msg}{self.screen_contents_for_error()}') - - def wait_till_child_exits(self, timeout=30 if BaseTest.is_ci else 10, require_exit_code=None): - end_time = time.monotonic() + timeout - while time.monotonic() <= end_time: - si_pid, status = os.waitpid(self.child_pid, os.WNOHANG) - if si_pid == self.child_pid and os.WIFEXITED(status): - ec = os.waitstatus_to_exitcode(status) if hasattr(os, 'waitstatus_to_exitcode') else require_exit_code - self.child_waited_for = True - if require_exit_code is not None and ec != require_exit_code: - raise AssertionError( - f'Child exited with exit status: {status} code: {ec} != {require_exit_code}.' - f' {self.screen_contents_for_error()}') - return status - with suppress(OSError): - self.process_input_from_child(timeout=0.02) - raise AssertionError(f'Child did not exit in {timeout} seconds. {self.screen_contents_for_error()}') - - def set_window_size(self, rows=25, columns=80, send_signal=True): - if hasattr(self, 'screen'): - self.screen.resize(rows, columns) - if send_signal: - x_pixels = columns * self.cell_width - y_pixels = rows * self.cell_height - s = struct.pack('HHHH', rows, columns, x_pixels, y_pixels) - fcntl.ioctl(self.master_fd, termios.TIOCSWINSZ, s) - - def screen_contents_for_error(self): - from kitty.window import as_text - ans = as_text(self.screen, add_history=True, as_ansi=False) - return f'Screen contents as repr:\n{ans!r}\nScreen contents:\n{ans.rstrip()}' - - def screen_contents(self): - lines = [] - for i in range(self.screen.lines): - x = str(self.screen.line(i)) - if x: - lines.append(x) - return '\n'.join(lines) - - def last_cmd_output(self, as_ansi=False, add_wrap_markers=False): - from kitty.window import cmd_output - return cmd_output(self.screen, as_ansi=as_ansi, add_wrap_markers=add_wrap_markers) diff --git a/kitty_tests/atexit.py b/kitty_tests/atexit.py index 45a2033e3..508b2cb2a 100644 --- a/kitty_tests/atexit.py +++ b/kitty_tests/atexit.py @@ -12,7 +12,7 @@ import tempfile from kitty.constants import kitten_exe, kitty_exe from kitty.shm import SharedMemory -from . import BaseTest +from .base import BaseTest class Atexit(BaseTest): diff --git a/kitty_tests/base.py b/kitty_tests/base.py new file mode 100644 index 000000000..8c2c4a602 --- /dev/null +++ b/kitty_tests/base.py @@ -0,0 +1,515 @@ +#!/usr/bin/env python +# License: GPL v3 Copyright: 2016, Kovid Goyal + +import fcntl +import io +import os +import select +import shlex +import shutil +import signal +import struct +import sys +import termios +import time +from contextlib import contextmanager, suppress +from functools import wraps +from pty import CHILD, STDIN_FILENO, STDOUT_FILENO, fork +from unittest import TestCase + +from kitty.config import finalize_keys, finalize_mouse_mappings +from kitty.fast_data_types import TEXT_SIZE_CODE, Cursor, HistoryBuf, LineBuf, Screen, get_options, monotonic, set_options +from kitty.options.parse import merge_result_dicts +from kitty.options.types import Options, defaults +from kitty.rgb import to_color +from kitty.types import MouseEvent +from kitty.utils import read_screen_size +from kitty.window import da1, decode_cmdline, process_remote_print, process_title_from_child + + +def parse_bytes(screen, data, dump_callback=None): + data = memoryview(data) + while data: + dest = screen.test_create_write_buffer() + s = screen.test_commit_write_buffer(data, dest) + data = data[s:] + screen.test_parse_written_data(dump_callback) + + +def draw_multicell( + screen: Screen, text: str, width: int = 0, scale: int = 1, subscale_n: int = 0, subscale_d: int = 0, vertical_align: int = 0, horizontal_align: int = 0 + ) -> None: + cmd = f'\x1b]{TEXT_SIZE_CODE};w={width}:s={scale}:n={subscale_n}:d={subscale_d}:v={vertical_align}:h={horizontal_align};{text}\a' + parse_bytes(screen, cmd.encode()) + + +class Callbacks: + + def __init__(self, pty=None) -> None: + self.clear() + self.pty = pty + self.ftc = None + self.set_pointer_shape = lambda data: None + self.last_cmd_at = 0 + self.last_cmd_cmdline = '' + self.last_cmd_exit_status = sys.maxsize + + def write(self, data) -> None: + self.wtcbuf += bytes(data) + + def notify_child_of_resize(self): + self.num_of_resize_events += 1 + + def on_reset(self) -> None: + if self.pty is not None: + self.pty.reset_termios_state() + + def color_control(self, code, data) -> None: + from kitty.window import color_control + response = color_control(self.color_profile, code, data) + if response: + def p(x): + if '@' in x: + return (to_color(x.partition('@')[0]), int(255 * float(x.partition('@')[2]))) + ans = to_color(x) + if ans is None: + ans = x + return ans + parts = {x.partition('=')[0]:p(x.partition('=')[2]) for x in response.split(';')[1:]} + self.color_control_responses.append(parts) + + def title_changed(self, data, is_base64=False) -> None: + self.titlebuf.append(process_title_from_child(data, is_base64, '')) + + def osc_context(self, data): + pass + + def icon_changed(self, data) -> None: + self.iconbuf += str(data, 'utf-8') + + def set_dynamic_color(self, code, data='') -> None: + if code == 22: + self.set_pointer_shape(data) + else: + self.colorbuf += str(data or b'', 'utf-8') + + def set_color_table_color(self, code, data='') -> None: + self.ctbuf += '' + + def color_profile_popped(self, x) -> None: + pass + + def cmd_output_marking(self, is_start: bool | None, data: str = '') -> None: + if is_start: + self.last_cmd_at = monotonic() + self.last_cmd_cmdline = decode_cmdline(data) if data else data + else: + if self.last_cmd_at != 0: + self.last_cmd_at = 0 + with suppress(Exception): + self.last_cmd_exit_status = int(data) + + def request_capabilities(self, q) -> None: + from kitty.terminfo import get_capabilities + for c in get_capabilities(q, None): + self.write(c.encode('ascii')) + + def desktop_notify(self, osc_code: int, raw_data: memoryview) -> None: + self.notifications.append((osc_code, str(raw_data, 'utf-8'))) + + def open_url(self, url: str, hyperlink_id: int) -> None: + self.open_urls.append((url, hyperlink_id)) + + def clipboard_control(self, data: memoryview, is_partial: bool = False) -> None: + self.cc_buf.append((str(data, 'utf-8'), is_partial)) + + def clear(self) -> None: + self.wtcbuf = b'' + self.iconbuf = self.colorbuf = self.ctbuf = '' + self.titlebuf = [] + self.printbuf = [] + self.color_control_responses = [] + self.notifications = [] + self.open_urls = [] + self.cc_buf = [] + self.bell_count = 0 + self.clone_cmds = [] + self.current_clone_data = '' + self.last_cmd_exit_status = sys.maxsize + self.last_cmd_cmdline = '' + self.last_cmd_at = 0 + self.num_of_resize_events = 0 + self.da1 = [] + + def on_bell(self) -> None: + self.bell_count += 1 + + def on_da1(self) -> None: + opts = None + with suppress(RuntimeError): + opts = get_options() + if opts is None: + opts = defaults + payload = da1(opts) + self.da1.append(payload) + if self.pty and self.pty.needs_da1: + self.pty.send_da1_response(payload) + + def on_activity_since_last_focus(self) -> None: + pass + + def finish_scroll_animation(self) -> None: + pass + + def on_mouse_event(self, event): + ev = MouseEvent(**event) + opts = get_options() + action_def = opts.mousemap.get(ev) + if not action_def: + return False + self.current_mouse_button = ev.button + for action in opts.alias_map.resolve_aliases(action_def, 'mouse_map'): + getattr(self, action.func)(*action.args) + self.current_mouse_button = 0 + return True + + def handle_remote_print(self, msg): + text = process_remote_print(msg) + self.printbuf.append(text) + + def handle_remote_cmd(self, msg): + pass + + def handle_remote_clone(self, msg): + msg = str(msg, 'utf-8') + if not msg: + if self.current_clone_data: + cdata, self.current_clone_data = self.current_clone_data, '' + from kitty.launch import CloneCmd + self.clone_cmds.append(CloneCmd(cdata)) + self.current_clone_data = '' + return + num, rest = msg.split(':', 1) + if num == '0' or len(self.current_clone_data) > 1024 * 1024: + self.current_clone_data = '' + self.current_clone_data += rest + + def handle_remote_ssh(self, msg): + from kittens.ssh.utils import get_ssh_data + if self.pty: + for line in get_ssh_data(msg, "testing"): + self.pty.write_to_child(line) + + def handle_remote_echo(self, msg): + from base64 import standard_b64decode + if self.pty: + data = standard_b64decode(msg) + self.pty.write_to_child(data) + + def file_transmission(self, data): + if self.ftc: + self.ftc.handle_serialized_command(data) + + +def filled_line_buf(ynum=5, xnum=5, cursor=Cursor()): + ans = LineBuf(ynum, xnum) + cursor.x = 0 + for i in range(ynum): + t = (f'{i}') * xnum + ans.line(i).set_text(t, 0, xnum, cursor) + return ans + + +def filled_cursor(): + ans = Cursor() + ans.bold = ans.italic = ans.reverse = ans.strikethrough = ans.dim = True + ans.fg = 0x101 + ans.bg = 0x201 + ans.decoration_fg = 0x301 + return ans + + +def filled_history_buf(ynum=5, xnum=5, cursor=Cursor()): + lb = filled_line_buf(ynum, xnum, cursor) + ans = HistoryBuf(ynum, xnum) + for i in range(ynum): + ans.push(lb.line(i)) + return ans + + +is_ci = os.environ.get('CI') == 'true' +max_attempts = 4 if is_ci else 2 +sleep_duration = 4 if is_ci else 2 + + +def retry_on_failure(max_attempts=max_attempts, sleep_duration=sleep_duration): + def decorator(func): + @wraps(func) + def wrapper(*args, **kwargs): + for attempt in range(max_attempts): + try: + return func(*args, **kwargs) + except Exception: + if attempt < max_attempts - 1: # Don't sleep on the last attempt + time.sleep(sleep_duration) + print(f'{func.__name__} failed, retrying in {sleep_duration} seconds', file=sys.stderr) + else: + raise # Re-raise the last exception + return wrapper + return decorator + + +class BaseTest(TestCase): + + ae = TestCase.assertEqual + maxDiff = 2048 + is_ci = is_ci + + def rmtree_ignoring_errors(self, tdir): + try: + shutil.rmtree(tdir) + except FileNotFoundError as err: + print('Failed to delete the directory:', tdir, 'with error:', err, file=sys.stderr) + + def tearDown(self): + set_options(None) + + def set_options(self, options=None): + final_options = {'scrollback_pager_history_size': 1024, 'click_interval': 0.5} + if options: + final_options.update(options) + options = Options(merge_result_dicts(defaults._asdict(), final_options)) + finalize_keys(options, {}) + finalize_mouse_mappings(options, {}) + set_options(options) + return options + + def cmd_to_run_python_code(self, code): + from kitty.constants import kitty_exe + return [kitty_exe(), '+runpy', code] + + def create_screen(self, cols=5, lines=5, scrollback=5, cell_width=10, cell_height=20, options=None): + self.set_options(options) + c = Callbacks() + s = Screen(c, lines, cols, scrollback, cell_width, cell_height, 0, c) + c.color_profile = s.color_profile + return s + + def create_pty( + self, argv=None, cols=80, lines=100, scrollback=100, cell_width=10, cell_height=20, + options=None, cwd=None, env=None, stdin_fd=None, stdout_fd=None, needs_da1=False, + ): + self.set_options(options) + return PTY( + argv, lines, cols, scrollback, cell_width, cell_height, cwd, env, stdin_fd=stdin_fd, stdout_fd=stdout_fd, + needs_da1=needs_da1, + ) + + def assertEqualAttributes(self, c1, c2): + x1, y1, c1.x, c1.y = c1.x, c1.y, 0, 0 + x2, y2, c2.x, c2.y = c2.x, c2.y, 0, 0 + try: + self.assertEqual(c1, c2) + finally: + c1.x, c1.y, c2.x, c2.y = x1, y1, x2, y2 + + +debug_stdout = debug_stderr = -1 + + +@contextmanager +def forwardable_stdio(): + global debug_stderr, debug_stdout + debug_stdout = fd = os.dup(sys.stdout.fileno()) + os.set_inheritable(fd, True) + debug_stderr = fd = os.dup(sys.stderr.fileno()) + os.set_inheritable(fd, True) + try: + yield + finally: + os.close(debug_stderr) + os.close(debug_stdout) + debug_stderr = debug_stdout = -1 + + +class PTY: + + def __init__( + self, argv=None, rows=25, columns=80, scrollback=100, cell_width=10, cell_height=20, + cwd=None, env=None, stdin_fd=None, stdout_fd=None, needs_da1=True, window_id=0, + log_data_flow=False, + ): + self.is_child = False + self.log_data_flow = log_data_flow + if isinstance(argv, str): + argv = shlex.split(argv) + self.write_buf = b'' + if argv is None: + from kitty.child import openpty + self.master_fd, self.slave_fd = openpty() + self.child_pid = 0 + self.initial_termios_state = termios.tcgetattr(self.master_fd) + else: + self.child_pid, self.master_fd = fork() + self.is_child = self.child_pid == CHILD + self.child_waited_for = False + if self.is_child: + while read_screen_size().width != columns * cell_width: + time.sleep(0.01) + if cwd: + os.chdir(cwd) + if stdin_fd is not None: + os.dup2(stdin_fd, STDIN_FILENO) + os.close(stdin_fd) + if stdout_fd is not None: + os.dup2(stdout_fd, STDOUT_FILENO) + os.close(stdout_fd) + signal.pthread_sigmask(signal.SIG_SETMASK, ()) + env = os.environ if env is None else env + if debug_stdout > -1: + env['KITTY_STDIO_FORWARDED'] = str(debug_stdout) + os.execvpe(argv[0], argv, env) + if stdin_fd is not None: + os.close(stdin_fd) + if stdout_fd is not None: + os.close(stdout_fd) + os.set_blocking(self.master_fd, False) + self.cell_width = cell_width + self.cell_height = cell_height + self.set_window_size(rows=rows, columns=columns) + self.needs_da1 = needs_da1 + self.callbacks = Callbacks(self) + self.screen = Screen(self.callbacks, rows, columns, scrollback, cell_width, cell_height, window_id, self.callbacks) + self.received_bytes = b'' + + def reset_termios_state(self): + if s := getattr(self, 'initial_termios_state', None): + termios.tcsetattr(self.master_fd, termios.TCSANOW, s) + + def turn_off_echo(self): + s = termios.tcgetattr(self.master_fd) + s[3] &= ~termios.ECHO + termios.tcsetattr(self.master_fd, termios.TCSANOW, s) + + def is_echo_on(self): + s = termios.tcgetattr(self.master_fd) + return True if s[3] & termios.ECHO else False + + def __enter__(self): + return self + + def __exit__(self, *a): + self.__del__() + + def __del__(self): + if not self.is_child: + if hasattr(self, 'master_fd'): + os.close(self.master_fd) + del self.master_fd + if hasattr(self, 'slave_fd'): + os.close(self.slave_fd) + del self.slave_fd + if self.child_pid > 0 and not self.child_waited_for: + st = time.monotonic() + while time.monotonic() - st < 2: + pid, ec = os.waitpid(self.child_pid, os.WNOHANG) + if pid == self.child_pid: + self.child_waited_for = True + break + time.sleep(0.1) + if not self.child_waited_for: + os.kill(self.child_pid, signal.SIGKILL) + os.waitpid(self.child_pid, 0) + self.child_waited_for = True + + def write_to_child(self, data, flush=False): + if isinstance(data, str): + data = data.encode('utf-8') + if self.log_data_flow: + print('t -> c:', bytes(data)) + self.write_buf += data + if flush: + self.process_input_from_child(0) + + def send_da1_response(self, data: str) -> None: + self.write_to_child('\x1b[' + data, flush=False) # ]]]]]] + + def send_cmd_to_child(self, cmd, flush=False): + self.callbacks.last_cmd_exit_status = sys.maxsize + self.last_cmd = cmd + self.write_to_child(cmd + '\r', flush=flush) + + def process_input_from_child(self, timeout=10): + rd, wd, _ = select.select([self.master_fd], [self.master_fd] if self.write_buf else [], [], max(0, timeout)) + if wd: + n = os.write(self.master_fd, self.write_buf) + self.write_buf = self.write_buf[n:] + + bytes_read = 0 + if rd: + data = os.read(self.master_fd, io.DEFAULT_BUFFER_SIZE) + bytes_read += len(data) + self.received_bytes += data + if self.log_data_flow: + print('c -> t:', data) + parse_bytes(self.screen, data) + return bytes_read + + def wait_till(self, q, timeout=10, timeout_msg=None): + end_time = time.monotonic() + timeout + while not q() and time.monotonic() <= end_time: + try: + self.process_input_from_child(timeout=end_time - time.monotonic()) + except OSError as e: + if not q(): + raise Exception(f'Failed to read from pty with error: {e}. {self.screen_contents_for_error()}') from e + return + if not q(): + msg = 'The condition was not met' + if timeout_msg is not None: + msg = timeout_msg() + if not msg.endswith('\n'): + msg += '. ' + raise TimeoutError(f'Timed out after {timeout} seconds: {msg}{self.screen_contents_for_error()}') + + def wait_till_child_exits(self, timeout=30 if BaseTest.is_ci else 10, require_exit_code=None): + end_time = time.monotonic() + timeout + while time.monotonic() <= end_time: + si_pid, status = os.waitpid(self.child_pid, os.WNOHANG) + if si_pid == self.child_pid and os.WIFEXITED(status): + ec = os.waitstatus_to_exitcode(status) if hasattr(os, 'waitstatus_to_exitcode') else require_exit_code + self.child_waited_for = True + if require_exit_code is not None and ec != require_exit_code: + raise AssertionError( + f'Child exited with exit status: {status} code: {ec} != {require_exit_code}.' + f' {self.screen_contents_for_error()}') + return status + with suppress(OSError): + self.process_input_from_child(timeout=0.02) + raise AssertionError(f'Child did not exit in {timeout} seconds. {self.screen_contents_for_error()}') + + def set_window_size(self, rows=25, columns=80, send_signal=True): + if hasattr(self, 'screen'): + self.screen.resize(rows, columns) + if send_signal: + x_pixels = columns * self.cell_width + y_pixels = rows * self.cell_height + s = struct.pack('HHHH', rows, columns, x_pixels, y_pixels) + fcntl.ioctl(self.master_fd, termios.TIOCSWINSZ, s) + + def screen_contents_for_error(self): + from kitty.window import as_text + ans = as_text(self.screen, add_history=True, as_ansi=False) + return f'Screen contents as repr:\n{ans!r}\nScreen contents:\n{ans.rstrip()}' + + def screen_contents(self): + lines = [] + for i in range(self.screen.lines): + x = str(self.screen.line(i)) + if x: + lines.append(x) + return '\n'.join(lines) + + def last_cmd_output(self, as_ansi=False, add_wrap_markers=False): + from kitty.window import cmd_output + return cmd_output(self.screen, as_ansi=as_ansi, add_wrap_markers=add_wrap_markers) diff --git a/kitty_tests/check_build.py b/kitty_tests/check_build.py index 53d7189b8..aa9fad904 100644 --- a/kitty_tests/check_build.py +++ b/kitty_tests/check_build.py @@ -13,7 +13,7 @@ import textwrap import unittest from functools import partial -from . import BaseTest +from .base import BaseTest class TestBuild(BaseTest): diff --git a/kitty_tests/clipboard.py b/kitty_tests/clipboard.py index 56a17fd14..13a7ebdc3 100644 --- a/kitty_tests/clipboard.py +++ b/kitty_tests/clipboard.py @@ -6,7 +6,7 @@ from base64 import standard_b64decode, standard_b64encode from kitty.clipboard import WriteRequest from kitty.fast_data_types import StreamingBase64Decoder -from . import BaseTest +from .base import BaseTest class TestClipboard(BaseTest): diff --git a/kitty_tests/command_palette.py b/kitty_tests/command_palette.py index 6f5c4534c..6120d8fc4 100644 --- a/kitty_tests/command_palette.py +++ b/kitty_tests/command_palette.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # License: GPLv3 Copyright: 2024, Kovid Goyal -from . import BaseTest +from .base import BaseTest class TestCommandPalette(BaseTest): diff --git a/kitty_tests/completion.py b/kitty_tests/completion.py index 89e6d6147..3af192913 100644 --- a/kitty_tests/completion.py +++ b/kitty_tests/completion.py @@ -10,7 +10,7 @@ import tempfile from kitty.constants import kitten_exe as kitten -from . import BaseTest +from .base import BaseTest class TestCompletion(BaseTest): diff --git a/kitty_tests/crypto.py b/kitty_tests/crypto.py index aa9e251c6..af72cc6ff 100644 --- a/kitty_tests/crypto.py +++ b/kitty_tests/crypto.py @@ -4,7 +4,7 @@ import os -from . import BaseTest +from .base import BaseTest def is_rlimit_memlock_too_low() -> bool: diff --git a/kitty_tests/datatypes.py b/kitty_tests/datatypes.py index 6d8ca76c2..df60f5b1f 100644 --- a/kitty_tests/datatypes.py +++ b/kitty_tests/datatypes.py @@ -32,7 +32,7 @@ from kitty.fast_data_types import Cursor as C from kitty.rgb import to_color from kitty.utils import is_ok_to_read_image_file, is_path_in_temp_dir, sanitize_title, sanitize_url_for_display_to_user, shlex_split, shlex_split_with_positions -from . import BaseTest, filled_cursor, filled_history_buf, filled_line_buf +from .base import BaseTest, filled_cursor, filled_history_buf, filled_line_buf def create_lbuf(*lines): diff --git a/kitty_tests/dnd.py b/kitty_tests/dnd.py index bdfd9d392..b3829040e 100644 --- a/kitty_tests/dnd.py +++ b/kitty_tests/dnd.py @@ -26,7 +26,7 @@ from kitty.fast_data_types import ( ) from kitty.machine_id import machine_id -from . import BaseTest, parse_bytes +from .base import BaseTest, parse_bytes # ---- helpers ---------------------------------------------------------------- diff --git a/kitty_tests/dnd_kitten.py b/kitty_tests/dnd_kitten.py index 03a044672..7807513a2 100644 --- a/kitty_tests/dnd_kitten.py +++ b/kitty_tests/dnd_kitten.py @@ -34,7 +34,7 @@ from kitty.fast_data_types import ( ) from kitty.utils import as_file_url -from . import PTY, BaseTest +from .base import PTY, BaseTest from .dnd import WriteCapture diff --git a/kitty_tests/file_transmission.py b/kitty_tests/file_transmission.py index b661b8433..a9eb61a69 100644 --- a/kitty_tests/file_transmission.py +++ b/kitty_tests/file_transmission.py @@ -16,7 +16,7 @@ from kitty.constants import kitten_exe from kitty.file_transmission import Action, Compression, FileTransmissionCommand, FileType, TransmissionType, ZlibDecompressor from kitty.file_transmission import TestFileTransmission as FileTransmission -from . import PTY, BaseTest +from .base import PTY, BaseTest def response(id='test', msg='', file_id='', name='', action='status', status='', size=-1): diff --git a/kitty_tests/fonts.py b/kitty_tests/fonts.py index 268497407..d44de5550 100644 --- a/kitty_tests/fonts.py +++ b/kitty_tests/fonts.py @@ -28,7 +28,7 @@ from kitty.fonts.common import FontSpec, all_fonts_map, face_from_descriptor, ge from kitty.fonts.render import coalesce_symbol_maps, create_face, render_string, setup_for_testing, shape_string from kitty.options.types import Options -from . import BaseTest, draw_multicell +from .base import BaseTest, draw_multicell def parse_font_spec(spec): diff --git a/kitty_tests/glfw.py b/kitty_tests/glfw.py index 4e3bb7ddc..a5184a393 100644 --- a/kitty_tests/glfw.py +++ b/kitty_tests/glfw.py @@ -4,7 +4,7 @@ import sys import unittest -from . import BaseTest +from .base import BaseTest _plat = sys.platform.lower() is_macos = 'darwin' in _plat diff --git a/kitty_tests/graphics.py b/kitty_tests/graphics.py index ec6a0444d..9575b613e 100644 --- a/kitty_tests/graphics.py +++ b/kitty_tests/graphics.py @@ -13,7 +13,7 @@ from io import BytesIO from kitty.fast_data_types import base64_decode, base64_encode, has_avx2, has_sse4_2, load_png_data, shm_unlink, shm_write, test_xor64 -from . import BaseTest, parse_bytes +from .base import BaseTest, parse_bytes try: from PIL import Image diff --git a/kitty_tests/keys.py b/kitty_tests/keys.py index 1023f2648..f4c39ad6c 100644 --- a/kitty_tests/keys.py +++ b/kitty_tests/keys.py @@ -9,7 +9,7 @@ from kitty.key_encoding import EventType, KeyEvent, decode_key_event, encode_key from kitty.keys import Mappings from kitty.options.utils import KeyFallbackType -from . import BaseTest +from .base import BaseTest class TestKeys(BaseTest): diff --git a/kitty_tests/layout.py b/kitty_tests/layout.py index 4c5c5d226..d7b3fbb8e 100644 --- a/kitty_tests/layout.py +++ b/kitty_tests/layout.py @@ -10,7 +10,7 @@ from kitty.types import WindowGeometry from kitty.window import EdgeWidths from kitty.window_list import WindowList, reset_group_id_counter -from . import BaseTest +from .base import BaseTest class Window: diff --git a/kitty_tests/main.py b/kitty_tests/main.py index 6fab27cf0..2a15524ba 100644 --- a/kitty_tests/main.py +++ b/kitty_tests/main.py @@ -20,7 +20,7 @@ from typing import ( Optional, ) -from . import BaseTest +from .base import BaseTest def contents(package: str) -> Iterator[str]: diff --git a/kitty_tests/mouse.py b/kitty_tests/mouse.py index efd6561b2..6c1bedba2 100644 --- a/kitty_tests/mouse.py +++ b/kitty_tests/mouse.py @@ -14,7 +14,7 @@ from kitty.fast_data_types import ( send_mock_mouse_event_to_window, ) -from . import BaseTest +from .base import BaseTest def send_mouse_event( diff --git a/kitty_tests/multicell.py b/kitty_tests/multicell.py index 49b5acf22..6aa75d18e 100644 --- a/kitty_tests/multicell.py +++ b/kitty_tests/multicell.py @@ -5,8 +5,8 @@ from functools import partial from kitty.fast_data_types import EXTEND_CELL, TEXT_SIZE_CODE, test_ch_and_idx, wcswidth -from . import BaseTest, parse_bytes -from . import draw_multicell as multicell +from .base import BaseTest, parse_bytes +from .base import draw_multicell as multicell class TestMulticell(BaseTest): diff --git a/kitty_tests/notifications.py b/kitty_tests/notifications.py index 0031ae3bc..282d3d278 100644 --- a/kitty_tests/notifications.py +++ b/kitty_tests/notifications.py @@ -9,7 +9,7 @@ from base64 import standard_b64encode from kitty.notifications import Channel, DesktopIntegration, IconDataCache, NotificationManager, UIState, Urgency -from . import BaseTest +from .base import BaseTest def n( diff --git a/kitty_tests/open_actions.py b/kitty_tests/open_actions.py index cb4593430..70f69953b 100644 --- a/kitty_tests/open_actions.py +++ b/kitty_tests/open_actions.py @@ -7,7 +7,7 @@ from contextlib import contextmanager from kitty.utils import get_editor -from . import BaseTest +from .base import BaseTest @contextmanager diff --git a/kitty_tests/options.py b/kitty_tests/options.py index 5e1631b00..1f16bfa5b 100644 --- a/kitty_tests/options.py +++ b/kitty_tests/options.py @@ -11,7 +11,7 @@ from kitty.fast_data_types import Color, test_cursor_blink_easing_function from kitty.options.utils import DELETE_ENV_VAR, EasingFunction, to_color from kitty.utils import log_error, shlex_split -from . import BaseTest +from .base import BaseTest class TestConfParsing(BaseTest): diff --git a/kitty_tests/parser.py b/kitty_tests/parser.py index 8685621c3..8e3bce9c3 100644 --- a/kitty_tests/parser.py +++ b/kitty_tests/parser.py @@ -15,7 +15,7 @@ from kitty.fast_data_types import ( test_utf8_decode_to_sentinel, ) -from . import BaseTest, parse_bytes +from .base import BaseTest, parse_bytes def cnv(x): diff --git a/kitty_tests/screen.py b/kitty_tests/screen.py index 25d94e3f1..ca3562a98 100644 --- a/kitty_tests/screen.py +++ b/kitty_tests/screen.py @@ -5,7 +5,7 @@ from kitty.fast_data_types import DECAWM, DECCOLM, DECOM, IRM, VT_PARSER_BUFFER_ from kitty.marks import marker_from_function, marker_from_regex, marker_from_text from kitty.window import pagerhist -from . import BaseTest, draw_multicell, parse_bytes +from .base import BaseTest, draw_multicell, parse_bytes class TestScreen(BaseTest): diff --git a/kitty_tests/search_query_parser.py b/kitty_tests/search_query_parser.py index fda28db8c..a04132ca4 100644 --- a/kitty_tests/search_query_parser.py +++ b/kitty_tests/search_query_parser.py @@ -2,7 +2,7 @@ # License: GPLv3 Copyright: 2022, Kovid Goyal -from . import BaseTest +from .base import BaseTest class TestSQP(BaseTest): diff --git a/kitty_tests/shell_integration.py b/kitty_tests/shell_integration.py index 897547f41..e64aaa2a9 100644 --- a/kitty_tests/shell_integration.py +++ b/kitty_tests/shell_integration.py @@ -17,7 +17,7 @@ from kitty.constants import is_macos, kitten_exe, kitty_base_dir, shell_integrat from kitty.fast_data_types import CURSOR_BEAM, CURSOR_BLOCK, CURSOR_UNDERLINE from kitty.shell_integration import setup_bash_env, setup_fish_env, setup_zsh_env -from . import BaseTest +from .base import BaseTest @lru_cache diff --git a/kitty_tests/shm.py b/kitty_tests/shm.py index b2cc70406..2d8d81b5f 100644 --- a/kitty_tests/shm.py +++ b/kitty_tests/shm.py @@ -9,7 +9,7 @@ from kitty.constants import kitten_exe from kitty.fast_data_types import shm_unlink from kitty.shm import SharedMemory -from . import BaseTest +from .base import BaseTest class SHMTest(BaseTest): diff --git a/kitty_tests/slang.py b/kitty_tests/slang.py index 201e00064..ee79cc741 100644 --- a/kitty_tests/slang.py +++ b/kitty_tests/slang.py @@ -6,7 +6,7 @@ import tempfile from kitty.shaders.slang import EntryPoint, SlangFile, Stage, build_import_graph, parse_slang_text, topological_sort -from . import BaseTest +from .base import BaseTest class TestSlang(BaseTest): diff --git a/kitty_tests/ssh.py b/kitty_tests/ssh.py index 9e126fe29..635cf8752 100644 --- a/kitty_tests/ssh.py +++ b/kitty_tests/ssh.py @@ -16,7 +16,7 @@ from kitty.constants import is_macos, kitten_exe, kitty_base_dir, runtime_dir from kitty.fast_data_types import CURSOR_BEAM, shm_unlink from kitty.utils import SSHConnectionData -from . import BaseTest, retry_on_failure +from .base import BaseTest, retry_on_failure from .shell_integration import bash_ok, basic_shell_env diff --git a/kitty_tests/tab_bar.py b/kitty_tests/tab_bar.py index f20af348a..ed9fe5647 100644 --- a/kitty_tests/tab_bar.py +++ b/kitty_tests/tab_bar.py @@ -6,7 +6,7 @@ from unittest.mock import patch from kitty.fast_data_types import LEFT_EDGE, Region from kitty.tab_bar import TabBar, TabBarData -from . import BaseTest +from .base import BaseTest def region(left: int, top: int, right: int, bottom: int) -> Region: diff --git a/kitty_tests/tui.py b/kitty_tests/tui.py index 67a407a02..8edc86bcc 100644 --- a/kitty_tests/tui.py +++ b/kitty_tests/tui.py @@ -2,7 +2,7 @@ # License: GPL v3 Copyright: 2018, Kovid Goyal -from . import BaseTest +from .base import BaseTest class TestTUI(BaseTest): diff --git a/kitty_tests/utmp.py b/kitty_tests/utmp.py index f2c8e38a8..21edb61c1 100644 --- a/kitty_tests/utmp.py +++ b/kitty_tests/utmp.py @@ -2,7 +2,7 @@ import subprocess from kitty.fast_data_types import num_users -from . import BaseTest +from .base import BaseTest class UTMPTest(BaseTest):