Merge remote-tracking branch 'origin/urlutils-tests'

This commit is contained in:
Florian Bruhin 2015-07-06 17:58:06 +02:00
commit 382b52a0b8
3 changed files with 496 additions and 171 deletions

View File

@ -266,7 +266,7 @@ def is_url(urlstr):
elif autosearch == 'naive':
log.url.debug("Checking via naive check")
url = _is_url_naive(urlstr)
else:
else: # pragma: no cover
raise ValueError("Invalid autosearch value")
log.url.debug("url = {}".format(url))
return url

View File

@ -28,7 +28,7 @@ import pytest
import stubs as stubsmod
from qutebrowser.config import configexc
from qutebrowser.utils import objreg
from qutebrowser.utils import objreg, usertypes
@pytest.fixture(scope='session', autouse=True)
@ -217,3 +217,58 @@ def pytest_runtest_setup(item):
pytest.skip("Can't be run when frozen!")
elif item.get_marker('frozen') and not getattr(sys, 'frozen', False):
pytest.skip("Can only run when frozen!")
class MessageMock:
"""Helper object for message_mock.
Attributes:
_monkeypatch: The pytest monkeypatch fixture.
MessageLevel: An enum with possible message levels.
Message: A namedtuple representing a message.
messages: A list of Message tuples.
"""
Message = collections.namedtuple('Message', ['level', 'win_id', 'text',
'immediate'])
MessageLevel = usertypes.enum('Level', ('error', 'info', 'warning'))
def __init__(self, monkeypatch):
self._monkeypatch = monkeypatch
self.messages = []
def _handle(self, level, win_id, text, immediately=False):
self.messages.append(self.Message(level, win_id, text, immediately))
def _handle_error(self, *args, **kwargs):
self._handle(self.MessageLevel.error, *args, **kwargs)
def _handle_info(self, *args, **kwargs):
self._handle(self.MessageLevel.info, *args, **kwargs)
def _handle_warning(self, *args, **kwargs):
self._handle(self.MessageLevel.warning, *args, **kwargs)
def getmsg(self):
"""Get the only message in self.messages.
Raises ValueError if there are multiple or no messages.
"""
if len(self.messages) != 1:
raise ValueError("Got {} messages but expected a single "
"one.".format(len(self.messages)))
return self.messages[0]
def patch(self, module_path):
"""Patch message.* in the given module (as a string)."""
self._monkeypatch.setattr(module_path + '.error', self._handle_error)
self._monkeypatch.setattr(module_path + '.info', self._handle_info)
self._monkeypatch.setattr(module_path + '.warning',
self._handle_warning)
@pytest.fixture
def message_mock(monkeypatch):
"""Fixture to get a MessageMock."""
return MessageMock(monkeypatch)

View File

@ -21,209 +21,479 @@
"""Tests for qutebrowser.utils.urlutils."""
import os.path
import collections
from PyQt5.QtCore import QUrl
import pytest
from qutebrowser.utils import urlutils
from qutebrowser.commands import cmdexc
from qutebrowser.utils import utils, urlutils, qtutils
def init_config_stub(stub, auto_search=True):
class FakeDNS:
"""Helper class for the fake_dns fixture.
Class attributes:
FakeDNSAnswer: Helper class/namedtuple imitating a QHostInfo object
(used by fromname_mock).
Attributes:
used: Whether the fake DNS server was used since it was
created/reseted.
answer: What to return for the given host(True/False). Needs to be set
when fromname_mock is called.
"""
FakeDNSAnswer = collections.namedtuple('FakeDNSAnswer', ['error'])
def __init__(self):
self.reset()
def __repr__(self):
return utils.get_repr(self, used=self.used, answer=self.answer)
def reset(self):
"""Reset used/answer as if the FakeDNS was freshly created."""
self.used = False
self.answer = None
def _get_error(self):
return not self.answer
def fromname_mock(self, _host):
"""Simple mock for QHostInfo::fromName returning a FakeDNSAnswer."""
if self.answer is None:
raise ValueError("Got called without answer being set. This means "
"something tried to make an unexpected DNS "
"request (QHostInfo::fromName).")
if self.used:
raise ValueError("Got used twice!.")
self.used = True
return self.FakeDNSAnswer(error=self._get_error)
@pytest.fixture(autouse=True)
def fake_dns(monkeypatch):
"""Patched QHostInfo.fromName to catch DNS requests.
With autouse=True so accidental DNS requests get discovered because the
fromname_mock will be called without answer being set.
"""
dns = FakeDNS()
monkeypatch.setattr('qutebrowser.utils.urlutils.QHostInfo.fromName',
dns.fromname_mock)
return dns
@pytest.fixture(autouse=True)
def urlutils_config_stub(config_stub, monkeypatch):
"""Initialize the given config_stub.
Args:
stub: The ConfigStub provided by the config_stub fixture.
auto_search: The value auto-search should have.
"""
stub.data = {
'general': {'auto-search': auto_search},
config_stub.data = {
'general': {'auto-search': True},
'searchengines': {
'test': 'http://www.qutebrowser.org/?q={}',
'DEFAULT': 'http://www.example.com/?q={}',
},
}
monkeypatch.setattr('qutebrowser.utils.urlutils.config', config_stub)
return config_stub
class TestSpecialURL:
@pytest.fixture
def urlutils_message_mock(message_mock):
"""Customized message_mock for the urlutils module."""
message_mock.patch('qutebrowser.utils.urlutils.message')
return message_mock
"""Test is_special_url.
Attributes:
SPECIAL_URLS: URLs which are special.
NORMAL_URLS: URLs which are not special.
class TestFuzzyUrl:
"""Tests for urlutils.fuzzy_url()."""
@pytest.fixture
def os_mock(self, mocker):
"""Mock the os module and some os.path functions."""
m = mocker.patch('qutebrowser.utils.urlutils.os')
# Using / to get the same behavior across OS'
m.path.join.side_effect = lambda *args: '/'.join(args)
m.path.expanduser.side_effect = os.path.expanduser
return m
@pytest.fixture
def is_url_mock(self, mocker):
return mocker.patch('qutebrowser.utils.urlutils.is_url')
@pytest.fixture
def get_search_url_mock(self, mocker):
return mocker.patch('qutebrowser.utils.urlutils._get_search_url')
def test_file_relative_cwd(self, os_mock):
"""Test with relative=True, cwd set, and an existing file."""
os_mock.path.exists.return_value = True
os_mock.path.isabs.return_value = False
url = urlutils.fuzzy_url('foo', cwd='cwd', relative=True)
os_mock.path.exists.assert_called_once_with('cwd/foo')
assert url == QUrl('file:cwd/foo')
def test_file_relative(self, os_mock):
"""Test with relative=True and cwd unset."""
os_mock.path.exists.return_value = True
os_mock.path.abspath.return_value = 'abs_path'
os_mock.path.isabs.return_value = False
url = urlutils.fuzzy_url('foo', relative=True)
os_mock.path.exists.assert_called_once_with('abs_path')
assert url == QUrl('file:abs_path')
def test_file_relative_os_error(self, os_mock, is_url_mock):
"""Test with relative=True, cwd unset and abspath raising OSError."""
os_mock.path.abspath.side_effect = OSError
os_mock.path.exists.return_value = True
os_mock.path.isabs.return_value = False
is_url_mock.return_value = True
url = urlutils.fuzzy_url('foo', relative=True)
assert not os_mock.path.exists.called
assert url == QUrl('http://foo')
def test_file_absolute(self, os_mock):
"""Test with an absolute path."""
os_mock.path.exists.return_value = True
os_mock.path.isabs.return_value = True
url = urlutils.fuzzy_url('/foo')
assert url == QUrl('file:///foo')
@pytest.mark.posix
def test_file_absolute_expanded(self, os_mock):
"""Make sure ~ gets expanded correctly."""
os_mock.path.exists.return_value = True
os_mock.path.isabs.return_value = True
url = urlutils.fuzzy_url('~/foo')
assert url == QUrl('file://' + os.path.expanduser('~/foo'))
def test_address(self, os_mock, is_url_mock):
"""Test passing something with relative=False."""
os_mock.path.isabs.return_value = False
is_url_mock.return_value = True
url = urlutils.fuzzy_url('foo')
assert url == QUrl('http://foo')
def test_search_term(self, os_mock, is_url_mock, get_search_url_mock):
"""Test passing something with do_search=True."""
os_mock.path.isabs.return_value = False
is_url_mock.return_value = False
get_search_url_mock.return_value = QUrl('search_url')
url = urlutils.fuzzy_url('foo', do_search=True)
assert url == QUrl('search_url')
def test_search_term_value_error(self, os_mock, is_url_mock,
get_search_url_mock):
"""Test with do_search=True and ValueError in _get_search_url."""
os_mock.path.isabs.return_value = False
is_url_mock.return_value = False
get_search_url_mock.side_effect = ValueError
url = urlutils.fuzzy_url('foo', do_search=True)
assert url == QUrl('http://foo')
def test_no_do_search(self, is_url_mock):
"""Test with do_search = False."""
is_url_mock.return_value = False
url = urlutils.fuzzy_url('foo', do_search=False)
assert url == QUrl('http://foo')
@pytest.mark.parametrize('do_search, exception', [
(True, qtutils.QtValueError),
(False, urlutils.FuzzyUrlError),
])
def test_invalid_url(self, do_search, exception, is_url_mock, monkeypatch):
"""Test with an invalid URL."""
is_url_mock.return_value = True
monkeypatch.setattr('qutebrowser.utils.urlutils.qurl_from_user_input',
lambda url: QUrl())
with pytest.raises(exception):
urlutils.fuzzy_url('foo', do_search=do_search)
@pytest.mark.parametrize('url, special', [
('file:///tmp/foo', True),
('about:blank', True),
('qute:version', True),
('http://www.qutebrowser.org/', False),
('www.qutebrowser.org', False),
])
def test_special_urls(url, special):
assert urlutils.is_special_url(QUrl(url)) == special
@pytest.mark.parametrize('url, host, query', [
('testfoo', 'www.example.com', 'q=testfoo'),
('test testfoo', 'www.qutebrowser.org', 'q=testfoo'),
('test testfoo bar foo', 'www.qutebrowser.org', 'q=testfoo bar foo'),
('test testfoo ', 'www.qutebrowser.org', 'q=testfoo'),
('!python testfoo', 'www.example.com', 'q=%21python testfoo'),
('blub testfoo', 'www.example.com', 'q=blub testfoo'),
])
def test_get_search_url(urlutils_config_stub, url, host, query):
"""Test _get_search_url().
Args:
url: The "URL" to enter.
host: The expected search machine host.
query: The expected search query.
"""
SPECIAL_URLS = (
'file:///tmp/foo',
'about:blank',
'qute:version'
)
NORMAL_URLS = (
'http://www.qutebrowser.org/',
'www.qutebrowser.org'
)
@pytest.mark.parametrize('url', SPECIAL_URLS)
def test_special_urls(self, url):
"""Test special URLs."""
u = QUrl(url)
assert urlutils.is_special_url(u)
@pytest.mark.parametrize('url', NORMAL_URLS)
def test_normal_urls(self, url):
"""Test non-special URLs."""
u = QUrl(url)
assert not urlutils.is_special_url(u)
url = urlutils._get_search_url(url)
assert url.host() == host
assert url.query() == query
class TestSearchUrl:
@pytest.mark.parametrize('is_url, is_url_no_autosearch, uses_dns, url', [
# Normal hosts
(True, True, False, 'http://foobar'),
(True, True, False, 'localhost:8080'),
(True, True, True, 'qutebrowser.org'),
(True, True, True, ' qutebrowser.org '),
(True, True, False, 'http://user:password@example.com/foo?bar=baz#fish'),
# IPs
(True, True, False, '127.0.0.1'),
(True, True, False, '::1'),
(True, True, True, '2001:41d0:2:6c11::1'),
(True, True, True, '94.23.233.17'),
# Special URLs
(True, True, False, 'file:///tmp/foo'),
(True, True, False, 'about:blank'),
(True, True, False, 'qute:version'),
(True, True, False, 'localhost'),
# _has_explicit_scheme False, special_url True
(True, True, False, 'qute::foo'),
# Invalid URLs
(False, True, False, ''),
(False, True, False, 'http:foo:0'),
# Not URLs
(False, True, False, 'foo bar'), # no DNS because of space
(False, True, False, 'localhost test'), # no DNS because of space
(False, True, False, 'another . test'), # no DNS because of space
(False, True, True, 'foo'),
(False, True, False, 'this is: not an URL'), # no DNS because of space
(False, True, False, '23.42'), # no DNS because bogus-IP
(False, True, False, '1337'), # no DNS because bogus-IP
(False, True, True, 'deadbeef'),
(False, True, False, '31c3'), # no DNS because bogus-IP
(False, True, False, 'foo::bar'), # no DNS because of no host
# Valid search term with autosearch
(False, False, False, 'test foo'),
# autosearch = False
(False, True, False, 'This is an URL without autosearch'),
])
def test_is_url(urlutils_config_stub, fake_dns, is_url, is_url_no_autosearch,
uses_dns, url):
"""Test is_url().
"""Test _get_search_url."""
@pytest.fixture(autouse=True)
def mock_config(self, config_stub, monkeypatch):
"""Fixture to patch urlutils.config with a stub."""
init_config_stub(config_stub)
monkeypatch.setattr('qutebrowser.utils.urlutils.config', config_stub)
def test_default_engine(self):
"""Test default search engine."""
url = urlutils._get_search_url('testfoo')
assert url.host() == 'www.example.com'
assert url.query() == 'q=testfoo'
def test_engine_pre(self):
"""Test search engine name with one word."""
url = urlutils._get_search_url('test testfoo')
assert url.host() == 'www.qutebrowser.org'
assert url.query() == 'q=testfoo'
def test_engine_pre_multiple_words(self):
"""Test search engine name with multiple words."""
url = urlutils._get_search_url('test testfoo bar foo')
assert url.host() == 'www.qutebrowser.org'
assert url.query() == 'q=testfoo bar foo'
def test_engine_pre_whitespace_at_end(self):
"""Test search engine name with one word and whitespace."""
url = urlutils._get_search_url('test testfoo ')
assert url.host() == 'www.qutebrowser.org'
assert url.query() == 'q=testfoo'
def test_engine_with_bang_pre(self):
"""Test search engine with a prepended !bang."""
url = urlutils._get_search_url('!python testfoo')
assert url.host() == 'www.example.com'
assert url.query() == 'q=%21python testfoo'
def test_engine_wrong(self):
"""Test with wrong search engine."""
url = urlutils._get_search_url('blub testfoo')
assert url.host() == 'www.example.com'
assert url.query() == 'q=blub testfoo'
class TestIsUrl:
"""Tests for is_url.
Class attributes:
URLS: A list of strings which are URLs.
NOT_URLS: A list of strings which aren't URLs.
Args:
is_url: Whether the given string is an URL with auto-search dns/naive.
is_url_no_autosearch: Whether the given string is an URL with
auto-search false.
uses_dns: Whether the given string should fire a DNS request for the
given URL.
url: The URL to test, as a string.
"""
urlutils_config_stub.data['general']['auto-search'] = 'dns'
if uses_dns:
fake_dns.answer = True
result = urlutils.is_url(url)
assert fake_dns.used
assert result
fake_dns.reset()
URLS = (
'http://foobar',
'localhost:8080',
'qutebrowser.org',
' qutebrowser.org ',
'127.0.0.1',
'::1',
'2001:41d0:2:6c11::1',
'94.23.233.17',
'http://user:password@qutebrowser.org/foo?bar=baz#fish',
)
fake_dns.answer = False
result = urlutils.is_url(url)
assert fake_dns.used
assert not result
else:
result = urlutils.is_url(url)
assert not fake_dns.used
assert result == is_url
NOT_URLS = (
'foo bar',
'localhost test',
'another . test',
'foo',
'this is: not an URL',
'23.42',
'1337',
'deadbeef',
'31c3',
'http:foo:0',
'foo::bar',
)
fake_dns.reset()
urlutils_config_stub.data['general']['auto-search'] = 'naive'
assert urlutils.is_url(url) == is_url
assert not fake_dns.used
@pytest.mark.parametrize('url', URLS)
def test_urls(self, monkeypatch, config_stub, url):
"""Test things which are URLs."""
init_config_stub(config_stub, 'naive')
monkeypatch.setattr('qutebrowser.utils.urlutils.config', config_stub)
assert urlutils.is_url(url), url
@pytest.mark.parametrize('url', NOT_URLS)
def test_not_urls(self, monkeypatch, config_stub, url):
"""Test things which are not URLs."""
init_config_stub(config_stub, 'naive')
monkeypatch.setattr('qutebrowser.utils.urlutils.config', config_stub)
assert not urlutils.is_url(url), url
@pytest.mark.parametrize('autosearch', [True, False])
def test_search_autosearch(self, monkeypatch, config_stub, autosearch):
"""Test explicit search with auto-search=True."""
init_config_stub(config_stub, autosearch)
monkeypatch.setattr('qutebrowser.utils.urlutils.config', config_stub)
assert not urlutils.is_url('test foo')
fake_dns.reset()
urlutils_config_stub.data['general']['auto-search'] = False
assert urlutils.is_url(url) == is_url_no_autosearch
assert not fake_dns.used
class TestQurlFromUserInput:
@pytest.mark.parametrize('user_input, output', [
('qutebrowser.org', 'http://qutebrowser.org'),
('http://qutebrowser.org', 'http://qutebrowser.org'),
('::1/foo', 'http://[::1]/foo'),
('[::1]/foo', 'http://[::1]/foo'),
('http://[::1]', 'http://[::1]'),
('qutebrowser.org', 'http://qutebrowser.org'),
('http://qutebrowser.org', 'http://qutebrowser.org'),
('::1/foo', 'http://[::1]/foo'),
('[::1]/foo', 'http://[::1]/foo'),
('http://[::1]', 'http://[::1]'),
])
def test_qurl_from_user_input(user_input, output):
"""Test qurl_from_user_input.
"""Tests for qurl_from_user_input."""
def test_url(self):
"""Test a normal URL."""
url = urlutils.qurl_from_user_input('qutebrowser.org')
assert url.toString() == 'http://qutebrowser.org'
def test_url_http(self):
"""Test a normal URL with http://."""
url = urlutils.qurl_from_user_input('http://qutebrowser.org')
assert url.toString() == 'http://qutebrowser.org'
def test_ipv6_bare(self):
"""Test an IPv6 without brackets."""
url = urlutils.qurl_from_user_input('::1/foo')
assert url.toString() == 'http://[::1]/foo'
def test_ipv6(self):
"""Test an IPv6 with brackets."""
url = urlutils.qurl_from_user_input('[::1]/foo')
assert url.toString() == 'http://[::1]/foo'
def test_ipv6_http(self):
"""Test an IPv6 with http:// and brackets."""
url = urlutils.qurl_from_user_input('http://[::1]')
assert url.toString() == 'http://[::1]'
Args:
user_input: The string to pass to qurl_from_user_input.
output: The expected QUrl string.
"""
url = urlutils.qurl_from_user_input(user_input)
assert url.toString() == output
class TestFilenameFromUrl:
@pytest.mark.parametrize('url, valid, has_err_string', [
('http://www.example.com/', True, False),
('', False, False),
('://', False, True),
])
def test_invalid_url_error(urlutils_message_mock, url, valid, has_err_string):
"""Test invalid_url_error().
"""Tests for filename_from_url."""
Args:
url: The URL to check.
valid: Whether the QUrl is valid (isValid() == True).
has_err_string: Whether the QUrl is expected to have errorString set.
"""
qurl = QUrl(url)
assert qurl.isValid() == valid
if valid:
with pytest.raises(ValueError):
urlutils.invalid_url_error(0, qurl, '')
assert not urlutils_message_mock.messages
else:
assert bool(qurl.errorString()) == has_err_string
urlutils.invalid_url_error(0, qurl, 'frozzle')
def test_invalid_url(self):
"""Test with an invalid QUrl."""
assert urlutils.filename_from_url(QUrl()) is None
msg = urlutils_message_mock.getmsg()
assert msg.win_id == 0
assert not msg.immediate
if has_err_string:
expected_text = ("Trying to frozzle with invalid URL - " +
qurl.errorString())
else:
expected_text = "Trying to frozzle with invalid URL"
assert msg.text == expected_text
def test_url_path(self):
"""Test with an URL with path."""
url = QUrl('http://qutebrowser.org/test.html')
assert urlutils.filename_from_url(url) == 'test.html'
def test_url_host(self):
"""Test with an URL with no path."""
url = QUrl('http://qutebrowser.org/')
assert urlutils.filename_from_url(url) == 'qutebrowser.org.html'
@pytest.mark.parametrize('url, valid, has_err_string', [
('http://www.example.com/', True, False),
('', False, False),
('://', False, True),
])
def test_raise_cmdexc_if_invalid(url, valid, has_err_string):
"""Test raise_cmdexc_if_invalid.
Args:
url: The URL to check.
valid: Whether the QUrl is valid (isValid() == True).
has_err_string: Whether the QUrl is expected to have errorString set.
"""
qurl = QUrl(url)
assert qurl.isValid() == valid
if valid:
urlutils.raise_cmdexc_if_invalid(qurl)
else:
assert bool(qurl.errorString()) == has_err_string
with pytest.raises(cmdexc.CommandError) as excinfo:
urlutils.raise_cmdexc_if_invalid(qurl)
if has_err_string:
expected_text = "Invalid URL - " + qurl.errorString()
else:
expected_text = "Invalid URL"
assert str(excinfo.value) == expected_text
@pytest.mark.parametrize('qurl, output', [
(QUrl(), None),
(QUrl('http://qutebrowser.org/test.html'), 'test.html'),
(QUrl('http://qutebrowser.org/foo.html#bar'), 'foo.html'),
(QUrl('http://user:password@qutebrowser.org/foo?bar=baz#fish'), 'foo'),
(QUrl('http://qutebrowser.org/'), 'qutebrowser.org.html'),
(QUrl('qute://'), None),
])
def test_filename_from_url(qurl, output):
assert urlutils.filename_from_url(qurl) == output
@pytest.mark.parametrize('qurl, tpl', [
(QUrl(), None),
(QUrl('qute://'), None),
(QUrl('qute://foobar'), None),
(QUrl('mailto:nobody'), None),
(QUrl('ftp://example.com/'),
('ftp', 'example.com', 21)),
(QUrl('ftp://example.com:2121/'),
('ftp', 'example.com', 2121)),
(QUrl('http://qutebrowser.org:8010/waterfall'),
('http', 'qutebrowser.org', 8010)),
(QUrl('https://example.com/'),
('https', 'example.com', 443)),
(QUrl('https://example.com:4343/'),
('https', 'example.com', 4343)),
(QUrl('http://user:password@qutebrowser.org/foo?bar=baz#fish'),
('http', 'qutebrowser.org', 80)),
])
def test_host_tuple(qurl, tpl):
"""Test host_tuple().
Args:
qurl: The QUrl to pass.
tpl: The expected tuple, or None if a ValueError is expected.
"""
if tpl is None:
with pytest.raises(ValueError):
urlutils.host_tuple(qurl)
else:
assert urlutils.host_tuple(qurl) == tpl
@pytest.mark.parametrize('url, raising, has_err_string', [
(None, False, False),
(QUrl(), False, False),
(QUrl('http://www.example.com/'), True, False),
(QUrl('://'), False, True),
])
def test_fuzzy_url_error(url, raising, has_err_string):
"""Test FuzzyUrlError.
Args:
url: The URL to pass to FuzzyUrlError.
raising; True if the FuzzyUrlError should raise itself.
has_err_string: Whether the QUrl is expected to have errorString set.
"""
if raising:
expected_exc = ValueError
else:
expected_exc = urlutils.FuzzyUrlError
with pytest.raises(expected_exc) as excinfo:
raise urlutils.FuzzyUrlError("Error message", url)
if not raising:
if has_err_string:
expected_text = "Error message: " + url.errorString()
else:
expected_text = "Error message"
assert str(excinfo.value) == expected_text