pmtest: add ability to spawn simple http servers

Signed-off-by: Andrew Gregory <andrew.gregory.8@gmail.com>
Signed-off-by: Allan McRae <allan@archlinux.org>
This commit is contained in:
Andrew Gregory 2021-01-19 10:30:09 -08:00 committed by Allan McRae
parent ad84a572aa
commit b82a975e76
3 changed files with 135 additions and 0 deletions

View file

@ -317,3 +317,26 @@ Example:
pactest will ensure the file /etc/test.conf exists in the filesystem. 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",
}
})

84
test/pacman/pmserve.py Normal file
View file

@ -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);

View file

@ -20,9 +20,11 @@ import shlex
import shutil import shutil
import stat import stat
import subprocess import subprocess
import threading
import time import time
import pmrule import pmrule
import pmserve
import pmdb import pmdb
import pmfile import pmfile
import tap import tap
@ -47,6 +49,8 @@ class pmtest(object):
"--hookdir", self.hookdir(), "--hookdir", self.hookdir(),
"--cachedir", self.cachedir()] "--cachedir", self.cachedir()]
self.http_servers = []
def __str__(self): def __str__(self):
return "name = %s\n" \ return "name = %s\n" \
"testname = %s\n" \ "testname = %s\n" \
@ -285,6 +289,8 @@ class pmtest(object):
output = None output = None
vprint("\trunning: %s" % " ".join(cmd)) vprint("\trunning: %s" % " ".join(cmd))
self.start_http_servers()
# Change to the tmp dir before running pacman, so that local package # Change to the tmp dir before running pacman, so that local package
# archives are made available more easily. # archives are made available more easily.
time_start = time.time() time_start = time.time()
@ -293,6 +299,8 @@ class pmtest(object):
time_end = time.time() time_end = time.time()
vprint("\ttime elapsed: %.2fs" % (time_end - time_start)) vprint("\ttime elapsed: %.2fs" % (time_end - time_start))
self.stop_http_servers()
if output: if output:
output.close() output.close()
@ -330,3 +338,23 @@ class pmtest(object):
def hookdir(self): def hookdir(self):
return os.path.join(self.root, util.PM_HOOKDIR) 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()