Signed-off-by: Andrew Gregory <andrew.gregory.8@gmail.com> --- test/pacman/README | 23 ++++++++++++ test/pacman/pmserve.py | 84 ++++++++++++++++++++++++++++++++++++++++++ test/pacman/pmtest.py | 28 ++++++++++++++ 3 files changed, 135 insertions(+) create mode 100644 test/pacman/pmserve.py diff --git a/test/pacman/README b/test/pacman/README index 224acd84..61a67d05 100644 --- a/test/pacman/README +++ b/test/pacman/README @@ -317,3 +317,26 @@ Example: pactest will ensure the file /etc/test.conf exists in the filesystem. +Serving Files +============= + +Tests can run a simple http server using the `add_simple_http_server` method, +which takes a dict with request paths for keys and their responses as values +and returns a url for the server. Responses may either be a simple string or +a dict with the following keys: `code`, `headers`, and `body`. If a value is +provided for the empty path it will be used as a fallback response for any +requests that do not match a path. + + url = self.add_simple_http_server({ + "/": "simple response", + "/custom": { + "headers": { "Content-Disposition": "attachment; filename=foo" }, + "body": ("Custom response. Code and any necessary headers " + "will by automatically set if not provided"), + } + "": { + "code": 404, + "headers": { "Content-Length": "14" }, + "body": "Page Not Found", + } + }) diff --git a/test/pacman/pmserve.py b/test/pacman/pmserve.py new file mode 100644 index 00000000..0ca34093 --- /dev/null +++ b/test/pacman/pmserve.py @@ -0,0 +1,84 @@ +# Copyright (c) 2020 Pacman Development Team <pacman-dev@archlinux.org> +# +# This program 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 2 of the License, or +# (at your option) any later version. +# +# This program 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 this program. If not, see <http://www.gnu.org/licenses/>. + +import http +import http.server +import sys +import re + +class pmHTTPServer(http.server.ThreadingHTTPServer): + pass + +class pmHTTPRequestHandler(http.server.BaseHTTPRequestHandler): + """BaseHTTPRequestHandler subclass with helper methods and common setup""" + + logfile = sys.stderr + + def respond(self, response, headers={}, code=200): + self.protocol_version = "HTTP/1.1" + self.send_response(code) + for header, value in headers.items(): + self.send_header(header, value) + self.end_headers() + self.wfile.write(response) + + def parse_range_bytes(self, text): + parser = re.compile(r'^bytes=(\d+)-(\d+)?$') + if m := parser.match(text): + return map(lambda d: None if d is None else int(d), m.groups()) + else: + raise ValueError("Unrecognized Range value") + + def respond_bytes(self, response, headers={}, code=200): + headers = headers.copy() + if code == 200 and self.headers['Range']: + (start, end) = self.parse_range_bytes(self.headers['Range']) + code = 206 + response = response[start:end] + headers.setdefault('Content-Range', 'bytes */%s' % (len(response))) + headers.setdefault('Content-Type', "application/octet-stream") + headers.setdefault('Content-Length', str(len(response))) + self.respond(response, headers, code) + + def respond_string(self, response, headers={}, code=200): + headers = headers.copy() + headers.setdefault('Content-Type', 'text/plain; charset=utf-8') + self.respond_bytes(response.encode('UTF-8'), headers, code) + + def log_message(self, format, *args): + if callable(self.logfile): + self.logfile = self.logfile() + self.logfile.write("%s - - [%s] %s\n" % + (self.address_string(), + self.log_date_time_string(), + format%args)) + +class pmStringHTTPRequestHandler(pmHTTPRequestHandler): + """pmHTTPRequestHandler subclass to respond with simple string messages""" + + responses = dict() + + def do_GET(self): + response = self.responses.get(self.path, self.responses.get('')) + if response is not None: + if isinstance(response, dict): + self.respond_string( + response.get('body', ''), + headers=response.get('headers', {}), + code=response.get('code', 200)) + else: + self.respond_string(response) + else: + self.send_error(http.HTTPStatus.NOT_FOUND); diff --git a/test/pacman/pmtest.py b/test/pacman/pmtest.py index 8669f31d..7e62fc92 100644 --- a/test/pacman/pmtest.py +++ b/test/pacman/pmtest.py @@ -20,9 +20,11 @@ import shutil import stat import subprocess +import threading import time import pmrule +import pmserve import pmdb import pmfile import tap @@ -47,6 +49,8 @@ def __init__(self, name, root, config): "--hookdir", self.hookdir(), "--cachedir", self.cachedir()] + self.http_servers = [] + def __str__(self): return "name = %s\n" \ "testname = %s\n" \ @@ -285,6 +289,8 @@ def run(self, pacman): output = None vprint("\trunning: %s" % " ".join(cmd)) + self.start_http_servers() + # Change to the tmp dir before running pacman, so that local package # archives are made available more easily. time_start = time.time() @@ -293,6 +299,8 @@ def run(self, pacman): time_end = time.time() vprint("\ttime elapsed: %.2fs" % (time_end - time_start)) + self.stop_http_servers() + if output: output.close() @@ -330,3 +338,23 @@ def cachedir(self): def hookdir(self): return os.path.join(self.root, util.PM_HOOKDIR) + + def add_simple_http_server(self, responses): + logfile = lambda h: open(os.path.join(self.root, 'var/log/httpd.log'), 'a') + handler = type(self.name + 'HTTPServer', + (pmserve.pmStringHTTPRequestHandler,), + {'responses': responses, 'logfile': logfile}) + server = pmserve.pmHTTPServer(('127.0.0.1', 0), handler) + self.http_servers.append(server) + host, port = server.server_address[:2] + return 'http://%s:%d' % (host, port) + + def start_http_servers(self): + for srv in self.http_servers: + thread = threading.Thread(target=srv.serve_forever) + thread.daemon = True + thread.start() + + def stop_http_servers(self): + for srv in self.http_servers: + srv.shutdown() -- 2.30.0