qutebrowser/tests/end2end/fixtures/webserver.py

207 lines
6.4 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/>.
2017-09-19 10:35:54 +02:00
"""Fixtures for the server webserver."""
2016-01-12 22:48:38 +01:00
import re
import sys
import json
import os.path
import http.client
2017-09-19 22:18:02 +02:00
import attr
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):
2017-09-19 10:35:54 +02:00
"""A parsed line from the 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
path_to_statuses = {
'/favicon.ico': [http.client.NOT_FOUND],
'/does-not-exist': [http.client.NOT_FOUND],
'/does-not-exist-2': [http.client.NOT_FOUND],
2017-09-19 10:35:54 +02:00
'/404': [http.client.NOT_FOUND],
2017-09-19 10:35:54 +02:00
'/redirect-later': [http.client.FOUND],
'/redirect-self': [http.client.FOUND],
'/redirect-to': [http.client.FOUND],
2017-09-19 10:35:54 +02:00
'/relative-redirect': [http.client.FOUND],
'/absolute-redirect': [http.client.FOUND],
'/cookies/set': [http.client.FOUND],
2017-09-19 10:35:54 +02:00
'/500-inline': [http.client.INTERNAL_SERVER_ERROR],
}
for i in range(15):
path_to_statuses['/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]
2017-10-22 22:16:02 +02:00
# pylint: enable=no-member
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
2017-09-19 22:18:02 +02:00
@attr.s(frozen=True, cmp=False, hash=True)
class ExpectedRequest:
"""Class to compare expected requests easily."""
2017-09-19 22:18:02 +02:00
verb = attr.ib()
path = attr.ib()
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
2016-01-12 22:48:38 +01:00
class WebserverProcess(testprocess.Process):
2017-09-19 10:35:54 +02:00
"""Abstraction over a running Flask 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)]
@pytest.fixture(scope='session', autouse=True)
2017-09-19 10:35:54 +02:00
def server(qapp):
"""Fixture for an server object which ensures clean setup/teardown."""
server = WebserverProcess('webserver_sub')
server.start()
yield server
server.terminate()
@pytest.fixture(autouse=True)
def server_per_test(server, request):
2017-09-19 10:35:54 +02:00
"""Fixture to clean server request list after each test."""
request.node._server_log = server.captured_log
server.before_test()
yield
2017-09-19 10:35:54 +02:00
server.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.
2017-09-19 10:35:54 +02:00
This needs to be explicitly used in a test, and overwrites the server log
2016-01-12 22:48:38 +01:00
used in that test.
"""
server = WebserverProcess('webserver_sub_ssl')
2017-09-19 10:35:54 +02:00
request.node._server_log = server.captured_log
2016-01-12 22:48:38 +01:00
server.start()
yield server
server.after_test()
server.terminate()