From 3f13c2bd3ee97aafabb2d4ccecc932b728b8ef34 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 6 Jul 2015 17:10:57 +0200 Subject: [PATCH] Add/improve tests for qutebrowser.utils.urlutils. --- qutebrowser/utils/urlutils.py | 2 +- tests/conftest.py | 57 +++- tests/utils/test_urlutils.py | 607 ++++++++++++++++++++++++---------- 3 files changed, 495 insertions(+), 171 deletions(-) diff --git a/qutebrowser/utils/urlutils.py b/qutebrowser/utils/urlutils.py index 5f17f4389..7d60a41e1 100644 --- a/qutebrowser/utils/urlutils.py +++ b/qutebrowser/utils/urlutils.py @@ -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 diff --git a/tests/conftest.py b/tests/conftest.py index 67e960102..4d8e6845b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -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) diff --git a/tests/utils/test_urlutils.py b/tests/utils/test_urlutils.py index 264de925b..662d00539 100644 --- a/tests/utils/test_urlutils.py +++ b/tests/utils/test_urlutils.py @@ -21,209 +21,478 @@ """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') + + 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