From bdb96becd66e0e06eac6eb0a0d78551b3cb7969d Mon Sep 17 00:00:00 2001 From: Daniel Karbach Date: Fri, 21 Oct 2016 15:53:58 +0200 Subject: [PATCH 01/15] unit tests for misc.lineparser --- qutebrowser/misc/lineparser.py | 6 +++ tests/unit/misc/test_lineparser.py | 84 ++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+) diff --git a/qutebrowser/misc/lineparser.py b/qutebrowser/misc/lineparser.py index b9300a392..4999c2e22 100644 --- a/qutebrowser/misc/lineparser.py +++ b/qutebrowser/misc/lineparser.py @@ -92,6 +92,12 @@ class BaseLineParser(QObject): Args: mode: The mode to use ('a'/'r'/'w') + + Raises: + IOError: if the file is already open + + Yields: + a file object for the config file """ assert self._configfile is not None if self._opened: diff --git a/tests/unit/misc/test_lineparser.py b/tests/unit/misc/test_lineparser.py index 7cf7a73b7..a37a5fe27 100644 --- a/tests/unit/misc/test_lineparser.py +++ b/tests/unit/misc/test_lineparser.py @@ -21,6 +21,7 @@ import os import pytest +from unittest import mock from qutebrowser.misc import lineparser as lineparsermod @@ -53,9 +54,49 @@ class TestBaseLineParser: lineparser._prepare_save() os_mock.makedirs.assert_called_with(self.CONFDIR, 0o755) + def test_prepare_save_no_config(self, mocker): + """Test if _prepare_save doesn't create a None config dir.""" + os_mock = mocker.patch('qutebrowser.misc.lineparser.os') + os_mock.path.exists.return_value = True + + lineparser = lineparsermod.BaseLineParser(None, self.FILENAME) + assert not lineparser._prepare_save() + assert not os_mock.makedirs.called + + def test_double_open(self, mocker, lineparser): + """Test if _open refuses reentry.""" + mocker.patch('qutebrowser.misc.lineparser.open', mock.mock_open()) + + with lineparser._open('r'): + with pytest.raises(IOError): + with lineparser._open('r'): + pass + + def test_binary(self, mocker): + """Test if _open and _write correctly handle binary files.""" + open_mock = mock.mock_open() + mocker.patch('qutebrowser.misc.lineparser.open', open_mock) + + testdata = b'\xf0\xff' + + lineparser = lineparsermod.BaseLineParser( + self.CONFDIR, self.FILENAME, binary=True) + with lineparser._open('r') as f: + lineparser._write(f, [testdata]) + + open_mock.assert_called_once_with( + os.path.join(self.CONFDIR, self.FILENAME), 'rb') + + open_mock().write.assert_has_calls([ + mock.call(testdata), + mock.call(b'\n') + ]) + class TestLineParser: + """Tests for LineParser.""" + @pytest.fixture def lineparser(self, tmpdir): """Fixture to get a LineParser for tests.""" @@ -63,7 +104,18 @@ class TestLineParser: lp.save() return lp + def test_init(self, tmpdir): + """Test if creating a line parser correctly reads its file.""" + (tmpdir / 'file').write('one\ntwo\n') + lineparser = lineparsermod.LineParser(str(tmpdir), 'file') + assert lineparser.data == ['one', 'two'] + + (tmpdir / 'file').write_binary(b'\xfe\n\xff\n') + lineparser = lineparsermod.LineParser(str(tmpdir), 'file', binary=True) + assert lineparser.data == [b'\xfe', b'\xff'] + def test_clear(self, tmpdir, lineparser): + """Test if clear() empties its file.""" lineparser.data = ['one', 'two'] lineparser.save() assert (tmpdir / 'file').read() == 'one\ntwo\n' @@ -71,6 +123,20 @@ class TestLineParser: assert not lineparser.data assert (tmpdir / 'file').read() == '' + def test_double_open(self, lineparser): + """Test if save() bails on an already open file.""" + with lineparser._open('r'): + with pytest.raises(IOError): + lineparser.save() + + def test_prepare_save(self, tmpdir, lineparser): + """Test if save() bails when _prepare_save() returns False.""" + (tmpdir / 'file').write('pristine\n') + lineparser.data = ['changed'] + lineparser._prepare_save = lambda: False + lineparser.save() + assert (tmpdir / 'file').read() == 'pristine\n' + class TestAppendLineParser: @@ -97,7 +163,17 @@ class TestAppendLineParser: lineparser.save() assert (tmpdir / 'file').read() == self._get_expected(new_data) + def test_save_without_configdir(self, tmpdir, lineparser): + """Test save() failing because no configdir was set.""" + new_data = ['new data 1', 'new data 2'] + lineparser.new_data = new_data + lineparser._configdir = None + assert not lineparser.save() + # make sure new data is still there + assert lineparser.new_data == new_data + def test_clear(self, tmpdir, lineparser): + """Check if calling clear() empties both pending and persisted data.""" lineparser.new_data = ['one', 'two'] lineparser.save() assert (tmpdir / 'file').read() == "old data 1\nold data 2\none\ntwo\n" @@ -108,6 +184,14 @@ class TestAppendLineParser: assert not lineparser.new_data assert (tmpdir / 'file').read() == "" + def test_clear_without_configdir(self, tmpdir, lineparser): + """Test clear() failing because no configdir was set.""" + new_data = ['new data 1', 'new data 2'] + lineparser.new_data = new_data + lineparser._configdir = None + assert not lineparser.clear() + assert lineparser.new_data == new_data + def test_iter_without_open(self, lineparser): """Test __iter__ without having called open().""" with pytest.raises(ValueError): From 5d92934d765ca57a96ea5f01ad7f3ffd43070731 Mon Sep 17 00:00:00 2001 From: Daniel Karbach Date: Mon, 24 Oct 2016 12:21:52 +0200 Subject: [PATCH 02/15] less brutal synthetic segfault --- qutebrowser/misc/utilcmds.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/qutebrowser/misc/utilcmds.py b/qutebrowser/misc/utilcmds.py index 62202998c..115348a9f 100644 --- a/qutebrowser/misc/utilcmds.py +++ b/qutebrowser/misc/utilcmds.py @@ -20,7 +20,8 @@ """Misc. utility commands exposed to the user.""" import functools -import types +import os +import signal import traceback try: @@ -142,10 +143,7 @@ def debug_crash(typ='exception'): typ: either 'exception' or 'segfault'. """ if typ == 'segfault': - # From python's Lib/test/crashers/bogus_code_obj.py - co = types.CodeType(0, 0, 0, 0, 0, b'\x04\x71\x00\x00', (), (), (), - '', '', 1, b'') - exec(co) + os.kill(os.getpid(), signal.SIGSEGV) raise Exception("Segfault failed (wat.)") else: raise Exception("Forced crash") From c801caa19ad64d8565f8fbf8097df1d1ac07197a Mon Sep 17 00:00:00 2001 From: Daniel Karbach Date: Mon, 24 Oct 2016 15:51:16 +0200 Subject: [PATCH 03/15] log debug console status changes --- qutebrowser/misc/utilcmds.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/qutebrowser/misc/utilcmds.py b/qutebrowser/misc/utilcmds.py index 115348a9f..affcb6f1d 100644 --- a/qutebrowser/misc/utilcmds.py +++ b/qutebrowser/misc/utilcmds.py @@ -171,12 +171,15 @@ def debug_console(): try: con_widget = objreg.get('debug-console') except KeyError: + log.misc.debug('initializing debug console') con_widget = consolewidget.ConsoleWidget() objreg.register('debug-console', con_widget) if con_widget.isVisible(): + log.misc.debug('hiding debug console') con_widget.hide() else: + log.misc.debug('showing debug console') con_widget.show() From 863bab3ccfbd4f52dfd35ce03208dc10e6816c8b Mon Sep 17 00:00:00 2001 From: Daniel Karbach Date: Mon, 24 Oct 2016 15:51:55 +0200 Subject: [PATCH 04/15] allow multiline matches in log messages --- tests/helpers/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/helpers/utils.py b/tests/helpers/utils.py index c8cf820cd..faafefa82 100644 --- a/tests/helpers/utils.py +++ b/tests/helpers/utils.py @@ -157,7 +157,7 @@ def pattern_match(*, pattern, value): True on a match, False otherwise. """ re_pattern = '.*'.join(re.escape(part) for part in pattern.split('*')) - return re.fullmatch(re_pattern, value) is not None + return re.fullmatch(re_pattern, value, flags=re.DOTALL) is not None def abs_datapath(): From e0d1fafe437fd48257f061d4f4cd36a177c3fb62 Mon Sep 17 00:00:00 2001 From: Daniel Karbach Date: Mon, 24 Oct 2016 15:53:21 +0200 Subject: [PATCH 05/15] tests for misc.utilcmds --- tests/end2end/features/test_utilcmds_bdd.py | 22 +++ tests/end2end/features/utilcmds.feature | 70 ++++++++++ tests/unit/misc/test_utilcmds.py | 145 ++++++++++++++++++++ 3 files changed, 237 insertions(+) create mode 100644 tests/end2end/features/test_utilcmds_bdd.py create mode 100644 tests/end2end/features/utilcmds.feature create mode 100644 tests/unit/misc/test_utilcmds.py diff --git a/tests/end2end/features/test_utilcmds_bdd.py b/tests/end2end/features/test_utilcmds_bdd.py new file mode 100644 index 000000000..248e915b6 --- /dev/null +++ b/tests/end2end/features/test_utilcmds_bdd.py @@ -0,0 +1,22 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2015-2016 Florian Bruhin (The Compiler) +# +# This file is part of qutebrowser. +# +# qutebrowser is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# qutebrowser is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with qutebrowser. If not, see . + +import pytest_bdd as bdd + +bdd.scenarios('utilcmds.feature') diff --git a/tests/end2end/features/utilcmds.feature b/tests/end2end/features/utilcmds.feature new file mode 100644 index 000000000..af7ab553e --- /dev/null +++ b/tests/end2end/features/utilcmds.feature @@ -0,0 +1,70 @@ +Feature: Miscellaneous utility commands exposed to the user. + + Background: + Given I open data/scroll/simple.html + And I run :tab-only + + ## :later + + Scenario: :later before + When I run :later 500 scroll down + Then the page should not be scrolled + + Scenario: :later after + When I run :later 500 scroll down + And I wait 0.6s + Then the page should be scrolled vertically + + # for some reason, argparser gives us the error instead, see #2046 + @xfail + Scenario: :later with negative delay + When I run :later -1 scroll down + Then the error "I can't run something in the past!" should be shown + + Scenario: :later with humongous delay + When I run :later 36893488147419103232 scroll down + Then the error "Numeric argument is too large for internal int representation." should be shown + + ## :repeat + + Scenario: :repeat simple + When I run :repeat 5 scroll-px 10 0 + And I wait until the scroll position changed to 50/0 + # Then already covered by above And + + Scenario: :repeat zero times + When I run :repeat 0 scroll-px 10 0 + And I wait 0.01s + Then the page should not be scrolled + + # argparser again + @xfail + Scenario: :repeat negative times + When I run :repeat -4 scroll-px 10 0 + Then the error "A negative count doesn't make sense." should be shown + And the page should not be scrolled + + ## :debug-all-objects + + Scenario: :debug-all-objects + When I run :debug-all-objects + Then "*Qt widgets - *Qt objects - *" should be logged + + ## :debug-cache-stats + + Scenario: :debug-cache-stats + When I run :debug-cache-stats + Then "config: CacheInfo(*)" should be logged + And "style: CacheInfo(*)" should be logged + + ## :debug-console + + # (!) the following two scenarios have a sequential dependency + Scenario: opening the debug console + When I run :debug-console + Then "initializing debug console" should be logged + And "showing debug console" should be logged + + Scenario: closing the debug console + When I run :debug-console + Then "hiding debug console" should be logged diff --git a/tests/unit/misc/test_utilcmds.py b/tests/unit/misc/test_utilcmds.py new file mode 100644 index 000000000..9477e8f68 --- /dev/null +++ b/tests/unit/misc/test_utilcmds.py @@ -0,0 +1,145 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2015-2016 Florian Bruhin (The Compiler) +# +# This file is part of qutebrowser. +# +# qutebrowser is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# qutebrowser is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with qutebrowser. If not, see . + +"""Tests for qutebrowser.misc.utilcmds.""" + +import contextlib +import logging +import pytest +import signal + +_hunter_available = False +try: + import hunter # pylint: disable=unused-import + _hunter_available = True +except ImportError: + pass + +from qutebrowser.misc import utilcmds as utilcmdsmod + +from qutebrowser.commands import cmdexc + + +@contextlib.contextmanager +def _trapped_segv(handler): + """Temporarily install given signal handler for SIGSEGV.""" + old_handler = signal.signal(signal.SIGSEGV, handler) + yield + signal.signal(signal.SIGSEGV, old_handler) + + +def test_debug_crash(): + """Verify that debug_crash crashes as intended.""" + with pytest.raises(Exception): + utilcmdsmod.debug_crash(typ='exception') + + caught = False + + def _handler(num, frame): + """Temporary handler for segfault.""" + nonlocal caught + caught = num == signal.SIGSEGV + + with _trapped_segv(_handler): + # since we handle the segfault, execution will continue and run into + # the "Segfault failed (wat.)" Exception + with pytest.raises(Exception) as excinfo: + utilcmdsmod.debug_crash(typ='segfault') + assert caught + assert 'Segfault failed' in str(excinfo.value) + + +@pytest.mark.skipif(not _hunter_available, reason="hunter not available") +def test_debug_trace(mocker): + """Check if hunter.trace is properly called.""" + hunter_mock = mocker.patch('qutebrowser.misc.utilcmds.hunter') + utilcmdsmod.debug_trace(1) + assert hunter_mock.trace.assert_called_with(1) + + hunter_mock.trace.side_effect = Exception + with pytest.raises(Exception): + utilcmdsmod.debug_trace() + + +def test_debug_trace_no_hunter(monkeypatch): + """Test that an error is shown if debug_trace is called without hunter.""" + monkeypatch.setattr(utilcmdsmod, 'hunter', None) + with pytest.raises(cmdexc.CommandError): + utilcmdsmod.debug_trace() + + +class FakeModeMan: + + """ModeManager mock class for :repeat-command test. + + Attributes: + mode: False + """ + + def __init__(self): + self.mode = False + + +def test_repeat_command_initial(mocker): + """Test repeat_command first-time behaviour. + + If :repeat-command is called initially, it should err, because there's + nothing to repeat. + """ + objreg_mock = mocker.patch('qutebrowser.misc.utilcmds.objreg') + objreg_mock.get.return_value = FakeModeMan() + with pytest.raises(cmdexc.CommandError): + utilcmdsmod.repeat_command(win_id=0) + + +def test_debug_log_level(mocker): + """Test interactive log level changing.""" + formatter_mock = mocker.patch( + 'qutebrowser.misc.utilcmds.log.change_console_formatter') + handler_mock = mocker.patch( + 'qutebrowser.misc.utilcmds.log.console_handler') + utilcmdsmod.debug_log_level(level='debug') + formatter_mock.assert_called_with(logging.DEBUG) + handler_mock.setLevel.assert_called_with(logging.DEBUG) + + +class FakeWindow: + + """Mock class for window_only.""" + + def __init__(self, deleted=False): + self.closed = False + self.deleted = deleted + + def close(self): + """Flag as closed.""" + self.closed = True + + +def test_window_only(mocker, monkeypatch): + """Verify that window_only doesn't close the current or deleted windows.""" + test_windows = {0: FakeWindow(), 1: FakeWindow(True), 2: FakeWindow()} + winreg_mock = mocker.patch('qutebrowser.misc.utilcmds.objreg') + winreg_mock.window_registry = test_windows + sip_mock = mocker.patch('qutebrowser.misc.utilcmds.sip') + sip_mock.isdeleted.side_effect = lambda window: window.deleted + utilcmdsmod.window_only(current_win_id=0) + assert not test_windows[0].closed + assert not test_windows[1].closed + assert test_windows[2].closed From b5ffe979aa0eb9a74b621f2094d08e20af6ce5fb Mon Sep 17 00:00:00 2001 From: Daniel Karbach Date: Tue, 25 Oct 2016 08:48:04 +0200 Subject: [PATCH 06/15] "typo" in utilcmds test --- tests/unit/misc/test_utilcmds.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/misc/test_utilcmds.py b/tests/unit/misc/test_utilcmds.py index 9477e8f68..7ec534256 100644 --- a/tests/unit/misc/test_utilcmds.py +++ b/tests/unit/misc/test_utilcmds.py @@ -97,7 +97,7 @@ class FakeModeMan: def test_repeat_command_initial(mocker): - """Test repeat_command first-time behaviour. + """Test repeat_command first-time behavior. If :repeat-command is called initially, it should err, because there's nothing to repeat. From 9038b28ea4a9bc901cfac0fbfa2b5be87fb4dbee Mon Sep 17 00:00:00 2001 From: Daniel Karbach Date: Tue, 25 Oct 2016 08:52:08 +0200 Subject: [PATCH 07/15] different mocking of open() in lineparser test apparently, python 3.4 (and less, probably) does not import builtins into modules --- tests/unit/misc/test_lineparser.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/misc/test_lineparser.py b/tests/unit/misc/test_lineparser.py index a37a5fe27..adb5d1e82 100644 --- a/tests/unit/misc/test_lineparser.py +++ b/tests/unit/misc/test_lineparser.py @@ -65,7 +65,7 @@ class TestBaseLineParser: def test_double_open(self, mocker, lineparser): """Test if _open refuses reentry.""" - mocker.patch('qutebrowser.misc.lineparser.open', mock.mock_open()) + mocker.patch('builtins.open', mock.mock_open()) with lineparser._open('r'): with pytest.raises(IOError): @@ -75,7 +75,7 @@ class TestBaseLineParser: def test_binary(self, mocker): """Test if _open and _write correctly handle binary files.""" open_mock = mock.mock_open() - mocker.patch('qutebrowser.misc.lineparser.open', open_mock) + mocker.patch('builtins.open', open_mock) testdata = b'\xf0\xff' From 442549555b437e8ced6064dfd1e0db26322eafc3 Mon Sep 17 00:00:00 2001 From: Daniel Karbach Date: Tue, 25 Oct 2016 17:24:14 +0200 Subject: [PATCH 08/15] skip segfault test on windows while technically possible (on both machine and OS level), termination due to SIGSEGV cannot be prevented maybe the test could be rewritten to spawn a subprocess and check its exit status (of 11) --- tests/unit/misc/test_utilcmds.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/unit/misc/test_utilcmds.py b/tests/unit/misc/test_utilcmds.py index 7ec534256..b403abe01 100644 --- a/tests/unit/misc/test_utilcmds.py +++ b/tests/unit/misc/test_utilcmds.py @@ -21,8 +21,10 @@ import contextlib import logging +import os import pytest import signal +import time _hunter_available = False try: @@ -44,11 +46,15 @@ def _trapped_segv(handler): signal.signal(signal.SIGSEGV, old_handler) -def test_debug_crash(): +def test_debug_crash_exception(): """Verify that debug_crash crashes as intended.""" with pytest.raises(Exception): utilcmdsmod.debug_crash(typ='exception') +@pytest.mark.skipif(os.name == 'nt', + reason="current CPython/win can't recover from SIGSEGV") +def test_debug_crash_segfault(): + """Verify that debug_crash crashes as intended.""" caught = False def _handler(num, frame): @@ -61,6 +67,7 @@ def test_debug_crash(): # the "Segfault failed (wat.)" Exception with pytest.raises(Exception) as excinfo: utilcmdsmod.debug_crash(typ='segfault') + time.sleep(0.001) assert caught assert 'Segfault failed' in str(excinfo.value) From e1c467b3a0ed08370302e9fa277e0abb4583b899 Mon Sep 17 00:00:00 2001 From: Daniel Karbach Date: Thu, 27 Oct 2016 13:52:09 +0200 Subject: [PATCH 09/15] move utilcmds specific tests from misc --- tests/end2end/features/misc.feature | 104 -------------------- tests/end2end/features/utilcmds.feature | 125 +++++++++++++++++++++--- 2 files changed, 110 insertions(+), 119 deletions(-) diff --git a/tests/end2end/features/misc.feature b/tests/end2end/features/misc.feature index 701d9d7cf..50a436680 100644 --- a/tests/end2end/features/misc.feature +++ b/tests/end2end/features/misc.feature @@ -45,20 +45,6 @@ Feature: Various utility commands. When I run :set-cmd-text foo Then the error "Invalid command text 'foo'." should be shown - ## :message-* - - Scenario: :message-error - When I run :message-error "Hello World" - Then the error "Hello World" should be shown - - Scenario: :message-info - When I run :message-info "Hello World" - Then the message "Hello World" should be shown - - Scenario: :message-warning - When I run :message-warning "Hello World" - Then the warning "Hello World" should be shown - ## :jseval Scenario: :jseval @@ -243,16 +229,6 @@ Feature: Various utility commands. And I run :view-source Then the error "Already viewing source!" should be shown - # :debug-console - - @no_xvfb - Scenario: :debug-console smoke test - When I run :debug-console - And I wait for "Focus object changed: " in the log - And I run :debug-console - And I wait for "Focus object changed: *" in the log - Then no crash should happen - # :help Scenario: :help without topic @@ -496,31 +472,6 @@ Feature: Various utility commands. Then qute://log?level=error should be loaded And the page should contain the plaintext "No messages to show." - Scenario: Using :debug-log-capacity - When I run :debug-log-capacity 100 - And I run :message-info oldstuff - And I run :repeat 20 message-info otherstuff - And I run :message-info newstuff - And I open qute:log - Then the page should contain the plaintext "newstuff" - And the page should not contain the plaintext "oldstuff" - - Scenario: Using :debug-log-capacity with negative capacity - When I run :debug-log-capacity -1 - Then the error "Can't set a negative log capacity!" should be shown - - # :debug-log-level / :debug-log-filter - # Other :debug-log-{level,filter} features are tested in - # unit/utils/test_log.py as using them would break end2end tests. - - Scenario: Using debug-log-level with invalid level - When I run :debug-log-level hello - Then the error "level: Invalid value hello - expected one of: vdebug, debug, info, warning, error, critical" should be shown - - Scenario: Using debug-log-filter with invalid filter - When I run :debug-log-filter blah - Then the error "filters: Invalid value blah - expected one of: statusbar, *" should be shown - ## https://github.com/The-Compiler/qutebrowser/issues/1523 Scenario: Completing a single option argument @@ -561,51 +512,6 @@ Feature: Various utility commands. And I set general -> private-browsing to false Then the page should contain the plaintext "Local storage status: not working" - Scenario: :repeat-command - Given I open data/scroll/simple.html - And I run :tab-only - When I run :scroll down - And I run :repeat-command - And I run :scroll up - Then the page should be scrolled vertically - - Scenario: :repeat-command with count - Given I open data/scroll/simple.html - And I run :tab-only - When I run :scroll down with count 3 - And I wait until the scroll position changed - And I run :scroll up - And I wait until the scroll position changed - And I run :repeat-command with count 2 - And I wait until the scroll position changed to 0/0 - Then the page should not be scrolled - - Scenario: :repeat-command with not-normal command inbetween - Given I open data/scroll/simple.html - And I run :tab-only - When I run :scroll down with count 3 - And I wait until the scroll position changed - And I run :scroll up - And I wait until the scroll position changed - And I run :prompt-accept - And I run :repeat-command with count 2 - And I wait until the scroll position changed to 0/0 - Then the page should not be scrolled - And the error "prompt-accept: This command is only allowed in prompt/yesno mode, not normal." should be shown - - @qtwebengine_createWindow - Scenario: :repeat-command with mode-switching command - Given I open data/hints/link_blank.html - And I run :tab-only - When I hint with args "all" - And I run :leave-mode - And I run :repeat-command - And I run :follow-hint a - And I wait until data/hello.txt is loaded - Then the following tabs should be open: - - data/hints/link_blank.html - - data/hello.txt (active) - Scenario: Using 0 as count When I run :scroll down with count 0 Then the error "scroll: A zero count is not allowed for this command!" should be shown @@ -723,13 +629,3 @@ Feature: Various utility commands. And I run :command-accept And I set general -> private-browsing to false Then the message "blah" should be shown - - ## :run-with-count - - Scenario: :run-with-count - When I run :run-with-count 2 scroll down - Then "command called: scroll ['down'] (count=2)" should be logged - - Scenario: :run-with-count with count - When I run :run-with-count 2 scroll down with count 3 - Then "command called: scroll ['down'] (count=6)" should be logged diff --git a/tests/end2end/features/utilcmds.feature b/tests/end2end/features/utilcmds.feature index af7ab553e..5f58ced3c 100644 --- a/tests/end2end/features/utilcmds.feature +++ b/tests/end2end/features/utilcmds.feature @@ -9,6 +9,8 @@ Feature: Miscellaneous utility commands exposed to the user. Scenario: :later before When I run :later 500 scroll down Then the page should not be scrolled + # wait for scroll to execture so we don't ruin our future + And the page should be scrolled vertically Scenario: :later after When I run :later 500 scroll down @@ -30,13 +32,37 @@ Feature: Miscellaneous utility commands exposed to the user. Scenario: :repeat simple When I run :repeat 5 scroll-px 10 0 And I wait until the scroll position changed to 50/0 - # Then already covered by above And + # Then already covered by above And Scenario: :repeat zero times When I run :repeat 0 scroll-px 10 0 And I wait 0.01s Then the page should not be scrolled + ## :run-with-count + + Scenario: :run-with-count + When I run :run-with-count 2 scroll down + Then "command called: scroll ['down'] (count=2)" should be logged + + Scenario: :run-with-count with count + When I run :run-with-count 2 scroll down with count 3 + Then "command called: scroll ['down'] (count=6)" should be logged + + ## :message-* + + Scenario: :message-error + When I run :message-error "Hello World" + Then the error "Hello World" should be shown + + Scenario: :message-info + When I run :message-info "Hello World" + Then the message "Hello World" should be shown + + Scenario: :message-warning + When I run :message-warning "Hello World" + Then the warning "Hello World" should be shown + # argparser again @xfail Scenario: :repeat negative times @@ -50,21 +76,90 @@ Feature: Miscellaneous utility commands exposed to the user. When I run :debug-all-objects Then "*Qt widgets - *Qt objects - *" should be logged - ## :debug-cache-stats + ## :debug-cache-stats - Scenario: :debug-cache-stats - When I run :debug-cache-stats - Then "config: CacheInfo(*)" should be logged - And "style: CacheInfo(*)" should be logged + Scenario: :debug-cache-stats + When I run :debug-cache-stats + Then "config: CacheInfo(*)" should be logged + And "style: CacheInfo(*)" should be logged - ## :debug-console + ## :debug-console - # (!) the following two scenarios have a sequential dependency - Scenario: opening the debug console - When I run :debug-console - Then "initializing debug console" should be logged - And "showing debug console" should be logged + @no_xvfb + Scenario: :debug-console smoke test + When I run :debug-console + And I wait for "Focus object changed: " in the log + And I run :debug-console + And I wait for "Focus object changed: *" in the log + Then "initializing debug console" should be logged + And "showing debug console" should be logged + And "hiding debug console" should be logged + And no crash should happen - Scenario: closing the debug console - When I run :debug-console - Then "hiding debug console" should be logged + ## :repeat-command + + Scenario: :repeat-command + When I run :scroll down + And I run :repeat-command + And I run :scroll up + Then the page should be scrolled vertically + + Scenario: :repeat-command with count + When I run :scroll down with count 3 + And I wait until the scroll position changed + And I run :scroll up + And I wait until the scroll position changed + And I run :repeat-command with count 2 + And I wait until the scroll position changed to 0/0 + Then the page should not be scrolled + + Scenario: :repeat-command with not-normal command inbetween + When I run :scroll down with count 3 + And I wait until the scroll position changed + And I run :scroll up + And I wait until the scroll position changed + And I run :prompt-accept + And I run :repeat-command with count 2 + And I wait until the scroll position changed to 0/0 + Then the page should not be scrolled + And the error "prompt-accept: This command is only allowed in prompt/yesno mode, not normal." should be shown + + @qtwebengine_createWindow + Scenario: :repeat-command with mode-switching command + When I open data/hints/link_blank.html + And I run :tab-only + And I hint with args "all" + And I run :leave-mode + And I run :repeat-command + And I run :follow-hint a + And I wait until data/hello.txt is loaded + Then the following tabs should be open: + - data/hints/link_blank.html + - data/hello.txt (active) + + ## :debug-log-capacity + + Scenario: Using :debug-log-capacity + When I run :debug-log-capacity 100 + And I run :message-info oldstuff + And I run :repeat 20 message-info otherstuff + And I run :message-info newstuff + And I open qute:log + Then the page should contain the plaintext "newstuff" + And the page should not contain the plaintext "oldstuff" + + Scenario: Using :debug-log-capacity with negative capacity + When I run :debug-log-capacity -1 + Then the error "Can't set a negative log capacity!" should be shown + + ## :debug-log-level / :debug-log-filter + # Other :debug-log-{level,filter} features are tested in + # unit/utils/test_log.py as using them would break end2end tests. + + Scenario: Using debug-log-level with invalid level + When I run :debug-log-level hello + Then the error "level: Invalid value hello - expected one of: vdebug, debug, info, warning, error, critical" should be shown + + Scenario: Using debug-log-filter with invalid filter + When I run :debug-log-filter blah + Then the error "filters: Invalid value blah - expected one of: statusbar, *" should be shown From 64cf8fcd39d1efef439fb355f00b51e88f5422f4 Mon Sep 17 00:00:00 2001 From: Daniel Karbach Date: Thu, 27 Oct 2016 13:52:42 +0200 Subject: [PATCH 10/15] lineparser/utilcmds test cleanup * fix date in copyright * remove redundant class docstrings * don't rename utilcmds module in unit test * use `mode_manager` fixture in place of FakeModeMan * some whitespace --- tests/end2end/features/test_utilcmds_bdd.py | 2 +- tests/unit/misc/test_lineparser.py | 6 ---- tests/unit/misc/test_utilcmds.py | 39 ++++++++------------- 3 files changed, 15 insertions(+), 32 deletions(-) diff --git a/tests/end2end/features/test_utilcmds_bdd.py b/tests/end2end/features/test_utilcmds_bdd.py index 248e915b6..f90d587f6 100644 --- a/tests/end2end/features/test_utilcmds_bdd.py +++ b/tests/end2end/features/test_utilcmds_bdd.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2016 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/unit/misc/test_lineparser.py b/tests/unit/misc/test_lineparser.py index adb5d1e82..7b5710ece 100644 --- a/tests/unit/misc/test_lineparser.py +++ b/tests/unit/misc/test_lineparser.py @@ -28,8 +28,6 @@ from qutebrowser.misc import lineparser as lineparsermod class TestBaseLineParser: - """Tests for BaseLineParser.""" - CONFDIR = "this really doesn't matter" FILENAME = "and neither does this" @@ -95,8 +93,6 @@ class TestBaseLineParser: class TestLineParser: - """Tests for LineParser.""" - @pytest.fixture def lineparser(self, tmpdir): """Fixture to get a LineParser for tests.""" @@ -140,8 +136,6 @@ class TestLineParser: class TestAppendLineParser: - """Tests for AppendLineParser.""" - BASE_DATA = ['old data 1', 'old data 2'] @pytest.fixture diff --git a/tests/unit/misc/test_utilcmds.py b/tests/unit/misc/test_utilcmds.py index b403abe01..cbfb88f9c 100644 --- a/tests/unit/misc/test_utilcmds.py +++ b/tests/unit/misc/test_utilcmds.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2016 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -33,7 +33,7 @@ try: except ImportError: pass -from qutebrowser.misc import utilcmds as utilcmdsmod +from qutebrowser.misc import utilcmds from qutebrowser.commands import cmdexc @@ -49,7 +49,8 @@ def _trapped_segv(handler): def test_debug_crash_exception(): """Verify that debug_crash crashes as intended.""" with pytest.raises(Exception): - utilcmdsmod.debug_crash(typ='exception') + utilcmds.debug_crash(typ='exception') + @pytest.mark.skipif(os.name == 'nt', reason="current CPython/win can't recover from SIGSEGV") @@ -66,7 +67,7 @@ def test_debug_crash_segfault(): # since we handle the segfault, execution will continue and run into # the "Segfault failed (wat.)" Exception with pytest.raises(Exception) as excinfo: - utilcmdsmod.debug_crash(typ='segfault') + utilcmds.debug_crash(typ='segfault') time.sleep(0.001) assert caught assert 'Segfault failed' in str(excinfo.value) @@ -76,43 +77,31 @@ def test_debug_crash_segfault(): def test_debug_trace(mocker): """Check if hunter.trace is properly called.""" hunter_mock = mocker.patch('qutebrowser.misc.utilcmds.hunter') - utilcmdsmod.debug_trace(1) + utilcmds.debug_trace(1) assert hunter_mock.trace.assert_called_with(1) hunter_mock.trace.side_effect = Exception with pytest.raises(Exception): - utilcmdsmod.debug_trace() + utilcmds.debug_trace() def test_debug_trace_no_hunter(monkeypatch): """Test that an error is shown if debug_trace is called without hunter.""" - monkeypatch.setattr(utilcmdsmod, 'hunter', None) + monkeypatch.setattr(utilcmds, 'hunter', None) with pytest.raises(cmdexc.CommandError): - utilcmdsmod.debug_trace() + utilcmds.debug_trace() -class FakeModeMan: - - """ModeManager mock class for :repeat-command test. - - Attributes: - mode: False - """ - - def __init__(self): - self.mode = False - - -def test_repeat_command_initial(mocker): +def test_repeat_command_initial(mocker, mode_manager): """Test repeat_command first-time behavior. If :repeat-command is called initially, it should err, because there's nothing to repeat. """ objreg_mock = mocker.patch('qutebrowser.misc.utilcmds.objreg') - objreg_mock.get.return_value = FakeModeMan() + objreg_mock.get.return_value = mode_manager with pytest.raises(cmdexc.CommandError): - utilcmdsmod.repeat_command(win_id=0) + utilcmds.repeat_command(win_id=0) def test_debug_log_level(mocker): @@ -121,7 +110,7 @@ def test_debug_log_level(mocker): 'qutebrowser.misc.utilcmds.log.change_console_formatter') handler_mock = mocker.patch( 'qutebrowser.misc.utilcmds.log.console_handler') - utilcmdsmod.debug_log_level(level='debug') + utilcmds.debug_log_level(level='debug') formatter_mock.assert_called_with(logging.DEBUG) handler_mock.setLevel.assert_called_with(logging.DEBUG) @@ -146,7 +135,7 @@ def test_window_only(mocker, monkeypatch): winreg_mock.window_registry = test_windows sip_mock = mocker.patch('qutebrowser.misc.utilcmds.sip') sip_mock.isdeleted.side_effect = lambda window: window.deleted - utilcmdsmod.window_only(current_win_id=0) + utilcmds.window_only(current_win_id=0) assert not test_windows[0].closed assert not test_windows[1].closed assert test_windows[2].closed From 23a62e952d628cb9d29eac07f78ab5d88c343ece Mon Sep 17 00:00:00 2001 From: Daniel Karbach Date: Thu, 27 Oct 2016 14:49:35 +0200 Subject: [PATCH 11/15] another lineparser/utilcmds test revision * verify exception message in lineparser double open * check for hunter with `pytest.importorskip` * stricter exception checking in debug_trace test --- tests/unit/misc/test_lineparser.py | 3 ++- tests/unit/misc/test_utilcmds.py | 17 ++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/unit/misc/test_lineparser.py b/tests/unit/misc/test_lineparser.py index 7b5710ece..3ea07a831 100644 --- a/tests/unit/misc/test_lineparser.py +++ b/tests/unit/misc/test_lineparser.py @@ -66,9 +66,10 @@ class TestBaseLineParser: mocker.patch('builtins.open', mock.mock_open()) with lineparser._open('r'): - with pytest.raises(IOError): + with pytest.raises(IOError) as excinfo: with lineparser._open('r'): pass + assert str(excinfo.value) == 'Refusing to double-open AppendLineParser.' def test_binary(self, mocker): """Test if _open and _write correctly handle binary files.""" diff --git a/tests/unit/misc/test_utilcmds.py b/tests/unit/misc/test_utilcmds.py index cbfb88f9c..1cdfed2f6 100644 --- a/tests/unit/misc/test_utilcmds.py +++ b/tests/unit/misc/test_utilcmds.py @@ -26,13 +26,6 @@ import pytest import signal import time -_hunter_available = False -try: - import hunter # pylint: disable=unused-import - _hunter_available = True -except ImportError: - pass - from qutebrowser.misc import utilcmds from qutebrowser.commands import cmdexc @@ -73,16 +66,22 @@ def test_debug_crash_segfault(): assert 'Segfault failed' in str(excinfo.value) -@pytest.mark.skipif(not _hunter_available, reason="hunter not available") def test_debug_trace(mocker): """Check if hunter.trace is properly called.""" + # but only if hunter is available + pytest.importorskip('hunter') hunter_mock = mocker.patch('qutebrowser.misc.utilcmds.hunter') utilcmds.debug_trace(1) assert hunter_mock.trace.assert_called_with(1) + def _mock_exception(): + """Side effect for testing debug_trace's reraise.""" + raise Exception('message') + hunter_mock.trace.side_effect = Exception - with pytest.raises(Exception): + with pytest.raises(CommandError) as excinfo: utilcmds.debug_trace() + assert str(excinfo.value) == 'Exception: message' def test_debug_trace_no_hunter(monkeypatch): From 6e510372fb404ebd5d8bbf407a36e9850d87401d Mon Sep 17 00:00:00 2001 From: Daniel Karbach Date: Thu, 27 Oct 2016 15:50:17 +0200 Subject: [PATCH 12/15] more cleanup jeez, this is getting embarrassing --- tests/unit/misc/test_lineparser.py | 4 ++-- tests/unit/misc/test_utilcmds.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/unit/misc/test_lineparser.py b/tests/unit/misc/test_lineparser.py index 3ea07a831..f5fbb2f4d 100644 --- a/tests/unit/misc/test_lineparser.py +++ b/tests/unit/misc/test_lineparser.py @@ -66,10 +66,10 @@ class TestBaseLineParser: mocker.patch('builtins.open', mock.mock_open()) with lineparser._open('r'): - with pytest.raises(IOError) as excinfo: + with pytest.raises(IOError) as excinf: with lineparser._open('r'): pass - assert str(excinfo.value) == 'Refusing to double-open AppendLineParser.' + assert str(excinf.value) == 'Refusing to double-open AppendLineParser.' def test_binary(self, mocker): """Test if _open and _write correctly handle binary files.""" diff --git a/tests/unit/misc/test_utilcmds.py b/tests/unit/misc/test_utilcmds.py index 1cdfed2f6..5445f91d2 100644 --- a/tests/unit/misc/test_utilcmds.py +++ b/tests/unit/misc/test_utilcmds.py @@ -79,7 +79,7 @@ def test_debug_trace(mocker): raise Exception('message') hunter_mock.trace.side_effect = Exception - with pytest.raises(CommandError) as excinfo: + with pytest.raises(cmdexc.CommandError) as excinfo: utilcmds.debug_trace() assert str(excinfo.value) == 'Exception: message' From e167f77d6844fa06aab95e4f5c9158e810df721f Mon Sep 17 00:00:00 2001 From: Daniel Karbach Date: Fri, 28 Oct 2016 10:44:55 +0200 Subject: [PATCH 13/15] separate test for hunter exceptions --- tests/unit/misc/test_utilcmds.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/unit/misc/test_utilcmds.py b/tests/unit/misc/test_utilcmds.py index 5445f91d2..1121dd926 100644 --- a/tests/unit/misc/test_utilcmds.py +++ b/tests/unit/misc/test_utilcmds.py @@ -74,11 +74,15 @@ def test_debug_trace(mocker): utilcmds.debug_trace(1) assert hunter_mock.trace.assert_called_with(1) +def test_debug_trace_exception(mocker): + """Check that exceptions thrown by hunter.trace are handled.""" + def _mock_exception(): """Side effect for testing debug_trace's reraise.""" raise Exception('message') - hunter_mock.trace.side_effect = Exception + hunter_mock = mocker.patch('qutebrowser.misc.utilcmds.hunter') + hunter_mock.trace.side_effect = _mock_exception with pytest.raises(cmdexc.CommandError) as excinfo: utilcmds.debug_trace() assert str(excinfo.value) == 'Exception: message' From 6fff45daeb301392e706fc83247d39461663c8fc Mon Sep 17 00:00:00 2001 From: Daniel Karbach Date: Fri, 28 Oct 2016 10:50:58 +0200 Subject: [PATCH 14/15] check exception messages in utilcmds tests --- tests/unit/misc/test_utilcmds.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/tests/unit/misc/test_utilcmds.py b/tests/unit/misc/test_utilcmds.py index 1121dd926..b42934edd 100644 --- a/tests/unit/misc/test_utilcmds.py +++ b/tests/unit/misc/test_utilcmds.py @@ -41,8 +41,9 @@ def _trapped_segv(handler): def test_debug_crash_exception(): """Verify that debug_crash crashes as intended.""" - with pytest.raises(Exception): + with pytest.raises(Exception) as excinfo: utilcmds.debug_crash(typ='exception') + assert str(excinfo.value) == 'Forced crash' @pytest.mark.skipif(os.name == 'nt', @@ -91,8 +92,10 @@ def test_debug_trace_exception(mocker): def test_debug_trace_no_hunter(monkeypatch): """Test that an error is shown if debug_trace is called without hunter.""" monkeypatch.setattr(utilcmds, 'hunter', None) - with pytest.raises(cmdexc.CommandError): + with pytest.raises(cmdexc.CommandError) as excinfo: utilcmds.debug_trace() + assert str(excinfo.value) == "You need to install 'hunter' to use this " \ + "command!" def test_repeat_command_initial(mocker, mode_manager): @@ -103,8 +106,9 @@ def test_repeat_command_initial(mocker, mode_manager): """ objreg_mock = mocker.patch('qutebrowser.misc.utilcmds.objreg') objreg_mock.get.return_value = mode_manager - with pytest.raises(cmdexc.CommandError): + with pytest.raises(cmdexc.CommandError) as excinfo: utilcmds.repeat_command(win_id=0) + assert str(excinfo.value) == "You didn't do anything yet." def test_debug_log_level(mocker): From cfa9068eedcce122fa85a1464e90d12b3d3ac476 Mon Sep 17 00:00:00 2001 From: Daniel Karbach Date: Fri, 28 Oct 2016 11:23:05 +0200 Subject: [PATCH 15/15] flake... *quitely shakes fist and then submits* --- tests/unit/misc/test_utilcmds.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/misc/test_utilcmds.py b/tests/unit/misc/test_utilcmds.py index b42934edd..41c2f1f15 100644 --- a/tests/unit/misc/test_utilcmds.py +++ b/tests/unit/misc/test_utilcmds.py @@ -75,9 +75,9 @@ def test_debug_trace(mocker): utilcmds.debug_trace(1) assert hunter_mock.trace.assert_called_with(1) + def test_debug_trace_exception(mocker): """Check that exceptions thrown by hunter.trace are handled.""" - def _mock_exception(): """Side effect for testing debug_trace's reraise.""" raise Exception('message')