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': elif autosearch == 'naive':
log.url.debug("Checking via naive check") log.url.debug("Checking via naive check")
url = _is_url_naive(urlstr) url = _is_url_naive(urlstr)
else: else: # pragma: no cover
raise ValueError("Invalid autosearch value") raise ValueError("Invalid autosearch value")
log.url.debug("url = {}".format(url)) log.url.debug("url = {}".format(url))
return url return url

View File

@ -28,7 +28,7 @@ import pytest
import stubs as stubsmod import stubs as stubsmod
from qutebrowser.config import configexc from qutebrowser.config import configexc
from qutebrowser.utils import objreg from qutebrowser.utils import objreg, usertypes
@pytest.fixture(scope='session', autouse=True) @pytest.fixture(scope='session', autouse=True)
@ -217,3 +217,58 @@ def pytest_runtest_setup(item):
pytest.skip("Can't be run when frozen!") pytest.skip("Can't be run when frozen!")
elif item.get_marker('frozen') and not getattr(sys, 'frozen', False): elif item.get_marker('frozen') and not getattr(sys, 'frozen', False):
pytest.skip("Can only run when frozen!") 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.""" """Tests for qutebrowser.utils.urlutils."""
import os.path
import collections
from PyQt5.QtCore import QUrl from PyQt5.QtCore import QUrl
import pytest 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. """Initialize the given config_stub.
Args: Args:
stub: The ConfigStub provided by the config_stub fixture. stub: The ConfigStub provided by the config_stub fixture.
auto_search: The value auto-search should have. auto_search: The value auto-search should have.
""" """
stub.data = { config_stub.data = {
'general': {'auto-search': auto_search}, 'general': {'auto-search': True},
'searchengines': { 'searchengines': {
'test': 'http://www.qutebrowser.org/?q={}', 'test': 'http://www.qutebrowser.org/?q={}',
'DEFAULT': 'http://www.example.com/?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: class TestFuzzyUrl:
SPECIAL_URLS: URLs which are special.
NORMAL_URLS: URLs which are not special. """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.
""" """
url = urlutils._get_search_url(url)
SPECIAL_URLS = ( assert url.host() == host
'file:///tmp/foo', assert url.query() == query
'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)
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.""" Args:
is_url: Whether the given string is an URL with auto-search dns/naive.
@pytest.fixture(autouse=True) is_url_no_autosearch: Whether the given string is an URL with
def mock_config(self, config_stub, monkeypatch): auto-search false.
"""Fixture to patch urlutils.config with a stub.""" uses_dns: Whether the given string should fire a DNS request for the
init_config_stub(config_stub) given URL.
monkeypatch.setattr('qutebrowser.utils.urlutils.config', config_stub) url: The URL to test, as a string.
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.
""" """
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 = ( fake_dns.answer = False
'http://foobar', result = urlutils.is_url(url)
'localhost:8080', assert fake_dns.used
'qutebrowser.org', assert not result
' qutebrowser.org ', else:
'127.0.0.1', result = urlutils.is_url(url)
'::1', assert not fake_dns.used
'2001:41d0:2:6c11::1', assert result == is_url
'94.23.233.17',
'http://user:password@qutebrowser.org/foo?bar=baz#fish',
)
NOT_URLS = ( fake_dns.reset()
'foo bar', urlutils_config_stub.data['general']['auto-search'] = 'naive'
'localhost test', assert urlutils.is_url(url) == is_url
'another . test', assert not fake_dns.used
'foo',
'this is: not an URL',
'23.42',
'1337',
'deadbeef',
'31c3',
'http:foo:0',
'foo::bar',
)
@pytest.mark.parametrize('url', URLS) fake_dns.reset()
def test_urls(self, monkeypatch, config_stub, url): urlutils_config_stub.data['general']['auto-search'] = False
"""Test things which are URLs.""" assert urlutils.is_url(url) == is_url_no_autosearch
init_config_stub(config_stub, 'naive') assert not fake_dns.used
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')
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.""" Args:
user_input: The string to pass to qurl_from_user_input.
def test_url(self): output: The expected QUrl string.
"""Test a normal URL.""" """
url = urlutils.qurl_from_user_input('qutebrowser.org') url = urlutils.qurl_from_user_input(user_input)
assert url.toString() == 'http://qutebrowser.org' assert url.toString() == output
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]'
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): msg = urlutils_message_mock.getmsg()
"""Test with an invalid QUrl.""" assert msg.win_id == 0
assert urlutils.filename_from_url(QUrl()) is None 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): @pytest.mark.parametrize('url, valid, has_err_string', [
"""Test with an URL with no path.""" ('http://www.example.com/', True, False),
url = QUrl('http://qutebrowser.org/') ('', False, False),
assert urlutils.filename_from_url(url) == 'qutebrowser.org.html' ('://', 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