qutebrowser/tests/unit/misc/test_editor.py
Ryan Roden-Corrent 23eb6a6c53 Fix test_editor for edit-on-write behavior.
Now that the editor fires editing_finished on every write, the unit
tests had to be updated.

- Add qtbot to the editor fixture to resolve `QtWarningMsg:
  QSocketNotifier: Can only be used with threads started with QThread`
- Use removePaths instead of disconnect to stop the watcher from
  signalling. This avoids an error when the editor is forcibly cleaned
  up by the tests without the signal ever being connected, but otherwise
  has the same behavior as disconnecting the singal.
- wait for a signal on write instead of proc closed
- wait for _watcher.fileChanged in test_unreadable to ensure the write
  event is fired before the test exits.
2018-01-27 15:03:18 -05:00

199 lines
6.5 KiB
Python

# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014-2017 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# 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 <http://www.gnu.org/licenses/>.
"""Tests for qutebrowser.misc.editor."""
import os
import os.path
import logging
from PyQt5.QtCore import QProcess
import pytest
from qutebrowser.misc import editor as editormod
from qutebrowser.utils import usertypes
@pytest.fixture(autouse=True)
def patch_things(config_stub, monkeypatch, stubs):
monkeypatch.setattr(editormod.guiprocess, 'QProcess',
stubs.fake_qprocess())
@pytest.fixture
def editor(caplog, qtbot):
ed = editormod.ExternalEditor()
yield ed
with caplog.at_level(logging.ERROR):
ed._remove_file = True
ed._cleanup()
class TestArg:
"""Test argument handling.
Attributes:
editor: The ExternalEditor instance to test.
"""
def test_placeholder(self, config_stub, editor):
"""Test starting editor with placeholder argument."""
config_stub.val.editor.command = ['bin', 'foo', '{}', 'bar']
editor.edit("")
editor._proc._proc.start.assert_called_with(
"bin", ["foo", editor._filename, "bar"])
def test_placeholder_inline(self, config_stub, editor):
"""Test starting editor with placeholder arg inside of another arg."""
config_stub.val.editor.command = ['bin', 'foo{}', 'bar']
editor.edit("")
editor._proc._proc.start.assert_called_with(
"bin", ["foo" + editor._filename, "bar"])
class TestFileHandling:
"""Test creation/deletion of tempfile."""
def test_ok(self, editor):
"""Test file handling when closing with an exit status == 0."""
editor.edit("")
filename = editor._filename
assert os.path.exists(filename)
assert os.path.basename(filename).startswith('qutebrowser-editor-')
editor._proc.finished.emit(0, QProcess.NormalExit)
assert not os.path.exists(filename)
def test_existing_file(self, editor, tmpdir):
"""Test editing an existing file."""
path = tmpdir / 'foo.txt'
path.ensure()
editor.edit_file(str(path))
editor._proc.finished.emit(0, QProcess.NormalExit)
assert path.exists()
def test_error(self, editor):
"""Test file handling when closing with an exit status != 0."""
editor.edit("")
filename = editor._filename
assert os.path.exists(filename)
editor._proc._proc.exitStatus = lambda: QProcess.CrashExit
editor._proc.finished.emit(1, QProcess.NormalExit)
assert os.path.exists(filename)
os.remove(filename)
def test_crash(self, editor):
"""Test file handling when closing with a crash."""
editor.edit("")
filename = editor._filename
assert os.path.exists(filename)
editor._proc._proc.exitStatus = lambda: QProcess.CrashExit
editor._proc.error.emit(QProcess.Crashed)
editor._proc.finished.emit(0, QProcess.CrashExit)
assert os.path.exists(filename)
os.remove(filename)
def test_unreadable(self, message_mock, editor, caplog, qtbot):
"""Test file handling when closing with an unreadable file."""
editor.edit("")
filename = editor._filename
assert os.path.exists(filename)
os.chmod(filename, 0o277)
if os.access(filename, os.R_OK):
# Docker container or similar
pytest.skip("File was still readable")
with caplog.at_level(logging.ERROR):
with qtbot.wait_signal(editor._watcher.fileChanged):
with open(filename, 'w', encoding='utf-8') as f:
f.write('unreadable')
editor._proc.finished.emit(0, QProcess.NormalExit)
assert not os.path.exists(filename)
msg = message_mock.getmsg(usertypes.MessageLevel.error)
assert msg.text.startswith("Failed to read back edited file: ")
def test_unwritable(self, monkeypatch, message_mock, editor, tmpdir,
caplog):
"""Test file handling when the initial file is not writable."""
tmpdir.chmod(0)
if os.access(str(tmpdir), os.W_OK):
# Docker container or similar
pytest.skip("File was still writable")
monkeypatch.setattr(editormod.tempfile, 'tempdir', str(tmpdir))
with caplog.at_level(logging.ERROR):
editor.edit("")
msg = message_mock.getmsg(usertypes.MessageLevel.error)
assert msg.text.startswith("Failed to create initial file: ")
assert editor._proc is None
def test_double_edit(self, editor):
editor.edit("")
with pytest.raises(ValueError):
editor.edit("")
@pytest.mark.parametrize('initial_text, edited_text', [
('', 'Hello'),
('Hello', 'World'),
('Hällö Wörld', 'Überprüfung'),
('\u2603', '\u2601') # Unicode snowman -> cloud
])
def test_modify(qtbot, editor, initial_text, edited_text):
"""Test if inputs get modified correctly."""
editor.edit(initial_text)
with open(editor._filename, 'r', encoding='utf-8') as f:
assert f.read() == initial_text
with qtbot.wait_signal(editor.editing_finished) as blocker:
with open(editor._filename, 'w', encoding='utf-8') as f:
f.write(edited_text)
editor._proc.finished.emit(0, QProcess.NormalExit)
assert blocker.args == [edited_text]
@pytest.mark.parametrize('text, caret_position, result', [
('', 0, (1, 1)),
('a', 0, (1, 1)),
('a\nb', 1, (1, 2)),
('a\nb', 2, (2, 1)),
('a\nb', 3, (2, 2)),
('a\nbb\nccc', 4, (2, 3)),
('a\nbb\nccc', 5, (3, 1)),
('a\nbb\nccc', 8, (3, 4)),
('', None, (1, 1)),
])
def test_calculation(editor, text, caret_position, result):
"""Test calculation for line and column given text and caret_position."""
assert editor._calc_line_and_column(text, caret_position) == result