From 230e457e79764292508826c5d53a5d5e1d1cd2ec Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 14 Dec 2019 13:31:08 -0800 Subject: [PATCH] split up the tests --- testing/__init__.py | 0 testing/runner.py | 142 +++ tests/babi_test.py | 2032 -------------------------------- tests/color_test.py | 15 + tests/command_mode_test.py | 145 +++ tests/conftest.py | 18 + tests/current_position_test.py | 19 + tests/cut_uncut_test.py | 49 + tests/file_test.py | 20 + tests/get_files_test.py | 27 + tests/go_to_line_test.py | 69 ++ tests/list_spy_test.py | 144 +++ tests/movement_test.py | 269 +++++ tests/multiple_files_test.py | 55 + tests/replace_test.py | 235 ++++ tests/resize_test.py | 161 +++ tests/save_test.py | 103 ++ tests/search_test.py | 298 +++++ tests/status_test.py | 20 + tests/suspend_test.py | 48 + tests/text_editing_test.py | 122 ++ tests/undo_redo_test.py | 127 ++ 22 files changed, 2086 insertions(+), 2032 deletions(-) create mode 100644 testing/__init__.py create mode 100644 testing/runner.py delete mode 100644 tests/babi_test.py create mode 100644 tests/color_test.py create mode 100644 tests/command_mode_test.py create mode 100644 tests/conftest.py create mode 100644 tests/current_position_test.py create mode 100644 tests/cut_uncut_test.py create mode 100644 tests/file_test.py create mode 100644 tests/get_files_test.py create mode 100644 tests/go_to_line_test.py create mode 100644 tests/list_spy_test.py create mode 100644 tests/movement_test.py create mode 100644 tests/multiple_files_test.py create mode 100644 tests/replace_test.py create mode 100644 tests/resize_test.py create mode 100644 tests/save_test.py create mode 100644 tests/search_test.py create mode 100644 tests/status_test.py create mode 100644 tests/suspend_test.py create mode 100644 tests/text_editing_test.py create mode 100644 tests/undo_redo_test.py diff --git a/testing/__init__.py b/testing/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/testing/runner.py b/testing/runner.py new file mode 100644 index 0000000..cccd8c1 --- /dev/null +++ b/testing/runner.py @@ -0,0 +1,142 @@ +import contextlib +import shlex +import sys +from typing import List + +from hecate import Runner + +import babi + + +class PrintsErrorRunner(Runner): + def __init__(self, *args, **kwargs): + self._screenshots: List[str] = [] + super().__init__(*args, **kwargs) + + def screenshot(self, *args, **kwargs): + ret = super().screenshot(*args, **kwargs) + if not self._screenshots or self._screenshots[-1] != ret: + self._screenshots.append(ret) + return ret + + @contextlib.contextmanager + def _onerror(self): + try: + yield + except Exception: # pragma: no cover + # take a screenshot of the final state + self.screenshot() + print('=' * 79, flush=True) + for screenshot in self._screenshots: + print(screenshot, end='', flush=True) + print('=' * 79, flush=True) + raise + + def await_exit(self, *args, **kwargs): + with self._onerror(): + return super().await_exit(*args, **kwargs) + + def await_text(self, text, timeout=None): + """copied from the base implementation but doesn't munge newlines""" + with self._onerror(): + for _ in self.poll_until_timeout(timeout): + screen = self.screenshot() + if text in screen: # pragma: no branch + return + raise AssertionError( + f'Timeout while waiting for text {text!r} to appear', + ) + + def await_text_missing(self, s): + """largely based on await_text""" + with self._onerror(): + for _ in self.poll_until_timeout(): + screen = self.screenshot() + munged = screen.replace('\n', '') + if s not in munged: # pragma: no branch + return + raise AssertionError( + f'Timeout while waiting for text {s!r} to disappear', + ) + + def get_pane_size(self): + cmd = ('display', '-t0', '-p', '#{pane_width}\t#{pane_height}') + w, h = self.tmux.execute_command(*cmd).split() + return int(w), int(h) + + def _get_cursor_position(self): + cmd = ('display', '-t0', '-p', '#{cursor_x}\t#{cursor_y}') + x, y = self.tmux.execute_command(*cmd).split() + return int(x), int(y) + + def await_cursor_position(self, *, x, y): + with self._onerror(): + for _ in self.poll_until_timeout(): + pos = self._get_cursor_position() + if pos == (x, y): # pragma: no branch + return + + raise AssertionError( + f'Timeout while waiting for cursor to reach {(x, y)}\n' + f'Last cursor position: {pos}', + ) + + def get_screen_line(self, n): + return self.screenshot().splitlines()[n] + + def get_cursor_line(self): + _, y = self._get_cursor_position() + return self.get_screen_line(y) + + @contextlib.contextmanager + def resize(self, width, height): + current_w, current_h = self.get_pane_size() + sleep_cmd = ( + 'bash', '-c', + f'echo {"*" * (current_w * current_h)} && ' + f'exec sleep infinity', + ) + + panes = 0 + + hsplit_w = current_w - width - 1 + if hsplit_w > 0: + cmd = ('split-window', '-ht0', '-l', hsplit_w, *sleep_cmd) + self.tmux.execute_command(*cmd) + panes += 1 + + vsplit_h = current_h - height - 1 + if vsplit_h > 0: # pragma: no branch # TODO + cmd = ('split-window', '-vt0', '-l', vsplit_h, *sleep_cmd) + self.tmux.execute_command(*cmd) + panes += 1 + + assert self.get_pane_size() == (width, height) + try: + yield + finally: + for _ in range(panes): + self.tmux.execute_command('kill-pane', '-t1') + + def press_and_enter(self, s): + self.press(s) + self.press('Enter') + + +@contextlib.contextmanager +def run(*args, color=True, **kwargs): + cmd = (sys.executable, '-mcoverage', 'run', '-m', 'babi', *args) + quoted = ' '.join(shlex.quote(p) for p in cmd) + term = 'screen-256color' if color else 'screen' + cmd = ('bash', '-c', f'export TERM={term}; exec {quoted}') + with PrintsErrorRunner(*cmd, **kwargs) as h: + h.await_text(babi.VERSION_STR) + yield h + + +@contextlib.contextmanager +def and_exit(h): + yield + # only try and exit in non-exceptional cases + h.press('^X') + h.await_exit() diff --git a/tests/babi_test.py b/tests/babi_test.py deleted file mode 100644 index 6b649a7..0000000 --- a/tests/babi_test.py +++ /dev/null @@ -1,2032 +0,0 @@ -import contextlib -import io -import os -import shlex -import sys -from typing import List -from unittest import mock - -import pytest -from hecate import Runner - -import babi - - -@pytest.fixture(autouse=True) -def xdg_data_home(tmpdir): - data_home = tmpdir.join('data_home') - with mock.patch.dict(os.environ, {'XDG_DATA_HOME': str(data_home)}): - yield data_home - - -def test_list_spy_repr(): - assert repr(babi.ListSpy(['a', 'b', 'c'])) == "ListSpy(['a', 'b', 'c'])" - - -def test_list_spy_item_retrieval(): - spy = babi.ListSpy(['a', 'b', 'c']) - assert spy[1] == 'b' - assert spy[-1] == 'c' - with pytest.raises(IndexError): - spy[3] - - -def test_list_spy_del(): - lst = ['a', 'b', 'c'] - - spy = babi.ListSpy(lst) - del spy[1] - - assert lst == ['a', 'c'] - - spy.undo(lst) - - assert lst == ['a', 'b', 'c'] - - -def test_list_spy_del_with_negative(): - lst = ['a', 'b', 'c'] - - spy = babi.ListSpy(lst) - del spy[-1] - - assert lst == ['a', 'b'] - - spy.undo(lst) - - assert lst == ['a', 'b', 'c'] - - -def test_list_spy_insert(): - lst = ['a', 'b', 'c'] - - spy = babi.ListSpy(lst) - spy.insert(1, 'q') - - assert lst == ['a', 'q', 'b', 'c'] - - spy.undo(lst) - - assert lst == ['a', 'b', 'c'] - - -def test_list_spy_insert_with_negative(): - lst = ['a', 'b', 'c'] - - spy = babi.ListSpy(lst) - spy.insert(-1, 'q') - - assert lst == ['a', 'b', 'q', 'c'] - - spy.undo(lst) - - assert lst == ['a', 'b', 'c'] - - -def test_list_spy_set_value(): - lst = ['a', 'b', 'c'] - - spy = babi.ListSpy(lst) - spy[1] = 'hello' - - assert lst == ['a', 'hello', 'c'] - - spy.undo(lst) - - assert lst == ['a', 'b', 'c'] - - -def test_list_spy_multiple_modifications(): - lst = ['a', 'b', 'c'] - - spy = babi.ListSpy(lst) - spy[1] = 'hello' - spy.insert(1, 'ohai') - del spy[0] - - assert lst == ['ohai', 'hello', 'c'] - - spy.undo(lst) - - assert lst == ['a', 'b', 'c'] - - -def test_list_spy_iter(): - spy = babi.ListSpy(['a', 'b', 'c']) - spy_iter = iter(spy) - assert next(spy_iter) == 'a' - assert next(spy_iter) == 'b' - assert next(spy_iter) == 'c' - with pytest.raises(StopIteration): - next(spy_iter) - - -def test_list_spy_append(): - lst = ['a', 'b', 'c'] - - spy = babi.ListSpy(lst) - spy.append('q') - - assert lst == ['a', 'b', 'c', 'q'] - - spy.undo(lst) - - assert lst == ['a', 'b', 'c'] - - -def test_list_spy_pop_default(): - lst = ['a', 'b', 'c'] - - spy = babi.ListSpy(lst) - spy.pop() - - assert lst == ['a', 'b'] - - spy.undo(lst) - - assert lst == ['a', 'b', 'c'] - - -def test_list_spy_pop_idx(): - lst = ['a', 'b', 'c'] - - spy = babi.ListSpy(lst) - spy.pop(1) - - assert lst == ['a', 'c'] - - spy.undo(lst) - - assert lst == ['a', 'b', 'c'] - - -def test_position_repr(): - ret = repr(babi.File('f.txt')) - assert ret == ( - 'File(\n' - " filename='f.txt',\n" - ' modified=False,\n' - ' lines=[],\n' - " nl='\\n',\n" - ' file_y=0,\n' - ' cursor_y=0,\n' - ' x=0,\n' - ' x_hint=0,\n' - ' sha256=None,\n' - ' undo_stack=[],\n' - ' redo_stack=[],\n' - ')' - ) - - -@pytest.mark.parametrize( - ('s', 'lines', 'nl', 'mixed'), - ( - pytest.param('', [''], '\n', False, id='trivial'), - pytest.param('1\n2\n', ['1', '2', ''], '\n', False, id='lf'), - pytest.param('1\r\n2\r\n', ['1', '2', ''], '\r\n', False, id='crlf'), - pytest.param('1\r\n2\n', ['1', '2', ''], '\n', True, id='mixed'), - pytest.param('1\n2', ['1', '2', ''], '\n', False, id='noeol'), - ), -) -def test_get_lines(s, lines, nl, mixed): - # sha256 tested below - ret_lines, ret_nl, ret_mixed, _ = babi._get_lines(io.StringIO(s)) - assert (ret_lines, ret_nl, ret_mixed) == (lines, nl, mixed) - - -def test_get_lines_sha256_checksum(): - ret = babi._get_lines(io.StringIO('')) - sha256 = 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855' - assert ret == ([''], '\n', False, sha256) - - -class PrintsErrorRunner(Runner): - def __init__(self, *args, **kwargs): - self._screenshots: List[str] = [] - super().__init__(*args, **kwargs) - - def screenshot(self, *args, **kwargs): - ret = super().screenshot(*args, **kwargs) - if not self._screenshots or self._screenshots[-1] != ret: - self._screenshots.append(ret) - return ret - - @contextlib.contextmanager - def _onerror(self): - try: - yield - except Exception: # pragma: no cover - # take a screenshot of the final state - self.screenshot() - print('=' * 79, flush=True) - for screenshot in self._screenshots: - print(screenshot, end='', flush=True) - print('=' * 79, flush=True) - raise - - def await_exit(self, *args, **kwargs): - with self._onerror(): - return super().await_exit(*args, **kwargs) - - def await_text(self, text, timeout=None): - """copied from the base implementation but doesn't munge newlines""" - with self._onerror(): - for _ in self.poll_until_timeout(timeout): - screen = self.screenshot() - if text in screen: # pragma: no branch - return - raise AssertionError( - f'Timeout while waiting for text {text!r} to appear', - ) - - def await_text_missing(self, s): - """largely based on await_text""" - with self._onerror(): - for _ in self.poll_until_timeout(): - screen = self.screenshot() - munged = screen.replace('\n', '') - if s not in munged: # pragma: no branch - return - raise AssertionError( - f'Timeout while waiting for text {s!r} to disappear', - ) - - def get_pane_size(self): - cmd = ('display', '-t0', '-p', '#{pane_width}\t#{pane_height}') - w, h = self.tmux.execute_command(*cmd).split() - return int(w), int(h) - - def _get_cursor_position(self): - cmd = ('display', '-t0', '-p', '#{cursor_x}\t#{cursor_y}') - x, y = self.tmux.execute_command(*cmd).split() - return int(x), int(y) - - def await_cursor_position(self, *, x, y): - with self._onerror(): - for _ in self.poll_until_timeout(): - pos = self._get_cursor_position() - if pos == (x, y): # pragma: no branch - return - - raise AssertionError( - f'Timeout while waiting for cursor to reach {(x, y)}\n' - f'Last cursor position: {pos}', - ) - - def get_screen_line(self, n): - return self.screenshot().splitlines()[n] - - def get_cursor_line(self): - _, y = self._get_cursor_position() - return self.get_screen_line(y) - - @contextlib.contextmanager - def resize(self, width, height): - current_w, current_h = self.get_pane_size() - sleep_cmd = ( - 'bash', '-c', - f'echo {"*" * (current_w * current_h)} && ' - f'exec sleep infinity', - ) - - panes = 0 - - hsplit_w = current_w - width - 1 - if hsplit_w > 0: - cmd = ('split-window', '-ht0', '-l', hsplit_w, *sleep_cmd) - self.tmux.execute_command(*cmd) - panes += 1 - - vsplit_h = current_h - height - 1 - if vsplit_h > 0: # pragma: no branch # TODO - cmd = ('split-window', '-vt0', '-l', vsplit_h, *sleep_cmd) - self.tmux.execute_command(*cmd) - panes += 1 - - assert self.get_pane_size() == (width, height) - try: - yield - finally: - for _ in range(panes): - self.tmux.execute_command('kill-pane', '-t1') - - def press_and_enter(self, s): - self.press(s) - self.press('Enter') - - -@contextlib.contextmanager -def run(*args, color=True, **kwargs): - cmd = (sys.executable, '-mcoverage', 'run', '-m', 'babi', *args) - quoted = ' '.join(shlex.quote(p) for p in cmd) - term = 'screen-256color' if color else 'screen' - cmd = ('bash', '-c', f'export TERM={term}; exec {quoted}') - with PrintsErrorRunner(*cmd, **kwargs) as h: - h.await_text(babi.VERSION_STR) - yield h - - -@contextlib.contextmanager -def and_exit(h): - yield - # only try and exit in non-exceptional cases - h.press('^X') - h.await_exit() - - -def trigger_command_mode(h): - # in order to enter a steady state, trigger an unknown key first and then - # press escape to open the command mode. this is necessary as `Escape` is - # the start of "escape sequences" and sending characters too quickly will - # be interpreted as a single keypress - h.press('^J') - h.await_text('unknown key') - h.press('Escape') - h.await_text_missing('unknown key') - - -@pytest.fixture -def ten_lines(tmpdir): - f = tmpdir.join('f') - f.write('\n'.join(f'line_{i}' for i in range(10))) - yield f - - -@pytest.mark.parametrize('color', (True, False)) -def test_color_test(color): - with run('--color-test', color=color) as h, and_exit(h): - h.await_text('* 1* 2') - - -def test_can_start_without_color(): - with run(color=False) as h, and_exit(h): - pass - - -def test_window_height_2(tmpdir): - # 2 tall: - # - header is hidden, otherwise behaviour is normal - f = tmpdir.join('f.txt') - f.write('hello world') - - with run(str(f)) as h, and_exit(h): - h.await_text('hello world') - - with h.resize(80, 2): - h.await_text_missing(babi.VERSION_STR) - assert h.screenshot() == 'hello world\n\n' - h.press('^J') - h.await_text('unknown key') - - h.await_text(babi.VERSION_STR) - - -def test_window_height_1(tmpdir): - # 1 tall: - # - only file contents as body - # - status takes precedence over body, but cleared after single action - f = tmpdir.join('f.txt') - f.write('hello world') - - with run(str(f)) as h, and_exit(h): - h.await_text('hello world') - - with h.resize(80, 1): - h.await_text_missing(babi.VERSION_STR) - assert h.screenshot() == 'hello world\n' - h.press('^J') - h.await_text('unknown key') - h.press('Right') - h.await_text_missing('unknown key') - h.press('Down') - - -def test_status_clearing_behaviour(): - with run() as h, and_exit(h): - h.press('^J') - h.await_text('unknown key') - for i in range(24): - h.press('LEFT') - h.await_text('unknown key') - h.press('LEFT') - h.await_text_missing('unknown key') - - -def test_reacts_to_resize(): - with run() as h, and_exit(h): - first_line = h.get_screen_line(0) - with h.resize(40, 20): - # the first line should be different after resize - h.await_text_missing(first_line) - - -def test_mixed_newlines(tmpdir): - f = tmpdir.join('f') - f.write_binary(b'foo\nbar\r\n') - with run(str(f)) as h, and_exit(h): - # should start as modified - h.await_text('f *') - h.await_text(r"mixed newlines will be converted to '\n'") - - -def test_new_file(): - with run('this_is_a_new_file') as h, and_exit(h): - h.await_text('this_is_a_new_file') - h.await_text('(new file)') - - -def test_not_a_file(tmpdir): - d = tmpdir.join('d').ensure_dir() - with run(str(d)) as h, and_exit(h): - h.await_text('<>') - h.await_text("d' is not a file") - - -def test_arrow_key_movement(tmpdir): - f = tmpdir.join('f') - f.write( - 'short\n' - '\n' - 'long long long long\n', - ) - with run(str(f)) as h, and_exit(h): - h.await_text('short') - h.await_cursor_position(x=0, y=1) - # should not go off the beginning of the file - h.press('Left') - h.await_cursor_position(x=0, y=1) - h.press('Up') - h.await_cursor_position(x=0, y=1) - # left and right should work - h.press('Right') - h.press('Right') - h.await_cursor_position(x=2, y=1) - h.press('Left') - h.await_cursor_position(x=1, y=1) - # up should still be a noop on line 1 - h.press('Up') - h.await_cursor_position(x=1, y=1) - # down once should put it on the beginning of the second line - h.press('Down') - h.await_cursor_position(x=0, y=2) - # down again should restore the x positon on the next line - h.press('Down') - h.await_cursor_position(x=1, y=3) - # down once more should put it on the special end-of-file line - h.press('Down') - h.await_cursor_position(x=0, y=4) - # should not go off the end of the file - h.press('Down') - h.await_cursor_position(x=0, y=4) - h.press('Right') - h.await_cursor_position(x=0, y=4) - # left should put it at the end of the line - h.press('Left') - h.await_cursor_position(x=19, y=3) - # right should put it to the next line - h.press('Right') - h.await_cursor_position(x=0, y=4) - # if the hint-x is too high it should not go past the end of line - h.press('Left') - h.press('Up') - h.press('Up') - h.await_cursor_position(x=5, y=1) - # and moving back down should still retain the hint-x - h.press('Down') - h.press('Down') - h.await_cursor_position(x=19, y=3) - - -@pytest.mark.parametrize( - ('page_up', 'page_down'), - (('PageUp', 'PageDown'), ('^Y', '^V')), -) -def test_page_up_and_page_down(ten_lines, page_up, page_down): - with run(str(ten_lines), height=10) as h, and_exit(h): - h.press('Down') - h.press('Down') - h.press(page_up) - h.await_cursor_position(x=0, y=1) - - h.press(page_down) - h.await_text('line_8') - h.await_cursor_position(x=0, y=1) - assert h.get_cursor_line() == 'line_6' - - h.press(page_up) - h.await_text_missing('line_8') - h.await_cursor_position(x=0, y=1) - assert h.get_cursor_line() == 'line_0' - - h.press(page_down) - h.press(page_down) - h.await_cursor_position(x=0, y=5) - assert h.get_cursor_line() == '' - h.press('Up') - h.await_cursor_position(x=0, y=4) - assert h.get_cursor_line() == 'line_9' - - -def test_page_up_page_down_size_small_window(ten_lines): - with run(str(ten_lines), height=4) as h, and_exit(h): - h.press('PageDown') - h.await_text('line_2') - h.await_cursor_position(x=0, y=1) - assert h.get_cursor_line() == 'line_1' - - h.press('Down') - h.press('PageUp') - h.await_text_missing('line_2') - h.await_cursor_position(x=0, y=1) - assert h.get_cursor_line() == 'line_0' - - -def test_ctrl_home(ten_lines): - with run(str(ten_lines), height=4) as h, and_exit(h): - for _ in range(3): - h.press('PageDown') - h.await_text_missing('line_0') - - h.press('^Home') - h.await_text('line_0') - h.await_cursor_position(x=0, y=1) - - -def test_ctrl_end(ten_lines): - with run(str(ten_lines), height=6) as h, and_exit(h): - h.press('^End') - h.await_cursor_position(x=0, y=3) - assert h.get_screen_line(2) == 'line_9' - - -def test_ctrl_end_already_on_last_page(ten_lines): - with run(str(ten_lines), height=9) as h, and_exit(h): - h.press('PageDown') - h.await_cursor_position(x=0, y=1) - h.await_text('line_9') - - h.press('^End') - h.await_cursor_position(x=0, y=6) - assert h.get_screen_line(5) == 'line_9' - - -def test_prompt_window_width(): - with run() as h, and_exit(h): - h.press('^_') - h.await_text('enter line number:') - h.press('123') - with h.resize(width=23, height=24): - h.await_text('\nenter line number: «3') - with h.resize(width=22, height=24): - h.await_text('\nenter line numb…: «3') - with h.resize(width=7, height=24): - h.await_text('\n…: «3') - with h.resize(width=6, height=24): - h.await_text('\n123') - h.press('Enter') - - -def test_go_to_line_line(ten_lines): - def _jump_to_line(n): - h.press('^_') - h.await_text('enter line number:') - h.press_and_enter(str(n)) - h.await_text_missing('enter line number:') - - with run(str(ten_lines), height=9) as h, and_exit(h): - # still on screen - _jump_to_line(3) - h.await_cursor_position(x=0, y=3) - # should go to beginning of file - _jump_to_line(0) - h.await_cursor_position(x=0, y=1) - # should go to end of the file - _jump_to_line(999) - h.await_cursor_position(x=0, y=4) - assert h.get_screen_line(3) == 'line_9' - # should also go to the end of the file - _jump_to_line(-1) - h.await_cursor_position(x=0, y=4) - assert h.get_screen_line(3) == 'line_9' - # should go to beginning of file - _jump_to_line(-999) - h.await_cursor_position(x=0, y=1) - assert h.get_cursor_line() == 'line_0' - - -@pytest.mark.parametrize('key', ('Enter', '^C')) -def test_go_to_line_cancel(ten_lines, key): - with run(str(ten_lines)) as h, and_exit(h): - h.press('Down') - h.await_cursor_position(x=0, y=2) - - h.press('^_') - h.await_text('enter line number:') - h.press(key) - h.await_cursor_position(x=0, y=2) - h.await_text('cancelled') - - -def test_go_to_line_not_an_integer(): - with run() as h, and_exit(h): - h.press('^_') - h.await_text('enter line number:') - h.press_and_enter('asdf') - h.await_text("not an integer: 'asdf'") - - -def test_search_wraps(ten_lines): - with run(str(ten_lines)) as h, and_exit(h): - h.press('Down') - h.press('Down') - h.await_cursor_position(x=0, y=3) - h.press('^W') - h.await_text('search:') - h.press_and_enter('^line_0$') - h.await_text('search wrapped') - h.await_cursor_position(x=0, y=1) - - -def test_search_find_next_line(ten_lines): - with run(str(ten_lines)) as h, and_exit(h): - h.await_cursor_position(x=0, y=1) - h.press('^W') - h.await_text('search:') - h.press_and_enter('^line_') - h.await_cursor_position(x=0, y=2) - - -def test_search_find_later_in_line(): - with run() as h, and_exit(h): - h.press_and_enter('lol') - h.press('Up') - h.press('Right') - h.await_cursor_position(x=1, y=1) - - h.press('^W') - h.await_text('search:') - h.press_and_enter('l') - h.await_cursor_position(x=2, y=1) - - -def test_search_only_one_match_already_at_that_match(ten_lines): - with run(str(ten_lines)) as h, and_exit(h): - h.press('Down') - h.await_cursor_position(x=0, y=2) - h.press('^W') - h.await_text('search:') - h.press_and_enter('^line_1$') - h.await_text('this is the only occurrence') - h.await_cursor_position(x=0, y=2) - - -def test_search_not_found(ten_lines): - with run(str(ten_lines)) as h, and_exit(h): - h.press('^W') - h.await_text('search:') - h.press_and_enter('this will not match') - h.await_text('no matches') - h.await_cursor_position(x=0, y=1) - - -def test_search_invalid_regex(ten_lines): - with run(str(ten_lines)) as h, and_exit(h): - h.press('^W') - h.await_text('search:') - h.press_and_enter('invalid(regex') - h.await_text("invalid regex: 'invalid(regex'") - - -@pytest.mark.parametrize('key', ('Enter', '^C')) -def test_search_cancel(ten_lines, key): - with run(str(ten_lines)) as h, and_exit(h): - h.press('^W') - h.await_text('search:') - h.press(key) - h.await_text('cancelled') - - -def test_search_repeated_search(ten_lines): - with run(str(ten_lines)) as h, and_exit(h): - h.press('^W') - h.press('line') - h.await_text('search: line') - h.press('Enter') - h.await_cursor_position(x=0, y=2) - - h.press('^W') - h.await_text('search [line]:') - h.press('Enter') - h.await_cursor_position(x=0, y=3) - - -def test_search_history_recorded(): - with run() as h, and_exit(h): - h.press('^W') - h.await_text('search:') - h.press_and_enter('asdf') - h.await_text('no matches') - - h.press('^W') - h.press('Up') - h.await_text('search [asdf]: asdf') - h.press('BSpace') - h.press('test') - h.await_text('search [asdf]: asdtest') - h.press('Down') - h.await_text_missing('asdtest') - h.press('Down') # can't go past the end - h.press('Up') - h.await_text('asdtest') - h.press('Up') # can't go past the beginning - h.await_text('asdtest') - h.press('enter') - h.await_text('no matches') - - h.press('^W') - h.press('Up') - h.await_text('search [asdtest]: asdtest') - h.press('Up') - h.await_text('search [asdtest]: asdf') - h.press('^C') - - -def test_search_history_duplicates_dont_repeat(): - with run() as h, and_exit(h): - h.press('^W') - h.await_text('search:') - h.press_and_enter('search1') - h.await_text('no matches') - - h.press('^W') - h.await_text('search [search1]:') - h.press_and_enter('search2') - h.await_text('no matches') - - h.press('^W') - h.await_text('search [search2]:') - h.press_and_enter('search2') - h.await_text('no matches') - - h.press('^W') - h.press('Up') - h.await_text('search2') - h.press('Up') - h.await_text('search1') - h.press('Enter') - - -def test_search_history_is_saved_between_sessions(xdg_data_home): - with run() as h, and_exit(h): - h.press('^W') - h.press_and_enter('search1') - h.press('^W') - h.press_and_enter('search2') - - contents = xdg_data_home.join('babi/history/search').read() - assert contents == 'search1\nsearch2\n' - - with run() as h, and_exit(h): - h.press('^W') - h.press('Up') - h.await_text('search: search2') - h.press('Up') - h.await_text('search: search1') - h.press('Enter') - - -def test_search_multiple_sessions_append_to_history(xdg_data_home): - xdg_data_home.join('babi/history/search').ensure().write( - 'orig\n' - 'history\n', - ) - - with run() as h1, and_exit(h1): - with run() as h2, and_exit(h2): - h2.press('^W') - h2.press_and_enter('h2 history') - h1.press('^W') - h1.press_and_enter('h1 history') - - contents = xdg_data_home.join('babi/history/search').read() - assert contents == ( - 'orig\n' - 'history\n' - 'h2 history\n' - 'h1 history\n' - ) - - -def test_search_reverse_search_history(xdg_data_home, ten_lines): - xdg_data_home.join('babi/history/search').ensure().write( - 'line_5\n' - 'line_3\n' - 'line_1\n', - ) - with run(str(ten_lines)) as h, and_exit(h): - h.press('^W') - h.press('^R') - h.await_text('search(reverse-search)``:') - h.press('line') - h.await_text('search(reverse-search)`line`: line_1') - h.press('a') - h.await_text('search(failed reverse-search)`linea`: line_1') - h.press('BSpace') - h.await_text('search(reverse-search)`line`: line_1') - h.press('^R') - h.await_text('search(reverse-search)`line`: line_3') - h.press('Enter') - h.await_cursor_position(x=0, y=4) - - -def test_search_reverse_search_history_pos_after(xdg_data_home, ten_lines): - xdg_data_home.join('babi/history/search').ensure().write( - 'line_3\n', - ) - with run(str(ten_lines), height=20) as h, and_exit(h): - h.press('^W') - h.press('^R') - h.press('line') - h.await_text('search(reverse-search)`line`: line_3') - h.press('Right') - h.await_text('search: line_3') - h.await_cursor_position(y=19, x=14) - h.press('^C') - - -def test_search_reverse_search_enter_saves_entry(xdg_data_home, ten_lines): - xdg_data_home.join('babi/history/search').ensure().write( - 'line_1\n' - 'line_3\n', - ) - with run(str(ten_lines)) as h, and_exit(h): - h.press('^W') - h.press('^R') - h.press('1') - h.await_text('search(reverse-search)`1`: line_1') - h.press('Enter') - h.press('^W') - h.press('Up') - h.await_text('search [line_1]: line_1') - h.press('^C') - - -def test_search_reverse_search_history_cancel(): - with run() as h, and_exit(h): - h.press('^W') - h.press('^R') - h.await_text('search(reverse-search)``:') - h.press('^C') - h.await_text('cancelled') - - -def test_search_reverse_search_resizing(): - with run() as h, and_exit(h): - h.press('^W') - h.press('^R') - with h.resize(width=24, height=24): - h.await_text('search(reverse-se…:') - h.press('^C') - - -def test_search_reverse_search_does_not_wrap_around(xdg_data_home): - xdg_data_home.join('babi/history/search').ensure().write( - 'line_1\n' - 'line_3\n', - ) - with run() as h, and_exit(h): - h.press('^W') - h.press('^R') - # this should not wrap around - for i in range(6): - h.press('^R') - h.await_text('search(reverse-search)``: line_1') - h.press('^C') - - -def test_search_reverse_search_ctrl_r_on_failed_match(xdg_data_home): - xdg_data_home.join('babi/history/search').ensure().write( - 'nomatch\n' - 'line_1\n', - ) - with run() as h, and_exit(h): - h.press('^W') - h.press('^R') - h.press('line') - h.await_text('search(reverse-search)`line`: line_1') - h.press('^R') - h.await_text('search(failed reverse-search)`line`: line_1') - h.press('^C') - - -def test_search_reverse_search_keeps_current_text_displayed(): - with run() as h, and_exit(h): - h.press('^W') - h.press('ohai') - h.await_text('search: ohai') - h.press('^R') - h.await_text('search(reverse-search)``: ohai') - h.press('^C') - - -@pytest.mark.parametrize('key', ('^C', 'Enter')) -def test_replace_cancel(key): - with run() as h, and_exit(h): - h.press('^\\') - h.await_text('search (to replace):') - h.press(key) - h.await_text('cancelled') - - -def test_replace_invalid_regex(): - with run() as h, and_exit(h): - h.press('^\\') - h.await_text('search (to replace):') - h.press_and_enter('(') - h.await_text("invalid regex: '('") - - -def test_replace_cancel_at_replace_string(): - with run() as h, and_exit(h): - h.press('^\\') - h.await_text('search (to replace):') - h.press_and_enter('hello') - h.await_text('replace with:') - h.press('^C') - h.await_text('cancelled') - - -def test_replace_actual_contents(ten_lines): - with run(str(ten_lines)) as h, and_exit(h): - h.press('^\\') - h.await_text('search (to replace):') - h.press_and_enter('line_0') - h.await_text('replace with:') - h.press_and_enter('ohai') - h.await_text('replace [y(es), n(o), a(ll)]?') - h.press('y') - h.await_text_missing('line_0') - h.await_text('ohai') - h.await_text(' *') - h.await_text('replaced 1 occurrence') - - -def test_replace_cancel_at_individual_replace(ten_lines): - with run(str(ten_lines)) as h, and_exit(h): - h.press('^\\') - h.await_text('search (to replace):') - h.press_and_enter(r'line_\d') - h.await_text('replace with:') - h.press_and_enter('ohai') - h.await_text('replace [y(es), n(o), a(ll)]?') - h.press('^C') - h.await_text('cancelled') - - -def test_replace_unknown_characters_at_individual_replace(ten_lines): - with run(str(ten_lines)) as h, and_exit(h): - h.press('^\\') - h.await_text('search (to replace):') - h.press_and_enter(r'line_\d') - h.await_text('replace with:') - h.press_and_enter('ohai') - h.await_text('replace [y(es), n(o), a(ll)]?') - h.press('?') - h.press('^C') - h.await_text('cancelled') - - -def test_replace_say_no_to_individual_replace(ten_lines): - with run(str(ten_lines)) as h, and_exit(h): - h.press('^\\') - h.await_text('search (to replace):') - h.press_and_enter('line_[135]') - h.await_text('replace with:') - h.press_and_enter('ohai') - h.await_text('replace [y(es), n(o), a(ll)]?') - h.press('y') - h.await_text_missing('line_1') - h.press('n') - h.await_text('line_3') - h.press('y') - h.await_text_missing('line_5') - h.await_text('replaced 2 occurrences') - - -def test_replace_all(ten_lines): - with run(str(ten_lines)) as h, and_exit(h): - h.press('^\\') - h.await_text('search (to replace):') - h.press_and_enter(r'line_(\d)') - h.await_text('replace with:') - h.press_and_enter(r'ohai+\1') - h.await_text('replace [y(es), n(o), a(ll)]?') - h.press('a') - h.await_text_missing('line') - h.await_text('ohai+1') - h.await_text('replaced 10 occurrences') - - -def test_replace_with_empty_string(ten_lines): - with run(str(ten_lines)) as h, and_exit(h): - h.press('^\\') - h.await_text('search (to replace):') - h.press_and_enter('line_1') - h.await_text('replace with:') - h.press('Enter') - h.await_text('replace [y(es), n(o), a(ll)]?') - h.press('y') - h.await_text_missing('line_1') - - -def test_replace_search_not_found(ten_lines): - with run(str(ten_lines)) as h, and_exit(h): - h.press('^\\') - h.await_text('search (to replace):') - h.press_and_enter('wat') - # TODO: would be nice to not prompt for a replace string in this case - h.await_text('replace with:') - h.press('Enter') - h.await_text('no matches') - - -def test_replace_small_window_size(ten_lines): - with run(str(ten_lines)) as h, and_exit(h): - h.press('^\\') - h.await_text('search (to replace):') - h.press_and_enter('line') - h.await_text('replace with:') - h.press_and_enter('wat') - h.await_text('replace [y(es), n(o), a(ll)]?') - - with h.resize(width=8, height=24): - h.await_text('replace…') - - h.press('^C') - - -def test_replace_line_goes_off_screen(): - with run() as h, and_exit(h): - h.press(f'{"a" * 20}{"b" * 90}') - h.press('^A') - h.await_text(f'{"a" * 20}{"b" * 59}»') - h.press('^\\') - h.await_text('search (to replace):') - h.press_and_enter('b+') - h.await_text('replace with:') - h.press_and_enter('wat') - h.await_text('replace [y(es), n(o), a(ll)]?') - h.await_text(f'{"a" * 20}{"b" * 59}»') - h.press('y') - h.await_text(f'{"a" * 20}wat') - h.await_text('replaced 1 occurrence') - - -def test_replace_undo_undoes_only_one(ten_lines): - with run(str(ten_lines)) as h, and_exit(h): - h.press('^\\') - h.await_text('search (to replace):') - h.press_and_enter('line') - h.await_text('replace with:') - h.press_and_enter('wat') - h.press('y') - h.await_text_missing('line_0') - h.press('y') - h.await_text_missing('line_1') - h.press('^C') - h.press('M-u') - h.await_text('line_1') - h.await_text_missing('line_0') - - -def test_replace_multiple_occurrences_in_line(): - with run() as h, and_exit(h): - h.press('baaaaabaaaaa') - h.press('^\\') - h.await_text('search (to replace):') - h.press_and_enter('a+') - h.await_text('replace with:') - h.press_and_enter('q') - h.await_text('replace [y(es), n(o), a(ll)]?') - h.press('a') - h.await_text('bqbq') - - -def test_replace_after_wrapping(ten_lines): - with run(str(ten_lines)) as h, and_exit(h): - h.press('Down') - h.press('^\\') - h.await_text('search (to replace):') - h.press_and_enter('line_[02]') - h.await_text('replace with:') - h.press_and_enter('ohai') - h.await_text('replace [y(es), n(o), a(ll)]?') - h.press('y') - h.await_text_missing('line_2') - h.press('y') - h.await_text_missing('line_0') - h.await_text('replaced 2 occurrences') - - -def test_replace_after_cursor_after_wrapping(): - with run() as h, and_exit(h): - h.press('baaab') - h.press('Left') - h.press('^\\') - h.await_text('search (to replace):') - h.press_and_enter('b') - h.await_text('replace with:') - h.press_and_enter('q') - h.await_text('replace [y(es), n(o), a(ll)]?') - h.press('n') - h.press('y') - h.await_text('replaced 1 occurrence') - h.await_text('qaaab') - - -def test_replace_separate_line_after_wrapping(ten_lines): - with run(str(ten_lines)) as h, and_exit(h): - h.press('Down') - h.press('Down') - h.press('^\\') - h.await_text('search (to replace):') - h.press_and_enter('line_[01]') - h.await_text('replace with:') - h.press_and_enter('_') - h.await_text('replace [y(es), n(o), a(ll)]?') - h.press('y') - h.await_text_missing('line_0') - h.press('y') - h.await_text_missing('line_1') - - -def test_scrolling_arrow_key_movement(ten_lines): - with run(str(ten_lines), height=10) as h, and_exit(h): - h.await_text('line_7') - # we should not have scrolled after 7 presses - for _ in range(7): - h.press('Down') - h.await_text('line_0') - h.await_cursor_position(x=0, y=8) - # but this should scroll down - h.press('Down') - h.await_text('line_8') - h.await_cursor_position(x=0, y=4) - assert h.get_cursor_line() == 'line_8' - # we should not have scrolled after 3 up presses - for _ in range(3): - h.press('Up') - h.await_text('line_9') - # but this should scroll up - h.press('Up') - h.await_text('line_0') - - -def test_ctrl_down_beginning_of_file(ten_lines): - with run(str(ten_lines), height=5) as h, and_exit(h): - h.press('^Down') - h.await_text('line_3') - h.await_cursor_position(x=0, y=1) - assert h.get_cursor_line() == 'line_1' - - -def test_ctrl_up_moves_screen_up_one_line(ten_lines): - with run(str(ten_lines), height=5) as h, and_exit(h): - h.press('^Down') - h.press('^Up') - h.await_text('line_0') - h.await_text('line_2') - h.await_cursor_position(x=0, y=2) - - -def test_ctrl_up_at_beginning_of_file_does_nothing(ten_lines): - with run(str(ten_lines), height=5) as h, and_exit(h): - h.press('^Up') - h.await_text('line_0') - h.await_text('line_2') - h.await_cursor_position(x=0, y=1) - - -def test_ctrl_up_at_bottom_of_screen(ten_lines): - with run(str(ten_lines), height=5) as h, and_exit(h): - h.press('^Down') - h.press('Down') - h.press('Down') - h.await_text('line_1') - h.await_text('line_3') - h.await_cursor_position(x=0, y=3) - h.press('^Up') - h.await_text('line_0') - h.await_cursor_position(x=0, y=3) - - -def test_ctrl_down_at_end_of_file(ten_lines): - with run(str(ten_lines), height=5) as h, and_exit(h): - h.press('^End') - for i in range(4): - h.press('^Down') - h.press('Up') - h.await_text('line_9') - assert h.get_cursor_line() == 'line_9' - - -def test_ctrl_down_causing_cursor_movement_should_fix_x(tmpdir): - f = tmpdir.join('f') - f.write('line_1\n\nline_2\n\nline_3\n\nline_4\n') - - with run(str(f), height=5) as h, and_exit(h): - h.press('Right') - h.press('^Down') - h.await_text_missing('\nline_1\n') - h.await_cursor_position(x=0, y=1) - - -def test_ctrl_up_causing_cursor_movement_should_fix_x(tmpdir): - f = tmpdir.join('f') - f.write('line_1\n\nline_2\n\nline_3\n\nline_4\n') - - with run(str(f), height=5) as h, and_exit(h): - h.press('^Down') - h.press('^Down') - h.press('Down') - h.press('Down') - h.press('Right') - h.await_text('line_3') - h.press('^Up') - h.await_text_missing('3') - h.await_cursor_position(x=0, y=3) - - -@pytest.mark.parametrize('k', ('End', '^E')) -def test_end_key(tmpdir, k): - f = tmpdir.join('f') - f.write('hello world\nhello world\n') - - with run(str(f)) as h, and_exit(h): - h.await_text('hello world') - h.await_cursor_position(x=0, y=1) - h.press(k) - h.await_cursor_position(x=11, y=1) - h.press('Down') - h.await_cursor_position(x=11, y=2) - - -@pytest.mark.parametrize('k', ('Home', '^A')) -def test_home_key(tmpdir, k): - f = tmpdir.join('f') - f.write('hello world\nhello world\n') - - with run(str(f)) as h, and_exit(h): - h.await_text('hello world') - h.press('Down') - h.press('Left') - h.await_cursor_position(x=11, y=1) - h.press(k) - h.await_cursor_position(x=0, y=1) - h.press('Down') - h.await_cursor_position(x=0, y=2) - - -def test_resize_scrolls_up(ten_lines): - with run(str(ten_lines)) as h, and_exit(h): - h.await_text('line_9') - - for _ in range(7): - h.press('Down') - h.await_cursor_position(x=0, y=8) - - # a resize to a height of 10 should not scroll - with h.resize(80, 10): - h.await_text_missing('line_8') - h.await_cursor_position(x=0, y=8) - - h.await_text('line_8') - - # but a resize to smaller should - with h.resize(80, 9): - h.await_text_missing('line_0') - h.await_cursor_position(x=0, y=3) - # make sure we're still on the same line - assert h.get_cursor_line() == 'line_7' - - -def test_resize_scroll_does_not_go_negative(ten_lines): - with run(str(ten_lines)) as h, and_exit(h): - for _ in range(5): - h.press('Down') - h.await_cursor_position(x=0, y=6) - - with h.resize(80, 7): - h.await_text_missing('line_9') - h.await_text('line_9') - - for _ in range(2): - h.press('Up') - - assert h.get_screen_line(1) == 'line_0' - - -def test_page_up_does_not_go_negative(ten_lines): - with run(str(ten_lines), height=10) as h, and_exit(h): - for _ in range(8): - h.press('Down') - h.await_cursor_position(x=0, y=4) - h.press('^Y') - h.await_cursor_position(x=0, y=1) - assert h.get_cursor_line() == 'line_0' - - -def test_very_narrow_window_status(): - with run(height=50) as h, and_exit(h): - with h.resize(5, 50): - h.press('^J') - h.await_text('unkno') - - -def test_horizontal_scrolling(tmpdir): - f = tmpdir.join('f') - lots_of_text = ''.join( - ''.join(str(i) * 10 for i in range(10)) - for _ in range(10) - ) - f.write(f'line1\n{lots_of_text}\n') - - with run(str(f)) as h, and_exit(h): - h.await_text('6777777777»') - h.press('Down') - for _ in range(78): - h.press('Right') - h.await_text('6777777777»') - h.press('Right') - h.await_text('«77777778') - h.await_text('344444444445»') - h.await_cursor_position(x=7, y=2) - for _ in range(71): - h.press('Right') - h.await_text('«77777778') - h.await_text('344444444445»') - h.press('Right') - h.await_text('«444445') - h.await_text('1222»') - - -def test_horizontal_scrolling_exact_width(tmpdir): - f = tmpdir.join('f') - f.write('0' * 80) - - with run(str(f)) as h, and_exit(h): - h.await_text('000') - for _ in range(78): - h.press('Right') - h.await_text_missing('»') - h.await_cursor_position(x=78, y=1) - h.press('Right') - h.await_text('«0000000') - h.await_cursor_position(x=7, y=1) - - -def test_horizontal_scrolling_narrow_window(tmpdir): - f = tmpdir.join('f') - f.write(''.join(str(i) * 10 for i in range(10))) - - with run(str(f)) as h, and_exit(h): - with h.resize(5, 24): - h.await_text('0000»') - for _ in range(3): - h.press('Right') - h.await_text('0000»') - h.press('Right') - h.await_cursor_position(x=3, y=1) - h.await_text('«000»') - for _ in range(6): - h.press('Right') - h.await_text('«001»') - - -def test_window_width_1(tmpdir): - f = tmpdir.join('f') - f.write('hello') - - with run(str(f)) as h, and_exit(h): - with h.resize(1, 24): - h.await_text('»') - for _ in range(3): - h.press('Right') - h.await_text('hello') - h.await_cursor_position(x=3, y=1) - - -def test_basic_text_editing(tmpdir): - with run() as h, and_exit(h): - h.press('hello world') - h.await_text('hello world') - h.press('Down') - h.press('bye!') - h.await_text('bye!') - assert h.screenshot().strip().endswith('world\nbye!') - - -def test_backspace_at_beginning_of_file(): - with run() as h, and_exit(h): - h.press('Bspace') - h.await_text_missing('unknown key') - assert h.screenshot().strip().splitlines()[1:] == [] - assert '*' not in h.screenshot() - - -def test_backspace_joins_lines(tmpdir): - f = tmpdir.join('f') - f.write('foo\nbar\nbaz\n') - - with run(str(f)) as h, and_exit(h): - h.await_text('foo') - h.press('Down') - h.press('Bspace') - h.await_text('foobar') - h.await_text('f *') - h.await_cursor_position(x=3, y=1) - # pressing down should retain the X position - h.press('Down') - h.await_cursor_position(x=3, y=2) - - -def test_backspace_at_end_of_file_still_allows_scrolling_down(tmpdir): - f = tmpdir.join('f') - f.write('hello world') - - with run(str(f)) as h, and_exit(h): - h.await_text('hello world') - h.press('Down') - h.press('Bspace') - h.press('Down') - h.await_cursor_position(x=0, y=2) - assert '*' not in h.screenshot() - - -def test_backspace_deletes_text(tmpdir): - f = tmpdir.join('f') - f.write('ohai there') - - with run(str(f)) as h, and_exit(h): - h.await_text('ohai there') - for _ in range(3): - h.press('Right') - h.press('Bspace') - h.await_text('ohi') - h.await_text('f *') - h.await_cursor_position(x=2, y=1) - - -def test_delete_at_end_of_file(tmpdir): - with run() as h, and_exit(h): - h.press('DC') - h.await_text_missing('unknown key') - h.await_text_missing('*') - - -def test_delete_removes_character_afterwards(tmpdir): - f = tmpdir.join('f') - f.write('hello world') - - with run(str(f)) as h, and_exit(h): - h.await_text('hello world') - h.press('Right') - h.press('DC') - h.await_text('hllo world') - h.await_text('f *') - - -def test_delete_at_end_of_line(tmpdir): - f = tmpdir.join('f') - f.write('hello\nworld\n') - - with run(str(f)) as h, and_exit(h): - h.await_text('hello') - h.press('Down') - h.press('Left') - h.press('DC') - h.await_text('helloworld') - h.await_text('f *') - - -def test_press_enter_beginning_of_file(tmpdir): - f = tmpdir.join('f') - f.write('hello world') - - with run(str(f)) as h, and_exit(h): - h.await_text('hello world') - h.press('Enter') - h.await_text('\n\nhello world') - h.await_cursor_position(x=0, y=2) - h.await_text('f *') - - -def test_press_enter_mid_line(tmpdir): - f = tmpdir.join('f') - f.write('hello world') - - with run(str(f)) as h, and_exit(h): - h.await_text('hello world') - for _ in range(5): - h.press('Right') - h.press('Enter') - h.await_text('hello\n world') - h.await_cursor_position(x=0, y=2) - h.press('Up') - h.await_cursor_position(x=0, y=1) - - -def test_suspend(tmpdir): - f = tmpdir.join('f') - f.write('hello') - - with PrintsErrorRunner('bash', '--norc') as h: - cmd = (sys.executable, '-mcoverage', 'run', '-m', 'babi', str(f)) - h.press_and_enter(' '.join(shlex.quote(part) for part in cmd)) - h.await_text(babi.VERSION_STR) - h.await_text('hello') - - h.press('^Z') - h.await_text_missing('hello') - - h.press_and_enter('fg') - h.await_text('hello') - - h.press('^X') - h.press_and_enter('exit') - h.await_exit() - - -def test_suspend_with_resize(tmpdir): - f = tmpdir.join('f') - f.write('hello') - - with PrintsErrorRunner('bash', '--norc') as h: - cmd = (sys.executable, '-mcoverage', 'run', '-m', 'babi', str(f)) - h.press_and_enter(' '.join(shlex.quote(part) for part in cmd)) - h.await_text(babi.VERSION_STR) - h.await_text('hello') - - h.press('^Z') - h.await_text_missing('hello') - - with h.resize(80, 10): - h.press_and_enter('fg') - h.await_text('hello') - - h.press('^X') - h.press_and_enter('exit') - h.await_exit() - - -def test_multiple_files(tmpdir): - a = tmpdir.join('file_a') - a.write('a text') - b = tmpdir.join('file_b') - b.write('b text') - c = tmpdir.join('file_c') - c.write('c text') - - with run(str(a), str(b), str(c)) as h: - h.await_text('file_a') - h.await_text('[1/3]') - h.await_text('a text') - h.press('Right') - h.await_cursor_position(x=1, y=1) - - h.press('M-Right') - h.await_text('file_b') - h.await_text('[2/3]') - h.await_text('b text') - h.await_cursor_position(x=0, y=1) - - h.press('M-Left') - h.await_text('file_a') - h.await_text('[1/3]') - h.await_cursor_position(x=1, y=1) - - # wrap around - h.press('M-Left') - h.await_text('file_c') - h.await_text('[3/3]') - h.await_text('c text') - - # make sure to clear statuses when switching files - h.press('^J') - h.await_text('unknown key') - h.press('M-Right') - h.await_text_missing('unknown key') - h.press('^J') - h.await_text('unknown key') - h.press('M-Left') - h.await_text_missing('unknown key') - - # also make sure to clear statuses when exiting files - h.press('^J') - h.await_text('unknown key') - h.press('^X') - h.await_text('file_a') - h.await_text_missing('unknown key') - h.press('^X') - h.await_text('file_b') - h.press('^X') - h.await_exit() - - -def test_save_no_filename_specified(tmpdir): - f = tmpdir.join('f') - - with run() as h, and_exit(h): - h.press('hello world') - h.press('^S') - h.await_text('enter filename:') - h.press_and_enter(str(f)) - h.await_text('saved! (1 line written)') - h.await_text_missing('*') - assert f.read() == 'hello world\n' - - -@pytest.mark.parametrize('k', ('Enter', '^C')) -def test_save_no_filename_specified_cancel(k): - with run() as h, and_exit(h): - h.press('hello world') - h.press('^S') - h.await_text('enter filename:') - h.press(k) - h.await_text('cancelled') - - -def test_saving_file_on_disk_changes(tmpdir): - # TODO: this should show some sort of diffing thing or just allow overwrite - f = tmpdir.join('f') - - with run(str(f)) as h, and_exit(h): - f.write('hello world') - - h.press('^S') - h.await_text('file changed on disk, not implemented') - - -def test_allows_saving_same_contents_as_modified_contents(tmpdir): - f = tmpdir.join('f') - - with run(str(f)) as h, and_exit(h): - f.write('hello world\n') - h.press('hello world') - h.await_text('hello world') - - h.press('^S') - h.await_text('saved! (1 line written)') - h.await_text_missing('*') - - assert f.read() == 'hello world\n' - - -def test_allows_saving_if_file_on_disk_does_not_change(tmpdir): - f = tmpdir.join('f') - f.write('hello world\n') - - with run(str(f)) as h, and_exit(h): - h.await_text('hello world') - h.press('ohai') - h.press('Enter') - - h.press('^S') - h.await_text('saved! (2 lines written)') - h.await_text_missing('*') - - assert f.read() == 'ohai\nhello world\n' - - -def test_save_file_when_it_did_not_exist(tmpdir): - f = tmpdir.join('f') - - with run(str(f)) as h, and_exit(h): - h.press('hello world') - h.press('^S') - h.await_text('saved! (1 line written)') - h.await_text_missing('*') - - assert f.read() == 'hello world\n' - - -def test_quit_via_colon_q(): - with run() as h: - trigger_command_mode(h) - h.press_and_enter(':q') - h.await_exit() - - -def test_key_navigation_in_command_mode(): - with run() as h, and_exit(h): - trigger_command_mode(h) - h.press('hello world') - h.await_cursor_position(x=11, y=23) - h.press('Left') - h.await_cursor_position(x=10, y=23) - h.press('Right') - h.await_cursor_position(x=11, y=23) - h.press('Home') - h.await_cursor_position(x=0, y=23) - h.press('End') - h.await_cursor_position(x=11, y=23) - h.press('^A') - h.await_cursor_position(x=0, y=23) - h.press('^E') - h.await_cursor_position(x=11, y=23) - - h.press('DC') # does nothing at end - h.await_cursor_position(x=11, y=23) - h.await_text('\nhello world\n') - - h.press('Bspace') - h.await_cursor_position(x=10, y=23) - h.await_text('\nhello worl\n') - - h.press('Home') - - h.press('Bspace') # does nothing at beginning - h.await_cursor_position(x=0, y=23) - h.await_text('\nhello worl\n') - - h.press('DC') - h.await_cursor_position(x=0, y=23) - h.await_text('\nello worl\n') - - # unknown keys don't do anything - h.press('^J') - h.await_text('\nello worl\n') - - h.press('Enter') - - -def test_save_via_command_mode(tmpdir): - f = tmpdir.join('f') - - with run(str(f)) as h, and_exit(h): - h.press('hello world') - trigger_command_mode(h) - h.press_and_enter(':w') - - assert f.read() == 'hello world\n' - - -def test_repeated_command_mode_does_not_show_previous_command(tmpdir): - f = tmpdir.join('f') - - with run(str(f)) as h, and_exit(h): - h.press('ohai') - trigger_command_mode(h) - h.press_and_enter(':w') - trigger_command_mode(h) - h.await_text_missing(':w') - h.press('Enter') - - -def test_write_and_quit(tmpdir): - f = tmpdir.join('f') - - with run(str(f)) as h, and_exit(h): - h.press('hello world') - trigger_command_mode(h) - h.press_and_enter(':wq') - h.await_exit() - - assert f.read() == 'hello world\n' - - -def test_resizing_and_scrolling_in_command_mode(): - with run(width=20) as h, and_exit(h): - h.press('a' * 15) - h.await_text(f'\n{"a" * 15}\n') - trigger_command_mode(h) - h.press('b' * 15) - h.await_text(f'\n{"b" * 15}\n') - - with h.resize(width=16, height=24): - h.await_text('\n«aaaaaa\n') # the text contents - h.await_text('\n«bbbbbb\n') # the text contents - h.await_cursor_position(x=7, y=23) - h.press('Left') - h.await_cursor_position(x=14, y=23) - h.await_text(f'\n{"b" * 15}\n') - - h.press('Enter') - - -def test_invalid_command(): - with run() as h, and_exit(h): - trigger_command_mode(h) - h.press_and_enter(':fake') - h.await_text('invalid command: :fake') - - -def test_empty_command_is_noop(): - with run() as h, and_exit(h): - h.press('hello ') - trigger_command_mode(h) - h.press('Enter') - h.press('world') - h.await_text('hello world') - h.await_text_missing('invalid command') - - -def test_cancel_command_mode(): - with run() as h, and_exit(h): - h.press('hello ') - trigger_command_mode(h) - h.press(':q') - h.press('^C') - h.press('world') - h.await_text('hello world') - h.await_text_missing('invalid command') - - -def test_current_position(ten_lines): - with run(str(ten_lines)) as h, and_exit(h): - h.press('^C') - h.await_text('line 1, col 1 (of 10 lines)') - h.press('Right') - h.press('^C') - h.await_text('line 1, col 2 (of 10 lines)') - h.press('Down') - h.press('^C') - h.await_text('line 2, col 2 (of 10 lines)') - h.press('Up') - for i in range(10): - h.press('^K') - h.press('^C') - h.await_text('line 1, col 1 (of 1 line)') - - -def test_cut_and_uncut(ten_lines): - with run(str(ten_lines)) as h, and_exit(h): - h.press('^K') - h.await_text_missing('line_0') - h.await_text(' *') - h.press('^U') - h.await_text('line_0') - - h.press('^Home') - h.press('^K') - h.press('^K') - h.await_text_missing('line_1') - h.press('^U') - h.await_text('line_0') - - -def test_cut_at_beginning_of_file(): - with run() as h, and_exit(h): - h.press('^K') - h.press('^K') - h.press('^K') - h.await_text_missing('*') - - -def test_cut_end_of_file(): - with run() as h, and_exit(h): - h.press('hi') - h.press('Down') - h.press('^K') - h.press('hi') - - -def test_cut_uncut_multiple_file_buffers(tmpdir): - f1 = tmpdir.join('f1') - f1.write('hello\nworld\n') - f2 = tmpdir.join('f2') - f2.write('good\nbye\n') - - with run(str(f1), str(f2)) as h, and_exit(h): - h.press('^K') - h.await_text_missing('hello') - h.press('^X') - h.await_text_missing('world') - h.press('^U') - h.await_text('hello\ngood\nbye\n') - - -def test_nothing_to_undo_redo(): - with run() as h, and_exit(h): - h.press('M-u') - h.await_text('nothing to undo!') - h.press('M-U') - h.await_text('nothing to redo!') - - -def test_undo_redo(): - with run() as h, and_exit(h): - h.press('hello') - h.await_text('hello') - h.press('M-u') - h.await_text('undo: text') - h.await_text_missing('hello') - h.await_text_missing(' *') - h.press('M-U') - h.await_text('redo: text') - h.await_text('hello') - h.await_text(' *') - - -def test_undo_redo_movement_interrupts_actions(): - with run() as h, and_exit(h): - h.press('hello') - h.press('Left') - h.press('Right') - h.press('world') - h.press('M-u') - h.await_text('undo: text') - h.await_text('hello') - - -def test_undo_redo_action_interrupts_actions(): - with run() as h, and_exit(h): - h.press('hello') - h.await_text('hello') - h.press('Bspace') - h.await_text_missing('hello') - h.press('M-u') - h.await_text('hello') - h.press('world') - h.await_text('helloworld') - h.press('M-u') - h.await_text_missing('world') - h.await_text('hello') - - -def test_undo_redo_mixed_newlines(tmpdir): - f = tmpdir.join('f') - f.write_binary(b'foo\nbar\r\n') - - with run(str(f)) as h, and_exit(h): - h.press('hello') - h.press('M-u') - h.await_text('undo: text') - h.await_text(' *') - - -def test_undo_redo_with_save(tmpdir): - f = tmpdir.join('f').ensure() - - with run(str(f)) as h, and_exit(h): - h.press('hello') - h.press('^S') - h.await_text_missing(' *') - h.press('M-u') - h.await_text(' *') - h.press('M-U') - h.await_text_missing(' *') - h.press('M-u') - h.await_text(' *') - h.press('^S') - h.await_text_missing(' *') - h.press('M-U') - h.await_text(' *') - - -def test_undo_redo_implicit_linebreak(tmpdir): - f = tmpdir.join('f') - - with run(str(f)) as h, and_exit(h): - h.press('hello') - h.press('M-u') - h.press('^S') - h.await_text('saved!') - assert f.read() == '' - h.press('M-U') - h.press('^S') - h.await_text('saved!') - assert f.read() == 'hello\n' - - -def test_redo_cleared_after_action(tmpdir): - with run() as h, and_exit(h): - h.press('hello') - h.press('M-u') - h.press('world') - h.press('M-U') - h.await_text('nothing to redo!') - - -def test_undo_no_action_when_noop(): - with run() as h, and_exit(h): - h.press('hello') - h.press('Enter') - h.press('world') - h.press('Down') - h.press('^K') - h.press('M-u') - h.await_text('undo: text') - h.await_cursor_position(x=0, y=2) - - -def test_undo_redo_causes_scroll(): - with run(height=8) as h, and_exit(h): - for i in range(10): - h.press('Enter') - h.await_cursor_position(x=0, y=3) - h.press('M-u') - h.await_cursor_position(x=0, y=1) - h.press('M-U') - h.await_cursor_position(x=0, y=4) diff --git a/tests/color_test.py b/tests/color_test.py new file mode 100644 index 0000000..1ac502f --- /dev/null +++ b/tests/color_test.py @@ -0,0 +1,15 @@ +import pytest + +from testing.runner import and_exit +from testing.runner import run + + +@pytest.mark.parametrize('color', (True, False)) +def test_color_test(color): + with run('--color-test', color=color) as h, and_exit(h): + h.await_text('* 1* 2') + + +def test_can_start_without_color(): + with run(color=False) as h, and_exit(h): + pass diff --git a/tests/command_mode_test.py b/tests/command_mode_test.py new file mode 100644 index 0000000..1dffd07 --- /dev/null +++ b/tests/command_mode_test.py @@ -0,0 +1,145 @@ +from testing.runner import and_exit +from testing.runner import run + + +def trigger_command_mode(h): + # in order to enter a steady state, trigger an unknown key first and then + # press escape to open the command mode. this is necessary as `Escape` is + # the start of "escape sequences" and sending characters too quickly will + # be interpreted as a single keypress + h.press('^J') + h.await_text('unknown key') + h.press('Escape') + h.await_text_missing('unknown key') + + +def test_quit_via_colon_q(): + with run() as h: + trigger_command_mode(h) + h.press_and_enter(':q') + h.await_exit() + + +def test_key_navigation_in_command_mode(): + with run() as h, and_exit(h): + trigger_command_mode(h) + h.press('hello world') + h.await_cursor_position(x=11, y=23) + h.press('Left') + h.await_cursor_position(x=10, y=23) + h.press('Right') + h.await_cursor_position(x=11, y=23) + h.press('Home') + h.await_cursor_position(x=0, y=23) + h.press('End') + h.await_cursor_position(x=11, y=23) + h.press('^A') + h.await_cursor_position(x=0, y=23) + h.press('^E') + h.await_cursor_position(x=11, y=23) + + h.press('DC') # does nothing at end + h.await_cursor_position(x=11, y=23) + h.await_text('\nhello world\n') + + h.press('Bspace') + h.await_cursor_position(x=10, y=23) + h.await_text('\nhello worl\n') + + h.press('Home') + + h.press('Bspace') # does nothing at beginning + h.await_cursor_position(x=0, y=23) + h.await_text('\nhello worl\n') + + h.press('DC') + h.await_cursor_position(x=0, y=23) + h.await_text('\nello worl\n') + + # unknown keys don't do anything + h.press('^J') + h.await_text('\nello worl\n') + + h.press('Enter') + + +def test_save_via_command_mode(tmpdir): + f = tmpdir.join('f') + + with run(str(f)) as h, and_exit(h): + h.press('hello world') + trigger_command_mode(h) + h.press_and_enter(':w') + + assert f.read() == 'hello world\n' + + +def test_repeated_command_mode_does_not_show_previous_command(tmpdir): + f = tmpdir.join('f') + + with run(str(f)) as h, and_exit(h): + h.press('ohai') + trigger_command_mode(h) + h.press_and_enter(':w') + trigger_command_mode(h) + h.await_text_missing(':w') + h.press('Enter') + + +def test_write_and_quit(tmpdir): + f = tmpdir.join('f') + + with run(str(f)) as h, and_exit(h): + h.press('hello world') + trigger_command_mode(h) + h.press_and_enter(':wq') + h.await_exit() + + assert f.read() == 'hello world\n' + + +def test_resizing_and_scrolling_in_command_mode(): + with run(width=20) as h, and_exit(h): + h.press('a' * 15) + h.await_text(f'\n{"a" * 15}\n') + trigger_command_mode(h) + h.press('b' * 15) + h.await_text(f'\n{"b" * 15}\n') + + with h.resize(width=16, height=24): + h.await_text('\n«aaaaaa\n') # the text contents + h.await_text('\n«bbbbbb\n') # the text contents + h.await_cursor_position(x=7, y=23) + h.press('Left') + h.await_cursor_position(x=14, y=23) + h.await_text(f'\n{"b" * 15}\n') + + h.press('Enter') + + +def test_invalid_command(): + with run() as h, and_exit(h): + trigger_command_mode(h) + h.press_and_enter(':fake') + h.await_text('invalid command: :fake') + + +def test_empty_command_is_noop(): + with run() as h, and_exit(h): + h.press('hello ') + trigger_command_mode(h) + h.press('Enter') + h.press('world') + h.await_text('hello world') + h.await_text_missing('invalid command') + + +def test_cancel_command_mode(): + with run() as h, and_exit(h): + h.press('hello ') + trigger_command_mode(h) + h.press(':q') + h.press('^C') + h.press('world') + h.await_text('hello world') + h.await_text_missing('invalid command') diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..33ef7dc --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,18 @@ +import os +from unittest import mock + +import pytest + + +@pytest.fixture(autouse=True) +def xdg_data_home(tmpdir): + data_home = tmpdir.join('data_home') + with mock.patch.dict(os.environ, {'XDG_DATA_HOME': str(data_home)}): + yield data_home + + +@pytest.fixture +def ten_lines(tmpdir): + f = tmpdir.join('f') + f.write('\n'.join(f'line_{i}' for i in range(10))) + yield f diff --git a/tests/current_position_test.py b/tests/current_position_test.py new file mode 100644 index 0000000..7916ac6 --- /dev/null +++ b/tests/current_position_test.py @@ -0,0 +1,19 @@ +from testing.runner import and_exit +from testing.runner import run + + +def test_current_position(ten_lines): + with run(str(ten_lines)) as h, and_exit(h): + h.press('^C') + h.await_text('line 1, col 1 (of 10 lines)') + h.press('Right') + h.press('^C') + h.await_text('line 1, col 2 (of 10 lines)') + h.press('Down') + h.press('^C') + h.await_text('line 2, col 2 (of 10 lines)') + h.press('Up') + for i in range(10): + h.press('^K') + h.press('^C') + h.await_text('line 1, col 1 (of 1 line)') diff --git a/tests/cut_uncut_test.py b/tests/cut_uncut_test.py new file mode 100644 index 0000000..1281b0d --- /dev/null +++ b/tests/cut_uncut_test.py @@ -0,0 +1,49 @@ +from testing.runner import and_exit +from testing.runner import run + + +def test_cut_and_uncut(ten_lines): + with run(str(ten_lines)) as h, and_exit(h): + h.press('^K') + h.await_text_missing('line_0') + h.await_text(' *') + h.press('^U') + h.await_text('line_0') + + h.press('^Home') + h.press('^K') + h.press('^K') + h.await_text_missing('line_1') + h.press('^U') + h.await_text('line_0') + + +def test_cut_at_beginning_of_file(): + with run() as h, and_exit(h): + h.press('^K') + h.press('^K') + h.press('^K') + h.await_text_missing('*') + + +def test_cut_end_of_file(): + with run() as h, and_exit(h): + h.press('hi') + h.press('Down') + h.press('^K') + h.press('hi') + + +def test_cut_uncut_multiple_file_buffers(tmpdir): + f1 = tmpdir.join('f1') + f1.write('hello\nworld\n') + f2 = tmpdir.join('f2') + f2.write('good\nbye\n') + + with run(str(f1), str(f2)) as h, and_exit(h): + h.press('^K') + h.await_text_missing('hello') + h.press('^X') + h.await_text_missing('world') + h.press('^U') + h.await_text('hello\ngood\nbye\n') diff --git a/tests/file_test.py b/tests/file_test.py new file mode 100644 index 0000000..29ea302 --- /dev/null +++ b/tests/file_test.py @@ -0,0 +1,20 @@ +import babi + + +def test_position_repr(): + ret = repr(babi.File('f.txt')) + assert ret == ( + 'File(\n' + " filename='f.txt',\n" + ' modified=False,\n' + ' lines=[],\n' + " nl='\\n',\n" + ' file_y=0,\n' + ' cursor_y=0,\n' + ' x=0,\n' + ' x_hint=0,\n' + ' sha256=None,\n' + ' undo_stack=[],\n' + ' redo_stack=[],\n' + ')' + ) diff --git a/tests/get_files_test.py b/tests/get_files_test.py new file mode 100644 index 0000000..488a773 --- /dev/null +++ b/tests/get_files_test.py @@ -0,0 +1,27 @@ +import io + +import pytest + +import babi + + +@pytest.mark.parametrize( + ('s', 'lines', 'nl', 'mixed'), + ( + pytest.param('', [''], '\n', False, id='trivial'), + pytest.param('1\n2\n', ['1', '2', ''], '\n', False, id='lf'), + pytest.param('1\r\n2\r\n', ['1', '2', ''], '\r\n', False, id='crlf'), + pytest.param('1\r\n2\n', ['1', '2', ''], '\n', True, id='mixed'), + pytest.param('1\n2', ['1', '2', ''], '\n', False, id='noeol'), + ), +) +def test_get_lines(s, lines, nl, mixed): + # sha256 tested below + ret_lines, ret_nl, ret_mixed, _ = babi._get_lines(io.StringIO(s)) + assert (ret_lines, ret_nl, ret_mixed) == (lines, nl, mixed) + + +def test_get_lines_sha256_checksum(): + ret = babi._get_lines(io.StringIO('')) + sha256 = 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855' + assert ret == ([''], '\n', False, sha256) diff --git a/tests/go_to_line_test.py b/tests/go_to_line_test.py new file mode 100644 index 0000000..3556658 --- /dev/null +++ b/tests/go_to_line_test.py @@ -0,0 +1,69 @@ +import pytest + +from testing.runner import and_exit +from testing.runner import run + + +def test_prompt_window_width(): + with run() as h, and_exit(h): + h.press('^_') + h.await_text('enter line number:') + h.press('123') + with h.resize(width=23, height=24): + h.await_text('\nenter line number: «3') + with h.resize(width=22, height=24): + h.await_text('\nenter line numb…: «3') + with h.resize(width=7, height=24): + h.await_text('\n…: «3') + with h.resize(width=6, height=24): + h.await_text('\n123') + h.press('Enter') + + +def test_go_to_line_line(ten_lines): + def _jump_to_line(n): + h.press('^_') + h.await_text('enter line number:') + h.press_and_enter(str(n)) + h.await_text_missing('enter line number:') + + with run(str(ten_lines), height=9) as h, and_exit(h): + # still on screen + _jump_to_line(3) + h.await_cursor_position(x=0, y=3) + # should go to beginning of file + _jump_to_line(0) + h.await_cursor_position(x=0, y=1) + # should go to end of the file + _jump_to_line(999) + h.await_cursor_position(x=0, y=4) + assert h.get_screen_line(3) == 'line_9' + # should also go to the end of the file + _jump_to_line(-1) + h.await_cursor_position(x=0, y=4) + assert h.get_screen_line(3) == 'line_9' + # should go to beginning of file + _jump_to_line(-999) + h.await_cursor_position(x=0, y=1) + assert h.get_cursor_line() == 'line_0' + + +@pytest.mark.parametrize('key', ('Enter', '^C')) +def test_go_to_line_cancel(ten_lines, key): + with run(str(ten_lines)) as h, and_exit(h): + h.press('Down') + h.await_cursor_position(x=0, y=2) + + h.press('^_') + h.await_text('enter line number:') + h.press(key) + h.await_cursor_position(x=0, y=2) + h.await_text('cancelled') + + +def test_go_to_line_not_an_integer(): + with run() as h, and_exit(h): + h.press('^_') + h.await_text('enter line number:') + h.press_and_enter('asdf') + h.await_text("not an integer: 'asdf'") diff --git a/tests/list_spy_test.py b/tests/list_spy_test.py new file mode 100644 index 0000000..9823259 --- /dev/null +++ b/tests/list_spy_test.py @@ -0,0 +1,144 @@ +import pytest + +import babi + + +def test_list_spy_repr(): + assert repr(babi.ListSpy(['a', 'b', 'c'])) == "ListSpy(['a', 'b', 'c'])" + + +def test_list_spy_item_retrieval(): + spy = babi.ListSpy(['a', 'b', 'c']) + assert spy[1] == 'b' + assert spy[-1] == 'c' + with pytest.raises(IndexError): + spy[3] + + +def test_list_spy_del(): + lst = ['a', 'b', 'c'] + + spy = babi.ListSpy(lst) + del spy[1] + + assert lst == ['a', 'c'] + + spy.undo(lst) + + assert lst == ['a', 'b', 'c'] + + +def test_list_spy_del_with_negative(): + lst = ['a', 'b', 'c'] + + spy = babi.ListSpy(lst) + del spy[-1] + + assert lst == ['a', 'b'] + + spy.undo(lst) + + assert lst == ['a', 'b', 'c'] + + +def test_list_spy_insert(): + lst = ['a', 'b', 'c'] + + spy = babi.ListSpy(lst) + spy.insert(1, 'q') + + assert lst == ['a', 'q', 'b', 'c'] + + spy.undo(lst) + + assert lst == ['a', 'b', 'c'] + + +def test_list_spy_insert_with_negative(): + lst = ['a', 'b', 'c'] + + spy = babi.ListSpy(lst) + spy.insert(-1, 'q') + + assert lst == ['a', 'b', 'q', 'c'] + + spy.undo(lst) + + assert lst == ['a', 'b', 'c'] + + +def test_list_spy_set_value(): + lst = ['a', 'b', 'c'] + + spy = babi.ListSpy(lst) + spy[1] = 'hello' + + assert lst == ['a', 'hello', 'c'] + + spy.undo(lst) + + assert lst == ['a', 'b', 'c'] + + +def test_list_spy_multiple_modifications(): + lst = ['a', 'b', 'c'] + + spy = babi.ListSpy(lst) + spy[1] = 'hello' + spy.insert(1, 'ohai') + del spy[0] + + assert lst == ['ohai', 'hello', 'c'] + + spy.undo(lst) + + assert lst == ['a', 'b', 'c'] + + +def test_list_spy_iter(): + spy = babi.ListSpy(['a', 'b', 'c']) + spy_iter = iter(spy) + assert next(spy_iter) == 'a' + assert next(spy_iter) == 'b' + assert next(spy_iter) == 'c' + with pytest.raises(StopIteration): + next(spy_iter) + + +def test_list_spy_append(): + lst = ['a', 'b', 'c'] + + spy = babi.ListSpy(lst) + spy.append('q') + + assert lst == ['a', 'b', 'c', 'q'] + + spy.undo(lst) + + assert lst == ['a', 'b', 'c'] + + +def test_list_spy_pop_default(): + lst = ['a', 'b', 'c'] + + spy = babi.ListSpy(lst) + spy.pop() + + assert lst == ['a', 'b'] + + spy.undo(lst) + + assert lst == ['a', 'b', 'c'] + + +def test_list_spy_pop_idx(): + lst = ['a', 'b', 'c'] + + spy = babi.ListSpy(lst) + spy.pop(1) + + assert lst == ['a', 'c'] + + spy.undo(lst) + + assert lst == ['a', 'b', 'c'] diff --git a/tests/movement_test.py b/tests/movement_test.py new file mode 100644 index 0000000..9cfb4f3 --- /dev/null +++ b/tests/movement_test.py @@ -0,0 +1,269 @@ +import pytest + +from testing.runner import and_exit +from testing.runner import run + + +def test_arrow_key_movement(tmpdir): + f = tmpdir.join('f') + f.write( + 'short\n' + '\n' + 'long long long long\n', + ) + with run(str(f)) as h, and_exit(h): + h.await_text('short') + h.await_cursor_position(x=0, y=1) + # should not go off the beginning of the file + h.press('Left') + h.await_cursor_position(x=0, y=1) + h.press('Up') + h.await_cursor_position(x=0, y=1) + # left and right should work + h.press('Right') + h.press('Right') + h.await_cursor_position(x=2, y=1) + h.press('Left') + h.await_cursor_position(x=1, y=1) + # up should still be a noop on line 1 + h.press('Up') + h.await_cursor_position(x=1, y=1) + # down once should put it on the beginning of the second line + h.press('Down') + h.await_cursor_position(x=0, y=2) + # down again should restore the x positon on the next line + h.press('Down') + h.await_cursor_position(x=1, y=3) + # down once more should put it on the special end-of-file line + h.press('Down') + h.await_cursor_position(x=0, y=4) + # should not go off the end of the file + h.press('Down') + h.await_cursor_position(x=0, y=4) + h.press('Right') + h.await_cursor_position(x=0, y=4) + # left should put it at the end of the line + h.press('Left') + h.await_cursor_position(x=19, y=3) + # right should put it to the next line + h.press('Right') + h.await_cursor_position(x=0, y=4) + # if the hint-x is too high it should not go past the end of line + h.press('Left') + h.press('Up') + h.press('Up') + h.await_cursor_position(x=5, y=1) + # and moving back down should still retain the hint-x + h.press('Down') + h.press('Down') + h.await_cursor_position(x=19, y=3) + + +@pytest.mark.parametrize( + ('page_up', 'page_down'), + (('PageUp', 'PageDown'), ('^Y', '^V')), +) +def test_page_up_and_page_down(ten_lines, page_up, page_down): + with run(str(ten_lines), height=10) as h, and_exit(h): + h.press('Down') + h.press('Down') + h.press(page_up) + h.await_cursor_position(x=0, y=1) + + h.press(page_down) + h.await_text('line_8') + h.await_cursor_position(x=0, y=1) + assert h.get_cursor_line() == 'line_6' + + h.press(page_up) + h.await_text_missing('line_8') + h.await_cursor_position(x=0, y=1) + assert h.get_cursor_line() == 'line_0' + + h.press(page_down) + h.press(page_down) + h.await_cursor_position(x=0, y=5) + assert h.get_cursor_line() == '' + h.press('Up') + h.await_cursor_position(x=0, y=4) + assert h.get_cursor_line() == 'line_9' + + +def test_page_up_page_down_size_small_window(ten_lines): + with run(str(ten_lines), height=4) as h, and_exit(h): + h.press('PageDown') + h.await_text('line_2') + h.await_cursor_position(x=0, y=1) + assert h.get_cursor_line() == 'line_1' + + h.press('Down') + h.press('PageUp') + h.await_text_missing('line_2') + h.await_cursor_position(x=0, y=1) + assert h.get_cursor_line() == 'line_0' + + +def test_ctrl_home(ten_lines): + with run(str(ten_lines), height=4) as h, and_exit(h): + for _ in range(3): + h.press('PageDown') + h.await_text_missing('line_0') + + h.press('^Home') + h.await_text('line_0') + h.await_cursor_position(x=0, y=1) + + +def test_ctrl_end(ten_lines): + with run(str(ten_lines), height=6) as h, and_exit(h): + h.press('^End') + h.await_cursor_position(x=0, y=3) + assert h.get_screen_line(2) == 'line_9' + + +def test_ctrl_end_already_on_last_page(ten_lines): + with run(str(ten_lines), height=9) as h, and_exit(h): + h.press('PageDown') + h.await_cursor_position(x=0, y=1) + h.await_text('line_9') + + h.press('^End') + h.await_cursor_position(x=0, y=6) + assert h.get_screen_line(5) == 'line_9' + + +def test_scrolling_arrow_key_movement(ten_lines): + with run(str(ten_lines), height=10) as h, and_exit(h): + h.await_text('line_7') + # we should not have scrolled after 7 presses + for _ in range(7): + h.press('Down') + h.await_text('line_0') + h.await_cursor_position(x=0, y=8) + # but this should scroll down + h.press('Down') + h.await_text('line_8') + h.await_cursor_position(x=0, y=4) + assert h.get_cursor_line() == 'line_8' + # we should not have scrolled after 3 up presses + for _ in range(3): + h.press('Up') + h.await_text('line_9') + # but this should scroll up + h.press('Up') + h.await_text('line_0') + + +def test_ctrl_down_beginning_of_file(ten_lines): + with run(str(ten_lines), height=5) as h, and_exit(h): + h.press('^Down') + h.await_text('line_3') + h.await_cursor_position(x=0, y=1) + assert h.get_cursor_line() == 'line_1' + + +def test_ctrl_up_moves_screen_up_one_line(ten_lines): + with run(str(ten_lines), height=5) as h, and_exit(h): + h.press('^Down') + h.press('^Up') + h.await_text('line_0') + h.await_text('line_2') + h.await_cursor_position(x=0, y=2) + + +def test_ctrl_up_at_beginning_of_file_does_nothing(ten_lines): + with run(str(ten_lines), height=5) as h, and_exit(h): + h.press('^Up') + h.await_text('line_0') + h.await_text('line_2') + h.await_cursor_position(x=0, y=1) + + +def test_ctrl_up_at_bottom_of_screen(ten_lines): + with run(str(ten_lines), height=5) as h, and_exit(h): + h.press('^Down') + h.press('Down') + h.press('Down') + h.await_text('line_1') + h.await_text('line_3') + h.await_cursor_position(x=0, y=3) + h.press('^Up') + h.await_text('line_0') + h.await_cursor_position(x=0, y=3) + + +def test_ctrl_down_at_end_of_file(ten_lines): + with run(str(ten_lines), height=5) as h, and_exit(h): + h.press('^End') + for i in range(4): + h.press('^Down') + h.press('Up') + h.await_text('line_9') + assert h.get_cursor_line() == 'line_9' + + +def test_ctrl_down_causing_cursor_movement_should_fix_x(tmpdir): + f = tmpdir.join('f') + f.write('line_1\n\nline_2\n\nline_3\n\nline_4\n') + + with run(str(f), height=5) as h, and_exit(h): + h.press('Right') + h.press('^Down') + h.await_text_missing('\nline_1\n') + h.await_cursor_position(x=0, y=1) + + +def test_ctrl_up_causing_cursor_movement_should_fix_x(tmpdir): + f = tmpdir.join('f') + f.write('line_1\n\nline_2\n\nline_3\n\nline_4\n') + + with run(str(f), height=5) as h, and_exit(h): + h.press('^Down') + h.press('^Down') + h.press('Down') + h.press('Down') + h.press('Right') + h.await_text('line_3') + h.press('^Up') + h.await_text_missing('3') + h.await_cursor_position(x=0, y=3) + + +@pytest.mark.parametrize('k', ('End', '^E')) +def test_end_key(tmpdir, k): + f = tmpdir.join('f') + f.write('hello world\nhello world\n') + + with run(str(f)) as h, and_exit(h): + h.await_text('hello world') + h.await_cursor_position(x=0, y=1) + h.press(k) + h.await_cursor_position(x=11, y=1) + h.press('Down') + h.await_cursor_position(x=11, y=2) + + +@pytest.mark.parametrize('k', ('Home', '^A')) +def test_home_key(tmpdir, k): + f = tmpdir.join('f') + f.write('hello world\nhello world\n') + + with run(str(f)) as h, and_exit(h): + h.await_text('hello world') + h.press('Down') + h.press('Left') + h.await_cursor_position(x=11, y=1) + h.press(k) + h.await_cursor_position(x=0, y=1) + h.press('Down') + h.await_cursor_position(x=0, y=2) + + +def test_page_up_does_not_go_negative(ten_lines): + with run(str(ten_lines), height=10) as h, and_exit(h): + for _ in range(8): + h.press('Down') + h.await_cursor_position(x=0, y=4) + h.press('^Y') + h.await_cursor_position(x=0, y=1) + assert h.get_cursor_line() == 'line_0' diff --git a/tests/multiple_files_test.py b/tests/multiple_files_test.py new file mode 100644 index 0000000..d140aac --- /dev/null +++ b/tests/multiple_files_test.py @@ -0,0 +1,55 @@ +from testing.runner import run + + +def test_multiple_files(tmpdir): + a = tmpdir.join('file_a') + a.write('a text') + b = tmpdir.join('file_b') + b.write('b text') + c = tmpdir.join('file_c') + c.write('c text') + + with run(str(a), str(b), str(c)) as h: + h.await_text('file_a') + h.await_text('[1/3]') + h.await_text('a text') + h.press('Right') + h.await_cursor_position(x=1, y=1) + + h.press('M-Right') + h.await_text('file_b') + h.await_text('[2/3]') + h.await_text('b text') + h.await_cursor_position(x=0, y=1) + + h.press('M-Left') + h.await_text('file_a') + h.await_text('[1/3]') + h.await_cursor_position(x=1, y=1) + + # wrap around + h.press('M-Left') + h.await_text('file_c') + h.await_text('[3/3]') + h.await_text('c text') + + # make sure to clear statuses when switching files + h.press('^J') + h.await_text('unknown key') + h.press('M-Right') + h.await_text_missing('unknown key') + h.press('^J') + h.await_text('unknown key') + h.press('M-Left') + h.await_text_missing('unknown key') + + # also make sure to clear statuses when exiting files + h.press('^J') + h.await_text('unknown key') + h.press('^X') + h.await_text('file_a') + h.await_text_missing('unknown key') + h.press('^X') + h.await_text('file_b') + h.press('^X') + h.await_exit() diff --git a/tests/replace_test.py b/tests/replace_test.py new file mode 100644 index 0000000..cf68fe2 --- /dev/null +++ b/tests/replace_test.py @@ -0,0 +1,235 @@ +import pytest + +from testing.runner import and_exit +from testing.runner import run + + +@pytest.mark.parametrize('key', ('^C', 'Enter')) +def test_replace_cancel(key): + with run() as h, and_exit(h): + h.press('^\\') + h.await_text('search (to replace):') + h.press(key) + h.await_text('cancelled') + + +def test_replace_invalid_regex(): + with run() as h, and_exit(h): + h.press('^\\') + h.await_text('search (to replace):') + h.press_and_enter('(') + h.await_text("invalid regex: '('") + + +def test_replace_cancel_at_replace_string(): + with run() as h, and_exit(h): + h.press('^\\') + h.await_text('search (to replace):') + h.press_and_enter('hello') + h.await_text('replace with:') + h.press('^C') + h.await_text('cancelled') + + +def test_replace_actual_contents(ten_lines): + with run(str(ten_lines)) as h, and_exit(h): + h.press('^\\') + h.await_text('search (to replace):') + h.press_and_enter('line_0') + h.await_text('replace with:') + h.press_and_enter('ohai') + h.await_text('replace [y(es), n(o), a(ll)]?') + h.press('y') + h.await_text_missing('line_0') + h.await_text('ohai') + h.await_text(' *') + h.await_text('replaced 1 occurrence') + + +def test_replace_cancel_at_individual_replace(ten_lines): + with run(str(ten_lines)) as h, and_exit(h): + h.press('^\\') + h.await_text('search (to replace):') + h.press_and_enter(r'line_\d') + h.await_text('replace with:') + h.press_and_enter('ohai') + h.await_text('replace [y(es), n(o), a(ll)]?') + h.press('^C') + h.await_text('cancelled') + + +def test_replace_unknown_characters_at_individual_replace(ten_lines): + with run(str(ten_lines)) as h, and_exit(h): + h.press('^\\') + h.await_text('search (to replace):') + h.press_and_enter(r'line_\d') + h.await_text('replace with:') + h.press_and_enter('ohai') + h.await_text('replace [y(es), n(o), a(ll)]?') + h.press('?') + h.press('^C') + h.await_text('cancelled') + + +def test_replace_say_no_to_individual_replace(ten_lines): + with run(str(ten_lines)) as h, and_exit(h): + h.press('^\\') + h.await_text('search (to replace):') + h.press_and_enter('line_[135]') + h.await_text('replace with:') + h.press_and_enter('ohai') + h.await_text('replace [y(es), n(o), a(ll)]?') + h.press('y') + h.await_text_missing('line_1') + h.press('n') + h.await_text('line_3') + h.press('y') + h.await_text_missing('line_5') + h.await_text('replaced 2 occurrences') + + +def test_replace_all(ten_lines): + with run(str(ten_lines)) as h, and_exit(h): + h.press('^\\') + h.await_text('search (to replace):') + h.press_and_enter(r'line_(\d)') + h.await_text('replace with:') + h.press_and_enter(r'ohai+\1') + h.await_text('replace [y(es), n(o), a(ll)]?') + h.press('a') + h.await_text_missing('line') + h.await_text('ohai+1') + h.await_text('replaced 10 occurrences') + + +def test_replace_with_empty_string(ten_lines): + with run(str(ten_lines)) as h, and_exit(h): + h.press('^\\') + h.await_text('search (to replace):') + h.press_and_enter('line_1') + h.await_text('replace with:') + h.press('Enter') + h.await_text('replace [y(es), n(o), a(ll)]?') + h.press('y') + h.await_text_missing('line_1') + + +def test_replace_search_not_found(ten_lines): + with run(str(ten_lines)) as h, and_exit(h): + h.press('^\\') + h.await_text('search (to replace):') + h.press_and_enter('wat') + # TODO: would be nice to not prompt for a replace string in this case + h.await_text('replace with:') + h.press('Enter') + h.await_text('no matches') + + +def test_replace_small_window_size(ten_lines): + with run(str(ten_lines)) as h, and_exit(h): + h.press('^\\') + h.await_text('search (to replace):') + h.press_and_enter('line') + h.await_text('replace with:') + h.press_and_enter('wat') + h.await_text('replace [y(es), n(o), a(ll)]?') + + with h.resize(width=8, height=24): + h.await_text('replace…') + + h.press('^C') + + +def test_replace_line_goes_off_screen(): + with run() as h, and_exit(h): + h.press(f'{"a" * 20}{"b" * 90}') + h.press('^A') + h.await_text(f'{"a" * 20}{"b" * 59}»') + h.press('^\\') + h.await_text('search (to replace):') + h.press_and_enter('b+') + h.await_text('replace with:') + h.press_and_enter('wat') + h.await_text('replace [y(es), n(o), a(ll)]?') + h.await_text(f'{"a" * 20}{"b" * 59}»') + h.press('y') + h.await_text(f'{"a" * 20}wat') + h.await_text('replaced 1 occurrence') + + +def test_replace_undo_undoes_only_one(ten_lines): + with run(str(ten_lines)) as h, and_exit(h): + h.press('^\\') + h.await_text('search (to replace):') + h.press_and_enter('line') + h.await_text('replace with:') + h.press_and_enter('wat') + h.press('y') + h.await_text_missing('line_0') + h.press('y') + h.await_text_missing('line_1') + h.press('^C') + h.press('M-u') + h.await_text('line_1') + h.await_text_missing('line_0') + + +def test_replace_multiple_occurrences_in_line(): + with run() as h, and_exit(h): + h.press('baaaaabaaaaa') + h.press('^\\') + h.await_text('search (to replace):') + h.press_and_enter('a+') + h.await_text('replace with:') + h.press_and_enter('q') + h.await_text('replace [y(es), n(o), a(ll)]?') + h.press('a') + h.await_text('bqbq') + + +def test_replace_after_wrapping(ten_lines): + with run(str(ten_lines)) as h, and_exit(h): + h.press('Down') + h.press('^\\') + h.await_text('search (to replace):') + h.press_and_enter('line_[02]') + h.await_text('replace with:') + h.press_and_enter('ohai') + h.await_text('replace [y(es), n(o), a(ll)]?') + h.press('y') + h.await_text_missing('line_2') + h.press('y') + h.await_text_missing('line_0') + h.await_text('replaced 2 occurrences') + + +def test_replace_after_cursor_after_wrapping(): + with run() as h, and_exit(h): + h.press('baaab') + h.press('Left') + h.press('^\\') + h.await_text('search (to replace):') + h.press_and_enter('b') + h.await_text('replace with:') + h.press_and_enter('q') + h.await_text('replace [y(es), n(o), a(ll)]?') + h.press('n') + h.press('y') + h.await_text('replaced 1 occurrence') + h.await_text('qaaab') + + +def test_replace_separate_line_after_wrapping(ten_lines): + with run(str(ten_lines)) as h, and_exit(h): + h.press('Down') + h.press('Down') + h.press('^\\') + h.await_text('search (to replace):') + h.press_and_enter('line_[01]') + h.await_text('replace with:') + h.press_and_enter('_') + h.await_text('replace [y(es), n(o), a(ll)]?') + h.press('y') + h.await_text_missing('line_0') + h.press('y') + h.await_text_missing('line_1') diff --git a/tests/resize_test.py b/tests/resize_test.py new file mode 100644 index 0000000..c4e732b --- /dev/null +++ b/tests/resize_test.py @@ -0,0 +1,161 @@ +import babi +from testing.runner import and_exit +from testing.runner import run + + +def test_window_height_2(tmpdir): + # 2 tall: + # - header is hidden, otherwise behaviour is normal + f = tmpdir.join('f.txt') + f.write('hello world') + + with run(str(f)) as h, and_exit(h): + h.await_text('hello world') + + with h.resize(80, 2): + h.await_text_missing(babi.VERSION_STR) + assert h.screenshot() == 'hello world\n\n' + h.press('^J') + h.await_text('unknown key') + + h.await_text(babi.VERSION_STR) + + +def test_window_height_1(tmpdir): + # 1 tall: + # - only file contents as body + # - status takes precedence over body, but cleared after single action + f = tmpdir.join('f.txt') + f.write('hello world') + + with run(str(f)) as h, and_exit(h): + h.await_text('hello world') + + with h.resize(80, 1): + h.await_text_missing(babi.VERSION_STR) + assert h.screenshot() == 'hello world\n' + h.press('^J') + h.await_text('unknown key') + h.press('Right') + h.await_text_missing('unknown key') + h.press('Down') + + +def test_reacts_to_resize(): + with run() as h, and_exit(h): + first_line = h.get_screen_line(0) + with h.resize(40, 20): + # the first line should be different after resize + h.await_text_missing(first_line) + + +def test_resize_scrolls_up(ten_lines): + with run(str(ten_lines)) as h, and_exit(h): + h.await_text('line_9') + + for _ in range(7): + h.press('Down') + h.await_cursor_position(x=0, y=8) + + # a resize to a height of 10 should not scroll + with h.resize(80, 10): + h.await_text_missing('line_8') + h.await_cursor_position(x=0, y=8) + + h.await_text('line_8') + + # but a resize to smaller should + with h.resize(80, 9): + h.await_text_missing('line_0') + h.await_cursor_position(x=0, y=3) + # make sure we're still on the same line + assert h.get_cursor_line() == 'line_7' + + +def test_resize_scroll_does_not_go_negative(ten_lines): + with run(str(ten_lines)) as h, and_exit(h): + for _ in range(5): + h.press('Down') + h.await_cursor_position(x=0, y=6) + + with h.resize(80, 7): + h.await_text_missing('line_9') + h.await_text('line_9') + + for _ in range(2): + h.press('Up') + + assert h.get_screen_line(1) == 'line_0' + + +def test_horizontal_scrolling(tmpdir): + f = tmpdir.join('f') + lots_of_text = ''.join( + ''.join(str(i) * 10 for i in range(10)) + for _ in range(10) + ) + f.write(f'line1\n{lots_of_text}\n') + + with run(str(f)) as h, and_exit(h): + h.await_text('6777777777»') + h.press('Down') + for _ in range(78): + h.press('Right') + h.await_text('6777777777»') + h.press('Right') + h.await_text('«77777778') + h.await_text('344444444445»') + h.await_cursor_position(x=7, y=2) + for _ in range(71): + h.press('Right') + h.await_text('«77777778') + h.await_text('344444444445»') + h.press('Right') + h.await_text('«444445') + h.await_text('1222»') + + +def test_horizontal_scrolling_exact_width(tmpdir): + f = tmpdir.join('f') + f.write('0' * 80) + + with run(str(f)) as h, and_exit(h): + h.await_text('000') + for _ in range(78): + h.press('Right') + h.await_text_missing('»') + h.await_cursor_position(x=78, y=1) + h.press('Right') + h.await_text('«0000000') + h.await_cursor_position(x=7, y=1) + + +def test_horizontal_scrolling_narrow_window(tmpdir): + f = tmpdir.join('f') + f.write(''.join(str(i) * 10 for i in range(10))) + + with run(str(f)) as h, and_exit(h): + with h.resize(5, 24): + h.await_text('0000»') + for _ in range(3): + h.press('Right') + h.await_text('0000»') + h.press('Right') + h.await_cursor_position(x=3, y=1) + h.await_text('«000»') + for _ in range(6): + h.press('Right') + h.await_text('«001»') + + +def test_window_width_1(tmpdir): + f = tmpdir.join('f') + f.write('hello') + + with run(str(f)) as h, and_exit(h): + with h.resize(1, 24): + h.await_text('»') + for _ in range(3): + h.press('Right') + h.await_text('hello') + h.await_cursor_position(x=3, y=1) diff --git a/tests/save_test.py b/tests/save_test.py new file mode 100644 index 0000000..dcce822 --- /dev/null +++ b/tests/save_test.py @@ -0,0 +1,103 @@ +import pytest + +from testing.runner import and_exit +from testing.runner import run + + +def test_mixed_newlines(tmpdir): + f = tmpdir.join('f') + f.write_binary(b'foo\nbar\r\n') + with run(str(f)) as h, and_exit(h): + # should start as modified + h.await_text('f *') + h.await_text(r"mixed newlines will be converted to '\n'") + + +def test_new_file(): + with run('this_is_a_new_file') as h, and_exit(h): + h.await_text('this_is_a_new_file') + h.await_text('(new file)') + + +def test_not_a_file(tmpdir): + d = tmpdir.join('d').ensure_dir() + with run(str(d)) as h, and_exit(h): + h.await_text('<>') + h.await_text("d' is not a file") + + +def test_save_no_filename_specified(tmpdir): + f = tmpdir.join('f') + + with run() as h, and_exit(h): + h.press('hello world') + h.press('^S') + h.await_text('enter filename:') + h.press_and_enter(str(f)) + h.await_text('saved! (1 line written)') + h.await_text_missing('*') + assert f.read() == 'hello world\n' + + +@pytest.mark.parametrize('k', ('Enter', '^C')) +def test_save_no_filename_specified_cancel(k): + with run() as h, and_exit(h): + h.press('hello world') + h.press('^S') + h.await_text('enter filename:') + h.press(k) + h.await_text('cancelled') + + +def test_saving_file_on_disk_changes(tmpdir): + # TODO: this should show some sort of diffing thing or just allow overwrite + f = tmpdir.join('f') + + with run(str(f)) as h, and_exit(h): + f.write('hello world') + + h.press('^S') + h.await_text('file changed on disk, not implemented') + + +def test_allows_saving_same_contents_as_modified_contents(tmpdir): + f = tmpdir.join('f') + + with run(str(f)) as h, and_exit(h): + f.write('hello world\n') + h.press('hello world') + h.await_text('hello world') + + h.press('^S') + h.await_text('saved! (1 line written)') + h.await_text_missing('*') + + assert f.read() == 'hello world\n' + + +def test_allows_saving_if_file_on_disk_does_not_change(tmpdir): + f = tmpdir.join('f') + f.write('hello world\n') + + with run(str(f)) as h, and_exit(h): + h.await_text('hello world') + h.press('ohai') + h.press('Enter') + + h.press('^S') + h.await_text('saved! (2 lines written)') + h.await_text_missing('*') + + assert f.read() == 'ohai\nhello world\n' + + +def test_save_file_when_it_did_not_exist(tmpdir): + f = tmpdir.join('f') + + with run(str(f)) as h, and_exit(h): + h.press('hello world') + h.press('^S') + h.await_text('saved! (1 line written)') + h.await_text_missing('*') + + assert f.read() == 'hello world\n' diff --git a/tests/search_test.py b/tests/search_test.py new file mode 100644 index 0000000..98e951f --- /dev/null +++ b/tests/search_test.py @@ -0,0 +1,298 @@ +import pytest + +from testing.runner import and_exit +from testing.runner import run + + +def test_search_wraps(ten_lines): + with run(str(ten_lines)) as h, and_exit(h): + h.press('Down') + h.press('Down') + h.await_cursor_position(x=0, y=3) + h.press('^W') + h.await_text('search:') + h.press_and_enter('^line_0$') + h.await_text('search wrapped') + h.await_cursor_position(x=0, y=1) + + +def test_search_find_next_line(ten_lines): + with run(str(ten_lines)) as h, and_exit(h): + h.await_cursor_position(x=0, y=1) + h.press('^W') + h.await_text('search:') + h.press_and_enter('^line_') + h.await_cursor_position(x=0, y=2) + + +def test_search_find_later_in_line(): + with run() as h, and_exit(h): + h.press_and_enter('lol') + h.press('Up') + h.press('Right') + h.await_cursor_position(x=1, y=1) + + h.press('^W') + h.await_text('search:') + h.press_and_enter('l') + h.await_cursor_position(x=2, y=1) + + +def test_search_only_one_match_already_at_that_match(ten_lines): + with run(str(ten_lines)) as h, and_exit(h): + h.press('Down') + h.await_cursor_position(x=0, y=2) + h.press('^W') + h.await_text('search:') + h.press_and_enter('^line_1$') + h.await_text('this is the only occurrence') + h.await_cursor_position(x=0, y=2) + + +def test_search_not_found(ten_lines): + with run(str(ten_lines)) as h, and_exit(h): + h.press('^W') + h.await_text('search:') + h.press_and_enter('this will not match') + h.await_text('no matches') + h.await_cursor_position(x=0, y=1) + + +def test_search_invalid_regex(ten_lines): + with run(str(ten_lines)) as h, and_exit(h): + h.press('^W') + h.await_text('search:') + h.press_and_enter('invalid(regex') + h.await_text("invalid regex: 'invalid(regex'") + + +@pytest.mark.parametrize('key', ('Enter', '^C')) +def test_search_cancel(ten_lines, key): + with run(str(ten_lines)) as h, and_exit(h): + h.press('^W') + h.await_text('search:') + h.press(key) + h.await_text('cancelled') + + +def test_search_repeated_search(ten_lines): + with run(str(ten_lines)) as h, and_exit(h): + h.press('^W') + h.press('line') + h.await_text('search: line') + h.press('Enter') + h.await_cursor_position(x=0, y=2) + + h.press('^W') + h.await_text('search [line]:') + h.press('Enter') + h.await_cursor_position(x=0, y=3) + + +def test_search_history_recorded(): + with run() as h, and_exit(h): + h.press('^W') + h.await_text('search:') + h.press_and_enter('asdf') + h.await_text('no matches') + + h.press('^W') + h.press('Up') + h.await_text('search [asdf]: asdf') + h.press('BSpace') + h.press('test') + h.await_text('search [asdf]: asdtest') + h.press('Down') + h.await_text_missing('asdtest') + h.press('Down') # can't go past the end + h.press('Up') + h.await_text('asdtest') + h.press('Up') # can't go past the beginning + h.await_text('asdtest') + h.press('enter') + h.await_text('no matches') + + h.press('^W') + h.press('Up') + h.await_text('search [asdtest]: asdtest') + h.press('Up') + h.await_text('search [asdtest]: asdf') + h.press('^C') + + +def test_search_history_duplicates_dont_repeat(): + with run() as h, and_exit(h): + h.press('^W') + h.await_text('search:') + h.press_and_enter('search1') + h.await_text('no matches') + + h.press('^W') + h.await_text('search [search1]:') + h.press_and_enter('search2') + h.await_text('no matches') + + h.press('^W') + h.await_text('search [search2]:') + h.press_and_enter('search2') + h.await_text('no matches') + + h.press('^W') + h.press('Up') + h.await_text('search2') + h.press('Up') + h.await_text('search1') + h.press('Enter') + + +def test_search_history_is_saved_between_sessions(xdg_data_home): + with run() as h, and_exit(h): + h.press('^W') + h.press_and_enter('search1') + h.press('^W') + h.press_and_enter('search2') + + contents = xdg_data_home.join('babi/history/search').read() + assert contents == 'search1\nsearch2\n' + + with run() as h, and_exit(h): + h.press('^W') + h.press('Up') + h.await_text('search: search2') + h.press('Up') + h.await_text('search: search1') + h.press('Enter') + + +def test_search_multiple_sessions_append_to_history(xdg_data_home): + xdg_data_home.join('babi/history/search').ensure().write( + 'orig\n' + 'history\n', + ) + + with run() as h1, and_exit(h1): + with run() as h2, and_exit(h2): + h2.press('^W') + h2.press_and_enter('h2 history') + h1.press('^W') + h1.press_and_enter('h1 history') + + contents = xdg_data_home.join('babi/history/search').read() + assert contents == ( + 'orig\n' + 'history\n' + 'h2 history\n' + 'h1 history\n' + ) + + +def test_search_reverse_search_history(xdg_data_home, ten_lines): + xdg_data_home.join('babi/history/search').ensure().write( + 'line_5\n' + 'line_3\n' + 'line_1\n', + ) + with run(str(ten_lines)) as h, and_exit(h): + h.press('^W') + h.press('^R') + h.await_text('search(reverse-search)``:') + h.press('line') + h.await_text('search(reverse-search)`line`: line_1') + h.press('a') + h.await_text('search(failed reverse-search)`linea`: line_1') + h.press('BSpace') + h.await_text('search(reverse-search)`line`: line_1') + h.press('^R') + h.await_text('search(reverse-search)`line`: line_3') + h.press('Enter') + h.await_cursor_position(x=0, y=4) + + +def test_search_reverse_search_history_pos_after(xdg_data_home, ten_lines): + xdg_data_home.join('babi/history/search').ensure().write( + 'line_3\n', + ) + with run(str(ten_lines), height=20) as h, and_exit(h): + h.press('^W') + h.press('^R') + h.press('line') + h.await_text('search(reverse-search)`line`: line_3') + h.press('Right') + h.await_text('search: line_3') + h.await_cursor_position(y=19, x=14) + h.press('^C') + + +def test_search_reverse_search_enter_saves_entry(xdg_data_home, ten_lines): + xdg_data_home.join('babi/history/search').ensure().write( + 'line_1\n' + 'line_3\n', + ) + with run(str(ten_lines)) as h, and_exit(h): + h.press('^W') + h.press('^R') + h.press('1') + h.await_text('search(reverse-search)`1`: line_1') + h.press('Enter') + h.press('^W') + h.press('Up') + h.await_text('search [line_1]: line_1') + h.press('^C') + + +def test_search_reverse_search_history_cancel(): + with run() as h, and_exit(h): + h.press('^W') + h.press('^R') + h.await_text('search(reverse-search)``:') + h.press('^C') + h.await_text('cancelled') + + +def test_search_reverse_search_resizing(): + with run() as h, and_exit(h): + h.press('^W') + h.press('^R') + with h.resize(width=24, height=24): + h.await_text('search(reverse-se…:') + h.press('^C') + + +def test_search_reverse_search_does_not_wrap_around(xdg_data_home): + xdg_data_home.join('babi/history/search').ensure().write( + 'line_1\n' + 'line_3\n', + ) + with run() as h, and_exit(h): + h.press('^W') + h.press('^R') + # this should not wrap around + for i in range(6): + h.press('^R') + h.await_text('search(reverse-search)``: line_1') + h.press('^C') + + +def test_search_reverse_search_ctrl_r_on_failed_match(xdg_data_home): + xdg_data_home.join('babi/history/search').ensure().write( + 'nomatch\n' + 'line_1\n', + ) + with run() as h, and_exit(h): + h.press('^W') + h.press('^R') + h.press('line') + h.await_text('search(reverse-search)`line`: line_1') + h.press('^R') + h.await_text('search(failed reverse-search)`line`: line_1') + h.press('^C') + + +def test_search_reverse_search_keeps_current_text_displayed(): + with run() as h, and_exit(h): + h.press('^W') + h.press('ohai') + h.await_text('search: ohai') + h.press('^R') + h.await_text('search(reverse-search)``: ohai') + h.press('^C') diff --git a/tests/status_test.py b/tests/status_test.py new file mode 100644 index 0000000..f919c66 --- /dev/null +++ b/tests/status_test.py @@ -0,0 +1,20 @@ +from testing.runner import and_exit +from testing.runner import run + + +def test_status_clearing_behaviour(): + with run() as h, and_exit(h): + h.press('^J') + h.await_text('unknown key') + for i in range(24): + h.press('LEFT') + h.await_text('unknown key') + h.press('LEFT') + h.await_text_missing('unknown key') + + +def test_very_narrow_window_status(): + with run(height=50) as h, and_exit(h): + with h.resize(5, 50): + h.press('^J') + h.await_text('unkno') diff --git a/tests/suspend_test.py b/tests/suspend_test.py new file mode 100644 index 0000000..f5e42e6 --- /dev/null +++ b/tests/suspend_test.py @@ -0,0 +1,48 @@ +import shlex +import sys + +import babi +from testing.runner import PrintsErrorRunner + + +def test_suspend(tmpdir): + f = tmpdir.join('f') + f.write('hello') + + with PrintsErrorRunner('bash', '--norc') as h: + cmd = (sys.executable, '-mcoverage', 'run', '-m', 'babi', str(f)) + h.press_and_enter(' '.join(shlex.quote(part) for part in cmd)) + h.await_text(babi.VERSION_STR) + h.await_text('hello') + + h.press('^Z') + h.await_text_missing('hello') + + h.press_and_enter('fg') + h.await_text('hello') + + h.press('^X') + h.press_and_enter('exit') + h.await_exit() + + +def test_suspend_with_resize(tmpdir): + f = tmpdir.join('f') + f.write('hello') + + with PrintsErrorRunner('bash', '--norc') as h: + cmd = (sys.executable, '-mcoverage', 'run', '-m', 'babi', str(f)) + h.press_and_enter(' '.join(shlex.quote(part) for part in cmd)) + h.await_text(babi.VERSION_STR) + h.await_text('hello') + + h.press('^Z') + h.await_text_missing('hello') + + with h.resize(80, 10): + h.press_and_enter('fg') + h.await_text('hello') + + h.press('^X') + h.press_and_enter('exit') + h.await_exit() diff --git a/tests/text_editing_test.py b/tests/text_editing_test.py new file mode 100644 index 0000000..5e19bf7 --- /dev/null +++ b/tests/text_editing_test.py @@ -0,0 +1,122 @@ +from testing.runner import and_exit +from testing.runner import run + + +def test_basic_text_editing(tmpdir): + with run() as h, and_exit(h): + h.press('hello world') + h.await_text('hello world') + h.press('Down') + h.press('bye!') + h.await_text('bye!') + assert h.screenshot().strip().endswith('world\nbye!') + + +def test_backspace_at_beginning_of_file(): + with run() as h, and_exit(h): + h.press('Bspace') + h.await_text_missing('unknown key') + assert h.screenshot().strip().splitlines()[1:] == [] + assert '*' not in h.screenshot() + + +def test_backspace_joins_lines(tmpdir): + f = tmpdir.join('f') + f.write('foo\nbar\nbaz\n') + + with run(str(f)) as h, and_exit(h): + h.await_text('foo') + h.press('Down') + h.press('Bspace') + h.await_text('foobar') + h.await_text('f *') + h.await_cursor_position(x=3, y=1) + # pressing down should retain the X position + h.press('Down') + h.await_cursor_position(x=3, y=2) + + +def test_backspace_at_end_of_file_still_allows_scrolling_down(tmpdir): + f = tmpdir.join('f') + f.write('hello world') + + with run(str(f)) as h, and_exit(h): + h.await_text('hello world') + h.press('Down') + h.press('Bspace') + h.press('Down') + h.await_cursor_position(x=0, y=2) + assert '*' not in h.screenshot() + + +def test_backspace_deletes_text(tmpdir): + f = tmpdir.join('f') + f.write('ohai there') + + with run(str(f)) as h, and_exit(h): + h.await_text('ohai there') + for _ in range(3): + h.press('Right') + h.press('Bspace') + h.await_text('ohi') + h.await_text('f *') + h.await_cursor_position(x=2, y=1) + + +def test_delete_at_end_of_file(tmpdir): + with run() as h, and_exit(h): + h.press('DC') + h.await_text_missing('unknown key') + h.await_text_missing('*') + + +def test_delete_removes_character_afterwards(tmpdir): + f = tmpdir.join('f') + f.write('hello world') + + with run(str(f)) as h, and_exit(h): + h.await_text('hello world') + h.press('Right') + h.press('DC') + h.await_text('hllo world') + h.await_text('f *') + + +def test_delete_at_end_of_line(tmpdir): + f = tmpdir.join('f') + f.write('hello\nworld\n') + + with run(str(f)) as h, and_exit(h): + h.await_text('hello') + h.press('Down') + h.press('Left') + h.press('DC') + h.await_text('helloworld') + h.await_text('f *') + + +def test_press_enter_beginning_of_file(tmpdir): + f = tmpdir.join('f') + f.write('hello world') + + with run(str(f)) as h, and_exit(h): + h.await_text('hello world') + h.press('Enter') + h.await_text('\n\nhello world') + h.await_cursor_position(x=0, y=2) + h.await_text('f *') + + +def test_press_enter_mid_line(tmpdir): + f = tmpdir.join('f') + f.write('hello world') + + with run(str(f)) as h, and_exit(h): + h.await_text('hello world') + for _ in range(5): + h.press('Right') + h.press('Enter') + h.await_text('hello\n world') + h.await_cursor_position(x=0, y=2) + h.press('Up') + h.await_cursor_position(x=0, y=1) diff --git a/tests/undo_redo_test.py b/tests/undo_redo_test.py new file mode 100644 index 0000000..c6600c0 --- /dev/null +++ b/tests/undo_redo_test.py @@ -0,0 +1,127 @@ +from testing.runner import and_exit +from testing.runner import run + + +def test_nothing_to_undo_redo(): + with run() as h, and_exit(h): + h.press('M-u') + h.await_text('nothing to undo!') + h.press('M-U') + h.await_text('nothing to redo!') + + +def test_undo_redo(): + with run() as h, and_exit(h): + h.press('hello') + h.await_text('hello') + h.press('M-u') + h.await_text('undo: text') + h.await_text_missing('hello') + h.await_text_missing(' *') + h.press('M-U') + h.await_text('redo: text') + h.await_text('hello') + h.await_text(' *') + + +def test_undo_redo_movement_interrupts_actions(): + with run() as h, and_exit(h): + h.press('hello') + h.press('Left') + h.press('Right') + h.press('world') + h.press('M-u') + h.await_text('undo: text') + h.await_text('hello') + + +def test_undo_redo_action_interrupts_actions(): + with run() as h, and_exit(h): + h.press('hello') + h.await_text('hello') + h.press('Bspace') + h.await_text_missing('hello') + h.press('M-u') + h.await_text('hello') + h.press('world') + h.await_text('helloworld') + h.press('M-u') + h.await_text_missing('world') + h.await_text('hello') + + +def test_undo_redo_mixed_newlines(tmpdir): + f = tmpdir.join('f') + f.write_binary(b'foo\nbar\r\n') + + with run(str(f)) as h, and_exit(h): + h.press('hello') + h.press('M-u') + h.await_text('undo: text') + h.await_text(' *') + + +def test_undo_redo_with_save(tmpdir): + f = tmpdir.join('f').ensure() + + with run(str(f)) as h, and_exit(h): + h.press('hello') + h.press('^S') + h.await_text_missing(' *') + h.press('M-u') + h.await_text(' *') + h.press('M-U') + h.await_text_missing(' *') + h.press('M-u') + h.await_text(' *') + h.press('^S') + h.await_text_missing(' *') + h.press('M-U') + h.await_text(' *') + + +def test_undo_redo_implicit_linebreak(tmpdir): + f = tmpdir.join('f') + + with run(str(f)) as h, and_exit(h): + h.press('hello') + h.press('M-u') + h.press('^S') + h.await_text('saved!') + assert f.read() == '' + h.press('M-U') + h.press('^S') + h.await_text('saved!') + assert f.read() == 'hello\n' + + +def test_redo_cleared_after_action(tmpdir): + with run() as h, and_exit(h): + h.press('hello') + h.press('M-u') + h.press('world') + h.press('M-U') + h.await_text('nothing to redo!') + + +def test_undo_no_action_when_noop(): + with run() as h, and_exit(h): + h.press('hello') + h.press('Enter') + h.press('world') + h.press('Down') + h.press('^K') + h.press('M-u') + h.await_text('undo: text') + h.await_cursor_position(x=0, y=2) + + +def test_undo_redo_causes_scroll(): + with run(height=8) as h, and_exit(h): + for i in range(10): + h.press('Enter') + h.await_cursor_position(x=0, y=3) + h.press('M-u') + h.await_cursor_position(x=0, y=1) + h.press('M-U') + h.await_cursor_position(x=0, y=4)