diff options
-rw-r--r-- | mitmproxy/addons/__init__.py | 2 | ||||
-rw-r--r-- | mitmproxy/addons/command_history.py | 144 | ||||
-rw-r--r-- | mitmproxy/command.py | 6 | ||||
-rw-r--r-- | mitmproxy/tools/_main.py | 1 | ||||
-rw-r--r-- | mitmproxy/tools/console/commander/commander.py | 75 | ||||
-rw-r--r-- | mitmproxy/tools/console/statusbar.py | 11 | ||||
-rw-r--r-- | test/mitmproxy/addons/test_command_history.py | 306 | ||||
-rw-r--r-- | test/mitmproxy/tools/console/test_commander.py | 405 |
8 files changed, 733 insertions, 217 deletions
diff --git a/mitmproxy/addons/__init__.py b/mitmproxy/addons/__init__.py index 838fba9b..ee238938 100644 --- a/mitmproxy/addons/__init__.py +++ b/mitmproxy/addons/__init__.py @@ -4,6 +4,7 @@ from mitmproxy.addons import block from mitmproxy.addons import browser from mitmproxy.addons import check_ca from mitmproxy.addons import clientplayback +from mitmproxy.addons import command_history from mitmproxy.addons import core from mitmproxy.addons import cut from mitmproxy.addons import disable_h2c @@ -30,6 +31,7 @@ def default_addons(): anticomp.AntiComp(), check_ca.CheckCA(), clientplayback.ClientPlayback(), + command_history.CommandHistory(), cut.Cut(), disable_h2c.DisableH2C(), export.Export(), diff --git a/mitmproxy/addons/command_history.py b/mitmproxy/addons/command_history.py new file mode 100644 index 00000000..ad4c2346 --- /dev/null +++ b/mitmproxy/addons/command_history.py @@ -0,0 +1,144 @@ +import atexit +import collections +import os +import typing + +from mitmproxy import command +from mitmproxy import ctx + + +class CommandHistory: + def __init__(self, size: int = 300) -> None: + self.saved_commands: typing.Deque[str] = collections.deque(maxlen=size) + self.is_configured = False + + self.filtered_commands: typing.Deque[str] = collections.deque() + self.current_index: int = -1 + self.filter_str: str = '' + self.command_history_path: str = '' + + atexit.register(self.cleanup) + + def cleanup(self): + self._sync_saved_commands() + + @property + def last_filtered_index(self): + return len(self.filtered_commands) - 1 + + @command.command("command_history.clear") + def clear_history(self): + self.saved_commands.clear() + self.filtered_commands.clear() + + with open(self.command_history_path, 'w') as f: + f.truncate(0) + f.seek(0) + f.flush() + f.close() + + self.restart() + + @command.command("command_history.cancel") + def restart(self) -> None: + self.filtered_commands = self.saved_commands.copy() + self.current_index = -1 + + @command.command("command_history.next") + def get_next(self) -> str: + + if self.current_index == -1 or self.current_index == self.last_filtered_index: + self.current_index = -1 + return '' + elif self.current_index < self.last_filtered_index: + self.current_index += 1 + + ret = self.filtered_commands[self.current_index] + + return ret + + @command.command("command_history.prev") + def get_prev(self) -> str: + + if self.current_index == -1: + if self.last_filtered_index >= 0: + self.current_index = self.last_filtered_index + else: + return '' + + elif self.current_index > 0: + self.current_index -= 1 + + ret = self.filtered_commands[self.current_index] + + return ret + + @command.command("command_history.filter") + def set_filter(self, command: str) -> None: + self.filter_str = command + + _filtered_commands = [c for c in self.saved_commands if c.startswith(command)] + self.filtered_commands = collections.deque(_filtered_commands) + + if command and command not in self.filtered_commands: + self.filtered_commands.append(command) + + self.current_index = -1 + + @command.command("command_history.add") + def add_command(self, command: str) -> None: + if command.strip() == '': + return + + self._sync_saved_commands() + + if command in self.saved_commands: + self.saved_commands.remove(command) + self.saved_commands.append(command) + + _history_str = "\n".join(self.saved_commands) + with open(self.command_history_path, 'w') as f: + f.truncate(0) + f.seek(0) + f.write(_history_str) + f.flush() + f.close() + + self.restart() + + def _sync_saved_commands(self): + # First read all commands from the file to merge anything that may + # have come from a different instance of the mitmproxy or sister tools + if not os.path.exists(self.command_history_path): + return + + with open(self.command_history_path, 'r') as f: + _history_lines = f.readlines() + f.close() + + self.saved_commands.clear() + for l in _history_lines: + l = l.strip() + if l in self.saved_commands: + self.saved_commands.remove(l) + self.saved_commands.append(l.strip()) + + def configure(self, updated: typing.Set[str]): + if self.is_configured: + return + + _command_history_dir = os.path.expanduser(ctx.options.confdir) + if not os.path.exists(_command_history_dir): + os.makedirs(_command_history_dir) + + self.command_history_path = os.path.join(_command_history_dir, 'command_history') + _history_lines: typing.List[str] = [] + if os.path.exists(self.command_history_path): + with open(self.command_history_path, 'r') as f: + _history_lines = f.readlines() + f.close() + + for l in _history_lines: + self.add_command(l.strip()) + + self.is_configured = True diff --git a/mitmproxy/command.py b/mitmproxy/command.py index 6977ff91..48f9051e 100644 --- a/mitmproxy/command.py +++ b/mitmproxy/command.py @@ -100,8 +100,10 @@ class Command: def prepare_args(self, args: typing.Sequence[str]) -> inspect.BoundArguments: try: bound_arguments = self.signature.bind(*args) - except TypeError as v: - raise exceptions.CommandError(f"Command argument mismatch: {v.args[0]}") + except TypeError: + expected = f'Expected: {str(self.signature.parameters)}' + received = f'Received: {str(args)}' + raise exceptions.CommandError(f"Command argument mismatch: \n\t{expected}\n\t{received}") for name, value in bound_arguments.arguments.items(): convert_to = self.signature.parameters[name].annotation diff --git a/mitmproxy/tools/_main.py b/mitmproxy/tools/_main.py index 0163e8d3..b98bbe90 100644 --- a/mitmproxy/tools/_main.py +++ b/mitmproxy/tools/_main.py @@ -83,6 +83,7 @@ def run( except SystemExit: arg_check.check() sys.exit(1) + try: opts.set(*args.setoptions, defer=True) optmanager.load_paths( diff --git a/mitmproxy/tools/console/commander/commander.py b/mitmproxy/tools/console/commander/commander.py index 02eda3bf..0f45aa1f 100644 --- a/mitmproxy/tools/console/commander/commander.py +++ b/mitmproxy/tools/console/commander/commander.py @@ -1,6 +1,4 @@ import abc -import collections -import copy import typing import urwid @@ -11,6 +9,8 @@ import mitmproxy.flow import mitmproxy.master import mitmproxy.types +from mitmproxy import command_lexer + class Completer: @abc.abstractmethod @@ -158,53 +158,15 @@ class CommandBuffer: self.completion = None -class CommandHistory: - def __init__(self, master: mitmproxy.master.Master, size: int = 30) -> None: - self.saved_commands: collections.deque = collections.deque( - [CommandBuffer(master, "")], - maxlen=size - ) - self.index: int = 0 - - @property - def last_index(self): - return len(self.saved_commands) - 1 - - def get_next(self) -> typing.Optional[CommandBuffer]: - if self.index < self.last_index: - self.index = self.index + 1 - return self.saved_commands[self.index] - return None - - def get_prev(self) -> typing.Optional[CommandBuffer]: - if self.index > 0: - self.index = self.index - 1 - return self.saved_commands[self.index] - return None - - def add_command(self, command: CommandBuffer, execution: bool = False) -> None: - if self.index == self.last_index or execution: - last_item = self.saved_commands[-1] - last_item_empty = not last_item.text - if last_item.text == command.text or (last_item_empty and execution): - self.saved_commands[-1] = copy.copy(command) - else: - self.saved_commands.append(command) - if not execution and self.index < self.last_index: - self.index += 1 - if execution: - self.index = self.last_index - - class CommandEdit(urwid.WidgetWrap): leader = ": " - def __init__(self, master: mitmproxy.master.Master, - text: str, history: CommandHistory) -> None: + def __init__(self, master: mitmproxy.master.Master, text: str) -> None: super().__init__(urwid.Text(self.leader)) self.master = master + self.active_filter = False + self.filter_str = '' self.cbuf = CommandBuffer(master, text) - self.history = history self.update() def keypress(self, size, key) -> None: @@ -236,15 +198,36 @@ class CommandEdit(urwid.WidgetWrap): self.cbuf.cursor = cursor_pos elif key == "backspace": self.cbuf.backspace() + if self.cbuf.text == '': + self.active_filter = False + self.master.commands.execute("command_history.filter ''") + self.filter_str = '' elif key == "left" or key == "ctrl b": self.cbuf.left() elif key == "right" or key == "ctrl f": self.cbuf.right() elif key == "up" or key == "ctrl p": - self.history.add_command(self.cbuf) - self.cbuf = self.history.get_prev() or self.cbuf + if self.active_filter is False: + self.active_filter = True + self.filter_str = self.cbuf.text + _cmd = command_lexer.quote(self.cbuf.text) + self.master.commands.execute("command_history.filter %s" % _cmd) + cmd = self.master.commands.execute("command_history.prev") + self.cbuf = CommandBuffer(self.master, cmd) elif key == "down" or key == "ctrl n": - self.cbuf = self.history.get_next() or self.cbuf + prev_cmd = self.cbuf.text + cmd = self.master.commands.execute("command_history.next") + + if cmd == '': + if prev_cmd == self.filter_str: + self.cbuf = CommandBuffer(self.master, prev_cmd) + else: + self.active_filter = False + self.master.commands.execute("command_history.filter ''") + self.filter_str = '' + self.cbuf = CommandBuffer(self.master, '') + else: + self.cbuf = CommandBuffer(self.master, cmd) elif key == "shift tab": self.cbuf.cycle_completion(False) elif key == "tab": diff --git a/mitmproxy/tools/console/statusbar.py b/mitmproxy/tools/console/statusbar.py index 43f5170d..39141b97 100644 --- a/mitmproxy/tools/console/statusbar.py +++ b/mitmproxy/tools/console/statusbar.py @@ -3,10 +3,11 @@ from typing import Optional import urwid +import mitmproxy.tools.console.master # noqa +from mitmproxy import command_lexer from mitmproxy.tools.console import common from mitmproxy.tools.console import signals from mitmproxy.tools.console import commandexecutor -import mitmproxy.tools.console.master # noqa from mitmproxy.tools.console.commander import commander @@ -43,8 +44,6 @@ class ActionBar(urwid.WidgetWrap): signals.status_prompt_onekey.connect(self.sig_prompt_onekey) signals.status_prompt_command.connect(self.sig_prompt_command) - self.command_history = commander.CommandHistory(master) - self.prompting = None self.onekey = False @@ -104,7 +103,6 @@ class ActionBar(urwid.WidgetWrap): self._w = commander.CommandEdit( self.master, partial, - self.command_history, ) if cursor is not None: self._w.cbuf.cursor = cursor @@ -134,7 +132,7 @@ class ActionBar(urwid.WidgetWrap): def keypress(self, size, k): if self.prompting: if k == "esc": - self.command_history.index = self.command_history.last_index + self.master.commands.execute('command_history.cancel') self.prompt_done() elif self.onekey: if k == "enter": @@ -142,7 +140,8 @@ class ActionBar(urwid.WidgetWrap): elif k in self.onekey: self.prompt_execute(k) elif k == "enter": - self.command_history.add_command(self._w.cbuf, True) + cmd = command_lexer.quote(self._w.cbuf.text) + self.master.commands.execute(f"command_history.add {cmd}") self.prompt_execute(self._w.get_edit_text()) else: if common.is_keypress(k): diff --git a/test/mitmproxy/addons/test_command_history.py b/test/mitmproxy/addons/test_command_history.py new file mode 100644 index 00000000..026ce53e --- /dev/null +++ b/test/mitmproxy/addons/test_command_history.py @@ -0,0 +1,306 @@ +import os +import pytest + +from mitmproxy import options +from mitmproxy.addons import command_history +from mitmproxy.test import taddons + + +@pytest.fixture(autouse=True) +def tctx(tmpdir): + # This runs before each test + dir_name = tmpdir.mkdir('mitmproxy').dirname + confdir = dir_name + + opts = options.Options() + opts.set(*[f"confdir={confdir}"]) + tctx = taddons.context(options=opts) + ch = command_history.CommandHistory() + tctx.master.addons.add(ch) + ch.configure([]) + + yield tctx + + # This runs after each test + ch.cleanup() + + +class TestCommandHistory: + def test_existing_command_history(self, tctx): + commands = ['cmd1', 'cmd2', 'cmd3'] + confdir = tctx.options.confdir + f = open(os.path.join(confdir, 'command_history'), 'w') + f.write("\n".join(commands)) + f.close() + + history = command_history.CommandHistory() + history.configure([]) + + saved_commands = [cmd for cmd in history.saved_commands] + assert saved_commands == ['cmd1', 'cmd2', 'cmd3'] + + history.cleanup() + + def test_add_command(self, tctx): + history = command_history.CommandHistory(3) + history.configure([]) + + history.add_command('cmd1') + history.add_command('cmd2') + + saved_commands = [cmd for cmd in history.saved_commands] + assert saved_commands == ['cmd1', 'cmd2'] + + history.add_command('') + saved_commands = [cmd for cmd in history.saved_commands] + assert saved_commands == ['cmd1', 'cmd2'] + + # The history size is only 3. So, we forget the first + # one command, when adding fourth command + history.add_command('cmd3') + history.add_command('cmd4') + saved_commands = [cmd for cmd in history.saved_commands] + assert saved_commands == ['cmd2', 'cmd3', 'cmd4'] + + history.add_command('') + saved_commands = [cmd for cmd in history.saved_commands] + assert saved_commands == ['cmd2', 'cmd3', 'cmd4'] + + # Commands with the same text are not repeated in the history one by one + history.add_command('cmd3') + saved_commands = [cmd for cmd in history.saved_commands] + assert saved_commands == ['cmd2', 'cmd4', 'cmd3'] + + history.add_command('cmd2') + saved_commands = [cmd for cmd in history.saved_commands] + assert saved_commands == ['cmd4', 'cmd3', 'cmd2'] + + history.cleanup() + + def test_get_next_and_prev(self, tctx): + history = command_history.CommandHistory(5) + history.configure([]) + + history.add_command('cmd1') + + assert history.get_next() == '' + assert history.get_next() == '' + assert history.get_prev() == 'cmd1' + assert history.get_prev() == 'cmd1' + assert history.get_prev() == 'cmd1' + assert history.get_next() == '' + assert history.get_next() == '' + + history.add_command('cmd2') + + assert history.get_next() == '' + assert history.get_next() == '' + assert history.get_prev() == 'cmd2' + assert history.get_prev() == 'cmd1' + assert history.get_prev() == 'cmd1' + assert history.get_next() == 'cmd2' + assert history.get_next() == '' + assert history.get_next() == '' + + history.add_command('cmd3') + + assert history.get_next() == '' + assert history.get_next() == '' + assert history.get_prev() == 'cmd3' + assert history.get_prev() == 'cmd2' + assert history.get_prev() == 'cmd1' + assert history.get_prev() == 'cmd1' + assert history.get_next() == 'cmd2' + assert history.get_next() == 'cmd3' + assert history.get_next() == '' + assert history.get_next() == '' + assert history.get_prev() == 'cmd3' + assert history.get_prev() == 'cmd2' + + history.add_command('cmd4') + + assert history.get_prev() == 'cmd4' + assert history.get_prev() == 'cmd3' + assert history.get_prev() == 'cmd2' + assert history.get_prev() == 'cmd1' + assert history.get_prev() == 'cmd1' + assert history.get_next() == 'cmd2' + assert history.get_next() == 'cmd3' + assert history.get_next() == 'cmd4' + assert history.get_next() == '' + assert history.get_next() == '' + + history.add_command('cmd5') + history.add_command('cmd6') + + assert history.get_next() == '' + assert history.get_prev() == 'cmd6' + assert history.get_prev() == 'cmd5' + assert history.get_prev() == 'cmd4' + assert history.get_next() == 'cmd5' + assert history.get_prev() == 'cmd4' + assert history.get_prev() == 'cmd3' + assert history.get_prev() == 'cmd2' + assert history.get_next() == 'cmd3' + assert history.get_prev() == 'cmd2' + assert history.get_prev() == 'cmd2' + assert history.get_next() == 'cmd3' + assert history.get_next() == 'cmd4' + assert history.get_next() == 'cmd5' + assert history.get_next() == 'cmd6' + assert history.get_next() == '' + assert history.get_next() == '' + + history.cleanup() + + def test_clear(self, tctx): + history = command_history.CommandHistory(3) + history.configure([]) + + history.add_command('cmd1') + history.add_command('cmd2') + history.clear_history() + + saved_commands = [cmd for cmd in history.saved_commands] + assert saved_commands == [] + + assert history.get_next() == '' + assert history.get_next() == '' + assert history.get_prev() == '' + assert history.get_prev() == '' + + history.cleanup() + + def test_filter(self, tctx): + history = command_history.CommandHistory(3) + history.configure([]) + + history.add_command('cmd1') + history.add_command('cmd2') + history.add_command('abc') + history.set_filter('c') + + assert history.get_next() == '' + assert history.get_next() == '' + assert history.get_prev() == 'c' + assert history.get_prev() == 'cmd2' + assert history.get_prev() == 'cmd1' + assert history.get_prev() == 'cmd1' + assert history.get_next() == 'cmd2' + assert history.get_next() == 'c' + assert history.get_next() == '' + assert history.get_next() == '' + + history.set_filter('') + + assert history.get_next() == '' + assert history.get_next() == '' + assert history.get_prev() == 'abc' + assert history.get_prev() == 'cmd2' + assert history.get_prev() == 'cmd1' + assert history.get_prev() == 'cmd1' + assert history.get_next() == 'cmd2' + assert history.get_next() == 'abc' + assert history.get_next() == '' + assert history.get_next() == '' + + history.cleanup() + + def test_multiple_instances(self, tctx): + instances = [ + command_history.CommandHistory(10), + command_history.CommandHistory(10), + command_history.CommandHistory(10) + ] + + for i in instances: + i.configure([]) + saved_commands = [cmd for cmd in i.saved_commands] + assert saved_commands == [] + + instances[0].add_command('cmd1') + saved_commands = [cmd for cmd in instances[0].saved_commands] + assert saved_commands == ['cmd1'] + + # These instances haven't yet added a new command, so they haven't + # yet reloaded their commands from the command file. + # This is expected, because if the user is filtering a command on + # another window, we don't want to interfere with that + saved_commands = [cmd for cmd in instances[1].saved_commands] + assert saved_commands == [] + saved_commands = [cmd for cmd in instances[2].saved_commands] + assert saved_commands == [] + + # Since the second instanced added a new command, its list of + # saved commands has been updated to have the commands from the + # first instance + its own commands + instances[1].add_command('cmd2') + saved_commands = [cmd for cmd in instances[1].saved_commands] + assert saved_commands == ['cmd1', 'cmd2'] + + saved_commands = [cmd for cmd in instances[0].saved_commands] + assert saved_commands == ['cmd1'] + + # Third instance is still empty as it has not yet ran any command + saved_commands = [cmd for cmd in instances[2].saved_commands] + assert saved_commands == [] + + instances[2].add_command('cmd3') + saved_commands = [cmd for cmd in instances[2].saved_commands] + assert saved_commands == ['cmd1', 'cmd2', 'cmd3'] + + instances[0].add_command('cmd4') + saved_commands = [cmd for cmd in instances[0].saved_commands] + assert saved_commands == ['cmd1', 'cmd2', 'cmd3', 'cmd4'] + + instances.append(command_history.CommandHistory(10)) + instances[3].configure([]) + saved_commands = [cmd for cmd in instances[3].saved_commands] + assert saved_commands == ['cmd1', 'cmd2', 'cmd3', 'cmd4'] + + instances[0].add_command('cmd_before_close') + instances.pop(0) + + saved_commands = [cmd for cmd in instances[0].saved_commands] + assert saved_commands == ['cmd1', 'cmd2'] + + instances[0].add_command('new_cmd') + saved_commands = [cmd for cmd in instances[0].saved_commands] + assert saved_commands == ['cmd1', 'cmd2', 'cmd3', 'cmd4', 'cmd_before_close', 'new_cmd'] + + instances.pop(0) + instances.pop(0) + instances.pop(0) + + _path = os.path.join(tctx.options.confdir, 'command_history') + lines = open(_path, 'r').readlines() + saved_commands = [cmd.strip() for cmd in lines] + assert saved_commands == ['cmd1', 'cmd2', 'cmd3', 'cmd4', 'cmd_before_close', 'new_cmd'] + + instances = [ + command_history.CommandHistory(10), + command_history.CommandHistory(10) + ] + + for i in instances: + i.configure([]) + i.clear_history() + saved_commands = [cmd for cmd in i.saved_commands] + assert saved_commands == [] + + instances[0].add_command('cmd1') + instances[0].add_command('cmd2') + instances[1].add_command('cmd3') + instances[1].add_command('cmd4') + instances[1].add_command('cmd5') + + saved_commands = [cmd for cmd in instances[1].saved_commands] + assert saved_commands == ['cmd1', 'cmd2', 'cmd3', 'cmd4', 'cmd5'] + + instances.pop() + instances.pop() + + _path = os.path.join(tctx.options.confdir, 'command_history') + lines = open(_path, 'r').readlines() + saved_commands = [cmd.strip() for cmd in lines] + assert saved_commands == ['cmd1', 'cmd2', 'cmd3', 'cmd4', 'cmd5'] diff --git a/test/mitmproxy/tools/console/test_commander.py b/test/mitmproxy/tools/console/test_commander.py index a77be043..4fa10eb8 100644 --- a/test/mitmproxy/tools/console/test_commander.py +++ b/test/mitmproxy/tools/console/test_commander.py @@ -1,9 +1,30 @@ import pytest +from mitmproxy import options +from mitmproxy.addons import command_history from mitmproxy.test import taddons from mitmproxy.tools.console.commander import commander +@pytest.fixture(autouse=True) +def tctx(tmpdir): + # This runs before each test + dir_name = tmpdir.mkdir('mitmproxy').dirname + confdir = dir_name + + opts = options.Options() + opts.set(*[f"confdir={confdir}"]) + tctx = taddons.context(options=opts) + ch = command_history.CommandHistory() + tctx.master.addons.add(ch) + ch.configure([]) + + yield tctx + + # This runs after each test + ch.cleanup() + + class TestListCompleter: def test_cycle(self): tests = [ @@ -23,178 +44,237 @@ class TestListCompleter: ["b", "ba", "bb", "b"] ], ] - for start, options, cycle in tests: - c = commander.ListCompleter(start, options) + for start, opts, cycle in tests: + c = commander.ListCompleter(start, opts) for expected in cycle: assert c.cycle() == expected class TestCommandEdit: - def test_open_command_bar(self): - with taddons.context() as tctx: - history = commander.CommandHistory(tctx.master, size=3) - edit = commander.CommandEdit(tctx.master, '', history) - try: - edit.update() - except IndexError: - pytest.faied("Unexpected IndexError") + def test_open_command_bar(self, tctx): + edit = commander.CommandEdit(tctx.master, '') - def test_insert(self): - with taddons.context() as tctx: - history = commander.CommandHistory(tctx.master, size=3) - edit = commander.CommandEdit(tctx.master, '', history) - edit.keypress(1, 'a') - assert edit.get_edit_text() == 'a' - - # Don't let users type a space before starting a command - # as a usability feature - history = commander.CommandHistory(tctx.master, size=3) - edit = commander.CommandEdit(tctx.master, '', history) - edit.keypress(1, ' ') - assert edit.get_edit_text() == '' + try: + edit.update() + except IndexError: + pytest.faied("Unexpected IndexError") - def test_backspace(self): - with taddons.context() as tctx: - history = commander.CommandHistory(tctx.master, size=3) - edit = commander.CommandEdit(tctx.master, '', history) - edit.keypress(1, 'a') - edit.keypress(1, 'b') - assert edit.get_edit_text() == 'ab' - edit.keypress(1, 'backspace') - assert edit.get_edit_text() == 'a' + def test_insert(self, tctx): + edit = commander.CommandEdit(tctx.master, '') + edit.keypress(1, 'a') + assert edit.get_edit_text() == 'a' - def test_left(self): - with taddons.context() as tctx: - history = commander.CommandHistory(tctx.master, size=3) - edit = commander.CommandEdit(tctx.master, '', history) - edit.keypress(1, 'a') - assert edit.cbuf.cursor == 1 - edit.keypress(1, 'left') - assert edit.cbuf.cursor == 0 + # Don't let users type a space before starting a command + # as a usability feature + edit = commander.CommandEdit(tctx.master, '') + edit.keypress(1, ' ') + assert edit.get_edit_text() == '' - # Do it again to make sure it won't go negative - edit.keypress(1, 'left') - assert edit.cbuf.cursor == 0 + def test_backspace(self, tctx): + edit = commander.CommandEdit(tctx.master, '') - def test_right(self): - with taddons.context() as tctx: - history = commander.CommandHistory(tctx.master, size=3) - edit = commander.CommandEdit(tctx.master, '', history) - edit.keypress(1, 'a') - assert edit.cbuf.cursor == 1 - - # Make sure cursor won't go past the text - edit.keypress(1, 'right') - assert edit.cbuf.cursor == 1 - - # Make sure cursor goes left and then back right - edit.keypress(1, 'left') - assert edit.cbuf.cursor == 0 - edit.keypress(1, 'right') - assert edit.cbuf.cursor == 1 - - def test_up_and_down(self): - with taddons.context() as tctx: - history = commander.CommandHistory(tctx.master, size=3) - edit = commander.CommandEdit(tctx.master, '', history) - - buf = commander.CommandBuffer(tctx.master, 'cmd1') - history.add_command(buf) - buf = commander.CommandBuffer(tctx.master, 'cmd2') - history.add_command(buf) - - edit.keypress(1, 'up') - assert edit.get_edit_text() == 'cmd2' - edit.keypress(1, 'up') - assert edit.get_edit_text() == 'cmd1' - edit.keypress(1, 'up') - assert edit.get_edit_text() == 'cmd1' - - history = commander.CommandHistory(tctx.master, size=5) - edit = commander.CommandEdit(tctx.master, '', history) - edit.keypress(1, 'a') - edit.keypress(1, 'b') - edit.keypress(1, 'c') - assert edit.get_edit_text() == 'abc' - edit.keypress(1, 'up') - assert edit.get_edit_text() == '' - edit.keypress(1, 'down') - assert edit.get_edit_text() == 'abc' - edit.keypress(1, 'down') - assert edit.get_edit_text() == 'abc' - - history = commander.CommandHistory(tctx.master, size=5) - edit = commander.CommandEdit(tctx.master, '', history) - buf = commander.CommandBuffer(tctx.master, 'cmd3') - history.add_command(buf) - edit.keypress(1, 'z') - edit.keypress(1, 'up') - assert edit.get_edit_text() == 'cmd3' - edit.keypress(1, 'down') - assert edit.get_edit_text() == 'z' - - -class TestCommandHistory: - def fill_history(self, commands): - with taddons.context() as tctx: - history = commander.CommandHistory(tctx.master, size=3) - for c in commands: - cbuf = commander.CommandBuffer(tctx.master, c) - history.add_command(cbuf) - return history, tctx.master - - def test_add_command(self): - commands = ["command1", "command2"] - history, tctx_master = self.fill_history(commands) - - saved_commands = [buf.text for buf in history.saved_commands] - assert saved_commands == [""] + commands - - # The history size is only 3. So, we forget the first - # one command, when adding fourth command - cbuf = commander.CommandBuffer(tctx_master, "command3") - history.add_command(cbuf) - saved_commands = [buf.text for buf in history.saved_commands] - assert saved_commands == commands + ["command3"] - - # Commands with the same text are not repeated in the history one by one - history.add_command(cbuf) - saved_commands = [buf.text for buf in history.saved_commands] - assert saved_commands == commands + ["command3"] - - # adding command in execution mode sets index at the beginning of the history - # and replace the last command buffer if it is empty or has the same text - cbuf = commander.CommandBuffer(tctx_master, "") - history.add_command(cbuf) - history.index = 0 - cbuf = commander.CommandBuffer(tctx_master, "command4") - history.add_command(cbuf, True) - assert history.index == history.last_index - saved_commands = [buf.text for buf in history.saved_commands] - assert saved_commands == ["command2", "command3", "command4"] - - def test_get_next(self): - commands = ["command1", "command2"] - history, tctx_master = self.fill_history(commands) - - history.index = -1 - expected_items = ["", "command1", "command2"] - for i in range(3): - assert history.get_next().text == expected_items[i] - # We are at the last item of the history - assert history.get_next() is None - - def test_get_prev(self): - commands = ["command1", "command2"] - history, tctx_master = self.fill_history(commands) - - expected_items = ["command2", "command1", ""] - history.index = history.last_index + 1 - for i in range(3): - assert history.get_prev().text == expected_items[i] - # We are at the first item of the history - assert history.get_prev() is None + edit.keypress(1, 'a') + edit.keypress(1, 'b') + assert edit.get_edit_text() == 'ab' + + edit.keypress(1, 'backspace') + assert edit.get_edit_text() == 'a' + + def test_left(self, tctx): + edit = commander.CommandEdit(tctx.master, '') + + edit.keypress(1, 'a') + assert edit.cbuf.cursor == 1 + + edit.keypress(1, 'left') + assert edit.cbuf.cursor == 0 + + # Do it again to make sure it won't go negative + edit.keypress(1, 'left') + assert edit.cbuf.cursor == 0 + + def test_right(self, tctx): + edit = commander.CommandEdit(tctx.master, '') + + edit.keypress(1, 'a') + assert edit.cbuf.cursor == 1 + + # Make sure cursor won't go past the text + edit.keypress(1, 'right') + assert edit.cbuf.cursor == 1 + + # Make sure cursor goes left and then back right + edit.keypress(1, 'left') + assert edit.cbuf.cursor == 0 + + edit.keypress(1, 'right') + assert edit.cbuf.cursor == 1 + + def test_up_and_down(self, tctx): + edit = commander.CommandEdit(tctx.master, '') + + tctx.master.commands.execute('command_history.clear') + tctx.master.commands.execute('command_history.add "cmd1"') + + edit.keypress(1, 'up') + assert edit.get_edit_text() == 'cmd1' + + edit.keypress(1, 'up') + assert edit.get_edit_text() == 'cmd1' + + edit.keypress(1, 'down') + assert edit.get_edit_text() == '' + + edit.keypress(1, 'down') + assert edit.get_edit_text() == '' + + edit = commander.CommandEdit(tctx.master, '') + + tctx.master.commands.execute('command_history.clear') + tctx.master.commands.execute('command_history.add "cmd1"') + tctx.master.commands.execute('command_history.add "cmd2"') + + edit.keypress(1, 'up') + assert edit.get_edit_text() == 'cmd2' + + edit.keypress(1, 'up') + assert edit.get_edit_text() == 'cmd1' + + edit.keypress(1, 'up') + assert edit.get_edit_text() == 'cmd1' + + edit.keypress(1, 'down') + assert edit.get_edit_text() == 'cmd2' + + edit.keypress(1, 'down') + assert edit.get_edit_text() == '' + + edit.keypress(1, 'a') + edit.keypress(1, 'b') + edit.keypress(1, 'c') + assert edit.get_edit_text() == 'abc' + + edit.keypress(1, 'up') + assert edit.get_edit_text() == 'abc' + + edit.keypress(1, 'down') + assert edit.get_edit_text() == 'abc' + + edit.keypress(1, 'down') + assert edit.get_edit_text() == 'abc' + + edit.keypress(1, 'up') + assert edit.get_edit_text() == 'abc' + + edit = commander.CommandEdit(tctx.master, '') + tctx.master.commands.execute('command_history.add "cmd3"') + + edit.keypress(1, 'z') + edit.keypress(1, 'up') + assert edit.get_edit_text() == 'z' + + edit.keypress(1, 'down') + assert edit.get_edit_text() == 'z' + + edit.keypress(1, 'down') + assert edit.get_edit_text() == 'z' + + edit.keypress(1, 'backspace') + assert edit.get_edit_text() == '' + + edit.keypress(1, 'up') + assert edit.get_edit_text() == 'cmd3' + + edit.keypress(1, 'up') + assert edit.get_edit_text() == 'cmd2' + + edit.keypress(1, 'up') + assert edit.get_edit_text() == 'cmd1' + + edit.keypress(1, 'down') + assert edit.get_edit_text() == 'cmd2' + + edit.keypress(1, 'down') + assert edit.get_edit_text() == 'cmd3' + + edit.keypress(1, 'down') + assert edit.get_edit_text() == '' + + edit.keypress(1, 'c') + assert edit.get_edit_text() == 'c' + + edit.keypress(1, 'down') + assert edit.get_edit_text() == '' + + edit.keypress(1, 'up') + assert edit.get_edit_text() == 'cmd3' + + edit.keypress(1, 'down') + assert edit.get_edit_text() == '' + + edit.keypress(1, 'up') + assert edit.get_edit_text() == 'cmd3' + + edit.keypress(1, 'up') + assert edit.get_edit_text() == 'cmd2' + + edit.keypress(1, 'down') + assert edit.get_edit_text() == 'cmd3' + + edit.keypress(1, 'down') + assert edit.get_edit_text() == '' + + edit.keypress(1, 'down') + assert edit.get_edit_text() == '' + + edit.keypress(1, 'up') + assert edit.get_edit_text() == 'cmd3' + + edit.keypress(1, 'up') + assert edit.get_edit_text() == 'cmd2' + + edit.keypress(1, 'up') + assert edit.get_edit_text() == 'cmd1' + + edit.keypress(1, 'up') + assert edit.get_edit_text() == 'cmd1' + + edit.keypress(1, 'up') + assert edit.get_edit_text() == 'cmd1' + + edit.keypress(1, 'down') + assert edit.get_edit_text() == 'cmd2' + + edit.keypress(1, 'down') + assert edit.get_edit_text() == 'cmd3' + + edit.keypress(1, 'down') + assert edit.get_edit_text() == '' + + edit.keypress(1, 'down') + assert edit.get_edit_text() == '' + + edit.keypress(1, 'backspace') + assert edit.get_edit_text() == '' + + edit.keypress(1, 'up') + assert edit.get_edit_text() == 'cmd3' + + edit.keypress(1, 'up') + assert edit.get_edit_text() == 'cmd2' + + edit.keypress(1, 'up') + assert edit.get_edit_text() == 'cmd1' + + edit.keypress(1, 'down') + assert edit.get_edit_text() == 'cmd2' + + edit.keypress(1, 'down') + assert edit.get_edit_text() == 'cmd3' + + edit.keypress(1, 'down') + assert edit.get_edit_text() == '' class TestCommandBuffer: @@ -255,8 +335,7 @@ class TestCommandBuffer: cb.cursor = len(cb.text) cb.cycle_completion() - ch = commander.CommandHistory(tctx.master, 30) - ce = commander.CommandEdit(tctx.master, "se", ch) + ce = commander.CommandEdit(tctx.master, "se") ce.keypress(1, 'tab') ce.update() ret = ce.cbuf.render() |