qutebrowser/tests/end2end/fixtures/webserver.py

218 lines
6.8 KiB
Python
Raw Normal View History

# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
2017-05-09 21:37:03 +02:00
# Copyright 2015-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/>.
"""Fixtures for the httpbin webserver."""
2016-01-12 22:48:38 +01:00
import re
import sys
import json
import os.path
import http.client
import pytest
from PyQt5.QtCore import pyqtSignal, QUrl
from end2end.fixtures import testprocess
2016-08-03 13:08:55 +02:00
from qutebrowser.utils import utils
class Request(testprocess.Line):
"""A parsed line from the httpbin/flask log output.
Attributes:
verb/path/status: Parsed from the log output.
"""
def __init__(self, data):
super().__init__(data)
try:
parsed = json.loads(data)
except ValueError:
raise testprocess.InvalidLine(data)
assert isinstance(parsed, dict)
assert set(parsed.keys()) == {'path', 'verb', 'status'}
self.verb = parsed['verb']
path = parsed['path']
self.path = '/' if path == '/' else path.rstrip('/')
self.status = parsed['status']
self._check_status()
def _check_status(self):
"""Check if the http status is what we expected."""
# WORKAROUND for https://github.com/PyCQA/pylint/issues/399 (?)
# pylint: disable=no-member, useless-suppression
path_to_statuses = {
'/favicon.ico': [http.client.NOT_FOUND],
'/does-not-exist': [http.client.NOT_FOUND],
'/does-not-exist-2': [http.client.NOT_FOUND],
'/status/404': [http.client.NOT_FOUND],
'/custom/redirect-later': [http.client.FOUND],
'/custom/redirect-self': [http.client.FOUND],
'/redirect-to': [http.client.FOUND],
'/cookies/set': [http.client.FOUND],
'/custom/500-inline': [http.client.INTERNAL_SERVER_ERROR],
}
for i in range(15):
path_to_statuses['/redirect/{}'.format(i)] = [http.client.FOUND]
path_to_statuses['/relative-redirect/{}'.format(i)] = [
http.client.FOUND]
path_to_statuses['/absolute-redirect/{}'.format(i)] = [
http.client.FOUND]
for suffix in ['', '1', '2', '3', '4', '5', '6']:
key = '/basic-auth/user{}/password{}'.format(suffix, suffix)
path_to_statuses[key] = [http.client.UNAUTHORIZED, http.client.OK]
default_statuses = [http.client.OK, http.client.NOT_MODIFIED]
sanitized = QUrl('http://localhost' + self.path).path() # Remove ?foo
expected_statuses = path_to_statuses.get(sanitized, default_statuses)
if self.status not in expected_statuses:
raise AssertionError(
"{} loaded with status {} but expected {}".format(
sanitized, self.status,
' / '.join(repr(e) for e in expected_statuses)))
def __eq__(self, other):
return NotImplemented
class ExpectedRequest:
"""Class to compare expected requests easily."""
def __init__(self, verb, path):
self.verb = verb
self.path = path
2015-11-18 19:56:49 +01:00
@classmethod
def from_request(cls, request):
"""Create an ExpectedRequest from a Request."""
return cls(request.verb, request.path)
def __eq__(self, other):
if isinstance(other, (Request, ExpectedRequest)):
2016-04-27 18:30:54 +02:00
return self.verb == other.verb and self.path == other.path
else:
return NotImplemented
def __hash__(self):
return hash(('ExpectedRequest', self.verb, self.path))
2015-11-18 19:56:49 +01:00
2015-11-21 00:10:49 +01:00
def __repr__(self):
return ('ExpectedRequest(verb={!r}, path={!r})'
.format(self.verb, self.path))
2016-01-12 22:48:38 +01:00
class WebserverProcess(testprocess.Process):
"""Abstraction over a running HTTPbin server process.
Reads the log from its stdout and parses it.
Signals:
new_request: Emitted when there's a new request received.
"""
new_request = pyqtSignal(Request)
Request = Request # So it can be used from the fixture easily.
ExpectedRequest = ExpectedRequest
KEYS = ['verb', 'path']
2016-01-12 22:48:38 +01:00
def __init__(self, script, parent=None):
super().__init__(parent)
2016-01-12 22:48:38 +01:00
self._script = script
2016-08-03 13:08:55 +02:00
self.port = utils.random_port()
self.new_data.connect(self.new_request)
def get_requests(self):
"""Get the requests to the server during this test."""
2015-10-13 07:11:02 +02:00
requests = self._get_data()
return [r for r in requests if r.path != '/favicon.ico']
def _parse_line(self, line):
self._log(line)
2016-01-12 22:48:38 +01:00
started_re = re.compile(r' \* Running on https?://127\.0\.0\.1:{}/ '
r'\(Press CTRL\+C to quit\)'.format(self.port))
if started_re.fullmatch(line):
self.ready.emit()
return None
return Request(line)
def _executable_args(self):
if hasattr(sys, 'frozen'):
executable = os.path.join(os.path.dirname(sys.executable),
2016-01-12 22:48:38 +01:00
self._script)
args = []
else:
executable = sys.executable
py_file = os.path.join(os.path.dirname(__file__),
2016-01-12 22:48:38 +01:00
self._script + '.py')
args = [py_file]
return executable, args
def _default_args(self):
return [str(self.port)]
def cleanup(self):
"""Clean up and shut down the process."""
self.proc.terminate()
self.proc.waitForFinished()
@pytest.fixture(scope='session', autouse=True)
def httpbin(qapp):
"""Fixture for an httpbin object which ensures clean setup/teardown."""
2016-01-12 22:48:38 +01:00
httpbin = WebserverProcess('webserver_sub')
httpbin.start()
yield httpbin
httpbin.cleanup()
@pytest.fixture(autouse=True)
def httpbin_after_test(httpbin, request):
"""Fixture to clean httpbin request list after each test."""
request.node._httpbin_log = httpbin.captured_log
yield
httpbin.after_test()
2016-01-12 22:48:38 +01:00
@pytest.fixture
2016-01-12 22:48:38 +01:00
def ssl_server(request, qapp):
"""Fixture for a webserver with a self-signed SSL certificate.
This needs to be explicitly used in a test, and overwrites the httpbin log
used in that test.
"""
server = WebserverProcess('webserver_sub_ssl')
request.node._httpbin_log = server.captured_log
server.start()
yield server
server.after_test()
server.cleanup()