Add a fake clipboard for tests

There are a lot of problems and flakiness with using a real clipboard.

Instead we now have a :debug-set-fake-clipboard command to set a text, and use
logging when getting the contents.

Fixes #1285.
This commit is contained in:
Florian Bruhin 2016-02-03 20:27:11 +01:00
parent 7fe818f9c8
commit 79f83a033d
14 changed files with 166 additions and 128 deletions

View File

@ -1236,6 +1236,7 @@ These commands are mainly intended for debugging. They are hidden if qutebrowser
|<<debug-crash,debug-crash>>|Crash for debugging purposes.
|<<debug-dump-page,debug-dump-page>>|Dump the current page's content to a file.
|<<debug-pyeval,debug-pyeval>>|Evaluate a python string and display the results as a web page.
|<<debug-set-fake-clipboard,debug-set-fake-clipboard>>|Put data into the fake clipboard and enable logging, used for tests.
|<<debug-trace,debug-trace>>|Trace executed code via hunter.
|<<debug-webaction,debug-webaction>>|Execute a webaction.
|==============
@ -1292,6 +1293,15 @@ Evaluate a python string and display the results as a web page.
* This command does not split arguments after the last argument and handles quotes literally.
* With this command, +;;+ is interpreted literally instead of splitting off a second command.
[[debug-set-fake-clipboard]]
=== debug-set-fake-clipboard
Syntax: +:debug-set-fake-clipboard ['s']+
Put data into the fake clipboard and enable logging, used for tests.
==== positional arguments
* +'s'+: The text to put into the fake clipboard, or unset to enable logging.
[[debug-trace]]
=== debug-trace
Syntax: +:debug-trace ['expr']+

View File

@ -260,7 +260,7 @@ def process_pos_args(args, via_ipc=False, cwd=None, target_arg=None):
if cmd.startswith(':'):
if win_id is None:
win_id = mainwindow.get_window(via_ipc, force_tab=True)
log.init.debug("Startup cmd {}".format(cmd))
log.init.debug("Startup cmd {!r}".format(cmd))
commandrunner = runners.CommandRunner(win_id)
commandrunner.run_safely_init(cmd[1:])
elif not cmd:

View File

@ -30,7 +30,7 @@ import xml.etree.ElementTree
from PyQt5.QtWebKit import QWebSettings
from PyQt5.QtWidgets import QApplication, QTabBar
from PyQt5.QtCore import Qt, QUrl, QEvent
from PyQt5.QtGui import QClipboard, QKeyEvent
from PyQt5.QtGui import QKeyEvent
from PyQt5.QtPrintSupport import QPrintDialog, QPrintPreviewDialog
from PyQt5.QtWebKitWidgets import QWebPage
import pygments
@ -659,7 +659,6 @@ class CommandDispatcher:
title: Yank the title instead of the URL.
domain: Yank only the scheme, domain, and port number.
"""
clipboard = QApplication.clipboard()
if title:
s = self._tabbed_browser.page_title(self._current_index())
what = 'title'
@ -673,14 +672,14 @@ class CommandDispatcher:
s = self._current_url().toString(
QUrl.FullyEncoded | QUrl.RemovePassword)
what = 'URL'
if sel and clipboard.supportsSelection():
mode = QClipboard.Selection
if sel and QApplication.clipboard().supportsSelection():
target = "primary selection"
else:
mode = QClipboard.Clipboard
sel = False
target = "clipboard"
log.misc.debug("Yanking to {}: '{}'".format(target, s))
clipboard.setText(s, mode)
utils.set_clipboard(s, selection=sel)
message.info(self._win_id, "Yanked {} to {}: {}".format(
what, target, s))
@ -811,14 +810,12 @@ class CommandDispatcher:
bg: Open in a background tab.
window: Open in new window.
"""
clipboard = QApplication.clipboard()
if sel and clipboard.supportsSelection():
mode = QClipboard.Selection
if sel and QApplication.clipboard().supportsSelection():
target = "Primary selection"
else:
mode = QClipboard.Clipboard
sel = False
target = "Clipboard"
text = clipboard.text(mode)
text = utils.get_clipboard(selection=sel)
if not text.strip():
raise cmdexc.CommandError("{} is empty.".format(target))
log.misc.debug("{} contained: '{}'".format(target,
@ -1313,17 +1310,19 @@ class CommandDispatcher:
if not elem.is_editable(strict=True):
raise cmdexc.CommandError("Focused element is not editable!")
clipboard = QApplication.clipboard()
if clipboard.supportsSelection():
sel = clipboard.text(QClipboard.Selection)
log.misc.debug("Pasting primary selection into element {}".format(
elem.debug_text()))
elem.evaluateJavaScript("""
var sel = '{}';
var event = document.createEvent('TextEvent');
event.initTextEvent('textInput', true, true, null, sel);
this.dispatchEvent(event);
""".format(webelem.javascript_escape(sel)))
try:
sel = utils.get_clipboard(selection=True)
except utils.SelectionUnsupportedError:
return
log.misc.debug("Pasting primary selection into element {}".format(
elem.debug_text()))
elem.evaluateJavaScript("""
var sel = '{}';
var event = document.createEvent('TextEvent');
event.initTextEvent('textInput', true, true, null, sel);
this.dispatchEvent(event);
""".format(webelem.javascript_escape(sel)))
def _clear_search(self, view, text):
"""Clear search string/highlights for the given view.
@ -1667,14 +1666,12 @@ class CommandDispatcher:
message.info(self._win_id, "Nothing to yank")
return
clipboard = QApplication.clipboard()
if sel and clipboard.supportsSelection():
mode = QClipboard.Selection
if sel and QApplication.clipboard().supportsSelection():
target = "primary selection"
else:
mode = QClipboard.Clipboard
sel = False
target = "clipboard"
clipboard.setText(s, mode)
utils.set_clipboard(s, sel)
message.info(self._win_id, "{} {} yanked to {}".format(
len(s), "char" if len(s) == 1 else "chars", target))
if not keep:

View File

@ -27,8 +27,7 @@ import string
from PyQt5.QtCore import (pyqtSignal, pyqtSlot, QObject, QEvent, Qt, QUrl,
QTimer)
from PyQt5.QtGui import QMouseEvent, QClipboard
from PyQt5.QtWidgets import QApplication
from PyQt5.QtGui import QMouseEvent
from PyQt5.QtWebKit import QWebElement
from PyQt5.QtWebKitWidgets import QWebPage
@ -36,8 +35,7 @@ from qutebrowser.config import config
from qutebrowser.keyinput import modeman, modeparsers
from qutebrowser.browser import webelem
from qutebrowser.commands import userscripts, cmdexc, cmdutils, runners
from qutebrowser.utils import (usertypes, log, qtutils, message,
objreg)
from qutebrowser.utils import usertypes, log, qtutils, message, objreg, utils
from qutebrowser.misc import guiprocess
@ -481,9 +479,9 @@ class HintManager(QObject):
context: The HintContext to use.
"""
sel = context.target == Target.yank_primary
mode = QClipboard.Selection if sel else QClipboard.Clipboard
urlstr = url.toString(QUrl.FullyEncoded | QUrl.RemovePassword)
QApplication.clipboard().setText(urlstr, mode)
utils.set_clipboard(urlstr, selection=sel)
msg = "Yanked URL to {}: {}".format(
"primary selection" if sel else "clipboard",
urlstr)

View File

@ -20,9 +20,9 @@
"""Misc. widgets used at different places."""
from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QSize
from PyQt5.QtWidgets import (QLineEdit, QApplication, QWidget, QHBoxLayout,
QLabel, QStyleOption, QStyle)
from PyQt5.QtGui import QValidator, QClipboard, QPainter
from PyQt5.QtWidgets import (QLineEdit, QWidget, QHBoxLayout, QLabel,
QStyleOption, QStyle)
from PyQt5.QtGui import QValidator, QPainter
from qutebrowser.utils import utils
from qutebrowser.misc import cmdhistory
@ -101,10 +101,12 @@ class CommandLineEdit(QLineEdit):
def keyPressEvent(self, e):
"""Override keyPressEvent to paste primary selection on Shift + Ins."""
if e.key() == Qt.Key_Insert and e.modifiers() == Qt.ShiftModifier:
clipboard = QApplication.clipboard()
if clipboard.supportsSelection():
try:
text = utils.get_clipboard(selection=True)
except utils.SelectionUnsupportedError:
pass
else:
e.accept()
text = clipboard.text(QClipboard.Selection)
self.insert(text)
return
super().keyPressEvent(e)

View File

@ -29,7 +29,7 @@ except ImportError:
hunter = None
from qutebrowser.browser.network import qutescheme
from qutebrowser.utils import log, objreg, usertypes, message, debug
from qutebrowser.utils import log, objreg, usertypes, message, debug, utils
from qutebrowser.commands import cmdutils, runners, cmdexc
from qutebrowser.config import style
from qutebrowser.misc import consolewidget
@ -198,3 +198,16 @@ def debug_pyeval(s, quiet=False):
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window='last-focused')
tabbed_browser.openurl(QUrl('qute:pyeval'), newtab=True)
@cmdutils.register(debug=True)
def debug_set_fake_clipboard(s=None):
"""Put data into the fake clipboard and enable logging, used for tests.
Args:
s: The text to put into the fake clipboard, or unset to enable logging.
"""
if s is None:
utils.log_clipboard = True
else:
utils.fake_clipboard = s

View File

@ -29,13 +29,23 @@ import contextlib
import itertools
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QKeySequence, QColor
from PyQt5.QtGui import QKeySequence, QColor, QClipboard
from PyQt5.QtWidgets import QApplication
import pkg_resources
import qutebrowser
from qutebrowser.utils import qtutils, log
fake_clipboard = None
log_clipboard = False
class SelectionUnsupportedError(Exception):
"""Raised if [gs]et_clipboard is used and selection=True is unsupported."""
def elide(text, length):
"""Elide text so it uses a maximum of length chars."""
if length < 1:
@ -743,3 +753,33 @@ def newest_slice(iterable, count):
return iterable
else:
return itertools.islice(iterable, len(iterable) - count, len(iterable))
def set_clipboard(data, selection=False):
"""Set the clipboard to some given data."""
clipboard = QApplication.clipboard()
if selection and not clipboard.supportsSelection():
raise SelectionUnsupportedError
if log_clipboard:
what = 'primary selection' if selection else 'clipboard'
log.misc.debug("Setting fake {}: {!r}".format(what, data))
else:
mode = QClipboard.Selection if selection else QClipboard.Clipboard
clipboard.setText(data, mode=mode)
def get_clipboard(selection=False):
"""Get data from the clipboard."""
global fake_clipboard
clipboard = QApplication.clipboard()
if selection and not clipboard.supportsSelection():
raise SelectionUnsupportedError
if fake_clipboard is not None:
data = fake_clipboard
fake_clipboard = None
else:
mode = QClipboard.Selection if selection else QClipboard.Clipboard
data = clipboard.text(mode=mode)
return data

View File

@ -81,6 +81,8 @@ def whitelist_generator():
yield 'qutebrowser.misc.utilcmds.pyeval_output'
yield 'utils.use_color'
yield 'qutebrowser.browser.mhtml.last_used_directory'
yield 'qutebrowser.utils.utils.fake_clipboard'
yield 'qutebrowser.utils.utils.log_clipboard'
# Other false-positives
yield ('qutebrowser.completion.models.sortfilter.CompletionFilterModel().'

View File

@ -30,28 +30,10 @@ import textwrap
import pytest
import yaml
import pytest_bdd as bdd
from PyQt5.QtCore import QElapsedTimer
from PyQt5.QtGui import QClipboard
from helpers import utils
class WaitForClipboardTimeout(Exception):
"""Raised when _wait_for_clipboard didn't get the expected message."""
def _clipboard_mode(qapp, what):
"""Get the QClipboard::Mode to use based on a string."""
if what == 'clipboard':
return QClipboard.Clipboard
elif what == 'primary selection':
assert qapp.clipboard().supportsSelection()
return QClipboard.Selection
else:
raise AssertionError
## Given
@ -207,21 +189,17 @@ def selection_supported(qapp):
@bdd.when(bdd.parsers.re(r'I put "(?P<content>.*)" into the '
r'(?P<what>primary selection|clipboard)'))
def fill_clipboard(qtbot, qapp, httpbin, what, content):
mode = _clipboard_mode(qapp, what)
def fill_clipboard(quteproc, httpbin, what, content):
content = content.replace('(port)', str(httpbin.port))
content = content.replace(r'\n', '\n')
clipboard = qapp.clipboard()
clipboard.setText(content, mode)
_wait_for_clipboard(qtbot, qapp.clipboard(), mode, content)
quteproc.send_cmd(':debug-set-fake-clipboard "{}"'.format(content))
@bdd.when(bdd.parsers.re(r'I put the following lines into the '
r'(?P<what>primary selection|clipboard):\n'
r'(?P<content>.+)$', flags=re.DOTALL))
def fill_clipboard_multiline(qtbot, qapp, httpbin, what, content):
fill_clipboard(qtbot, qapp, httpbin, what, textwrap.dedent(content))
def fill_clipboard_multiline(quteproc, httpbin, what, content):
fill_clipboard(quteproc, httpbin, what, textwrap.dedent(content))
## Then
@ -417,51 +395,18 @@ def check_open_tabs(quteproc, tabs):
assert 'active' not in session_tab
def _wait_for_clipboard(qtbot, clipboard, mode, expected):
timeout = 1000
timer = QElapsedTimer()
timer.start()
while True:
if clipboard.text(mode=mode) == expected:
return
# We need to poll the clipboard, as for some reason it can change with
# emitting changed (?).
with qtbot.waitSignal(clipboard.changed, timeout=100, raising=False):
pass
if timer.hasExpired(timeout):
mode_names = {
QClipboard.Clipboard: 'clipboard',
QClipboard.Selection: 'primary selection',
}
raise WaitForClipboardTimeout(
"Timed out after {timeout}ms waiting for {what}:\n"
" expected: {expected!r}\n"
" clipboard: {clipboard!r}\n"
" primary: {primary!r}.".format(
timeout=timeout, what=mode_names[mode],
expected=expected,
clipboard=clipboard.text(mode=QClipboard.Clipboard),
primary=clipboard.text(mode=QClipboard.Selection))
)
@bdd.then(bdd.parsers.re(r'the (?P<what>primary selection|clipboard) should '
r'contain "(?P<content>.*)"'))
def clipboard_contains(qtbot, qapp, httpbin, what, content):
mode = _clipboard_mode(qapp, what)
def clipboard_contains(quteproc, httpbin, what, content):
expected = content.replace('(port)', str(httpbin.port))
expected = expected.replace('\\n', '\n')
_wait_for_clipboard(qtbot, qapp.clipboard(), mode, expected)
quteproc.wait_for(message='Setting fake {}: {!r}'.format(what, expected))
@bdd.then(bdd.parsers.parse('the clipboard should contain:\n{content}'))
def clipboard_contains_multiline(qtbot, qapp, content):
def clipboard_contains_multiline(quteproc, content):
expected = textwrap.dedent(content)
_wait_for_clipboard(qtbot, qapp.clipboard(), QClipboard.Clipboard,
expected)
quteproc.wait_for(message='Setting fake clipboard: {!r}'.format(expected))
@bdd.then("qutebrowser should quit")

View File

@ -20,7 +20,7 @@
import pytest_bdd as bdd
# pylint: disable=unused-import
from test_yankpaste import skip_with_broken_clipboard
from test_yankpaste import init_fake_clipboard
bdd.scenarios('caret.feature')

View File

@ -20,7 +20,7 @@
import pytest_bdd as bdd
# pylint: disable=unused-import
from test_yankpaste import skip_with_broken_clipboard
from test_yankpaste import init_fake_clipboard
bdd.scenarios('search.feature')

View File

@ -26,21 +26,9 @@ bdd.scenarios('yankpaste.feature')
@pytest.fixture(autouse=True)
def skip_with_broken_clipboard(qtbot, qapp):
"""The clipboard seems to be broken on some platforms (OS X Yosemite?).
This skips the tests if this is the case.
"""
clipboard = qapp.clipboard()
with qtbot.waitSignal(clipboard.changed, raising=False):
clipboard.setText("Does this work?")
if clipboard.text() != "Does this work?":
pytest.skip("Clipboard seems to be broken on this platform.")
with qtbot.waitSignal(clipboard.changed):
clipboard.clear()
def init_fake_clipboard(quteproc):
"""Make sure the fake clipboard will be used."""
quteproc.send_cmd(':debug-set-fake-clipboard')
@bdd.when(bdd.parsers.parse('I set the text field to "{value}"'))

View File

@ -182,8 +182,6 @@ Feature: Yanking and pasting.
And I run :paste -t
Then no crash should happen
# https://github.com/The-Compiler/qutebrowser/issues/1285
@xfail
Scenario: Pasting multiple urls with an almost empty one
When I open about:blank
And I put "http://localhost:(port)/data/hello.txt\n \nhttp://localhost:(port)/data/hello2.txt" into the clipboard
@ -192,8 +190,6 @@ Feature: Yanking and pasting.
#### :paste-primary
# https://github.com/The-Compiler/qutebrowser/issues/1285
@xfail
Scenario: Pasting the primary selection into an empty text field
When selection is supported
And I open data/paste_primary.html

View File

@ -29,7 +29,7 @@ import functools
import collections
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QColor
from PyQt5.QtGui import QColor, QClipboard
import pytest
import qutebrowser
@ -930,3 +930,50 @@ class TestNewestSlice:
"""Test slices which shouldn't raise an exception."""
sliced = utils.newest_slice(items, count)
assert list(sliced) == list(expected)
class TestGetSetClipboard:
@pytest.fixture(autouse=True)
def clipboard_mock(self, mocker):
m = mocker.patch('qutebrowser.utils.utils.QApplication.clipboard',
autospec=True)
clipboard = m()
clipboard.text.return_value = 'mocked clipboard text'
return clipboard
def test_set(self, clipboard_mock, caplog):
utils.set_clipboard('Hello World')
clipboard_mock.setText.assert_called_with('Hello World',
mode=QClipboard.Clipboard)
assert not caplog.records
def test_set_unsupported_selection(self, clipboard_mock):
clipboard_mock.supportsSelection.return_value = False
with pytest.raises(utils.SelectionUnsupportedError):
utils.set_clipboard('foo', selection=True)
@pytest.mark.parametrize('selection, what', [
(True, 'primary selection'),
(False, 'clipboard'),
])
def test_set_logging(self, clipboard_mock, caplog, selection, what):
utils.log_clipboard = True
utils.set_clipboard('fake clipboard text', selection=selection)
assert not clipboard_mock.setText.called
expected = "Setting fake {}: 'fake clipboard text'".format(what)
assert caplog.records[0].message == expected
def test_get(self):
assert utils.get_clipboard() == 'mocked clipboard text'
def test_get_unsupported_selection(self, clipboard_mock):
clipboard_mock.supportsSelection.return_value = False
with pytest.raises(utils.SelectionUnsupportedError):
utils.get_clipboard(selection=True)
@pytest.mark.parametrize('selection', [True, False])
def test_get_fake_clipboard(self, selection):
utils.fake_clipboard = 'fake clipboard text'
utils.get_clipboard(selection=selection)
assert utils.fake_clipboard is None