From e34beeecfc807bd3afb9d5b6c2c764fd71027cde Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Wed, 14 Oct 2020 09:19:06 +0300 Subject: refactor: move subplot files to subplot/ The root of the source tree was getting a little crowded. --- subplot/daemon.py | 74 ++++++++++++++++++++++++++++++++++++ subplot/ewww.py | 106 ++++++++++++++++++++++++++++++++++++++++++++++++++++ subplot/ewww.yaml | 37 ++++++++++++++++++ subplot/http.py | 47 +++++++++++++++++++++++ subplot/runcmd.py | 73 ++++++++++++++++++++++++++++++++++++ subplot/runcmd.yaml | 13 +++++++ 6 files changed, 350 insertions(+) create mode 100644 subplot/daemon.py create mode 100644 subplot/ewww.py create mode 100644 subplot/ewww.yaml create mode 100644 subplot/http.py create mode 100644 subplot/runcmd.py create mode 100644 subplot/runcmd.yaml (limited to 'subplot') diff --git a/subplot/daemon.py b/subplot/daemon.py new file mode 100644 index 0000000..585fe5a --- /dev/null +++ b/subplot/daemon.py @@ -0,0 +1,74 @@ +############################################################################# +# Start and stop daemons, or background processes. + + +import logging +import os +import signal + + +# Start a process in the background. +def start_daemon(ctx, name, argv): + logging.debug(f"Starting daemon {name}") + logging.debug(f" ctx={ctx.as_dict()}") + logging.debug(f" name={name}") + logging.debug(f" argv={argv}") + + if "daemon" not in ctx.as_dict(): + ctx["daemon"] = {} + assert name not in ctx["daemon"] + this = ctx["daemon"][name] = { + "pid-file": f"{name}.pid", + "stderr": f"{name}.stderr", + "stdout": f"{name}.stdout", + } + runcmd( + ctx, + [ + "/usr/sbin/daemonize", + "-c", + os.getcwd(), + "-p", + this["pid-file"], + "-e", + this["stderr"], + "-o", + this["stdout"], + ] + + argv, + ) + exit_code_is(ctx, 0) + this["pid"] = int(open("ewww.pid").read().strip()) + assert process_exists(this["pid"]) + + logging.debug(f"Started daemon {name}") + logging.debug(f" ctx={ctx.as_dict()}") + + +# Stop a daemon. +def stop_daemon(ctx, name): + logging.debug(f"Stopping daemon {name}") + logging.debug(f" ctx={ctx.as_dict()}") + logging.debug(f" ctx['daemon']={ctx.as_dict()['daemon']}") + + this = ctx["daemon"][name] + terminate_process(this["pid"], signal.SIGKILL) + + +# Does a process exist? +def process_exists(pid): + try: + os.kill(pid, 0) + except ProcessLookupError: + return False + return True + + +# Terminate process. +def terminate_process(pid, signalno): + logging.debug(f"Terminating process {pid} with signal {signalno}") + try: + os.kill(pid, signalno) + except ProcessLookupError: + logging.debug("Process did not actually exist (anymore?)") + pass diff --git a/subplot/ewww.py b/subplot/ewww.py new file mode 100644 index 0000000..9282d64 --- /dev/null +++ b/subplot/ewww.py @@ -0,0 +1,106 @@ +############################################################################# +# Some helpers to make step functions simpler. + +import json +import logging +import os +import random +import re +import shutil +import signal +import socket +import subprocess +import time +import urllib.parse + +import yaml + + +# Name of Rust binary, debug-build. +def _binary(name): + return os.path.abspath(os.path.join(srcdir, "target", "debug", name)) + + +# Write a file with given content. +def _write(filename, content): + open(filename, "w").write(content) + + +# Construct a URL that points to server running on localhost by +# replacing the actual scheme and host with ones that work for test. +def _url(ctx, url): + port = ctx["config"]["port"] + c = urllib.parse.urlparse(url) + host = c[1] + c = (c[0], "localhost:{}".format(port)) + c[2:] + return urllib.parse.urlunparse(c), host + + +############################################################################# +# The actual step functions. + + +# Fail: use this for unimplemented steps. +def fixme(*args, **kwargs): + assert 0 + + +# Create a file. +def create_file(ctx, filename=None, content=None): + logging.debug(f"Creating file {filename} with {content}") + dirname = os.path.dirname(filename) + os.makedirs(dirname) + _write(filename, content) + + +# Copy test certificate from source tree, where it's been created previously by +# ./check. +def copy_test_certificate(ctx, cert=None, key=None): + logging.debug(f"Copying test.pem, test.key from srcdir to {cert} and {key}") + shutil.copy(os.path.join(srcdir, "test.pem"), cert) + shutil.copy(os.path.join(srcdir, "test.key"), key) + + +# Start server using named configuration file. +def start_server(ctx, filename=None): + logging.debug(f"Starting ewww with config file {filename}") + config = get_file(filename).decode("UTF-8") + config = yaml.safe_load(config) + port = config["port"] = random.randint(2000, 30000) + logging.debug(f"Picked randomly port for ewww: {config['port']}") + ctx["config"] = config + config = yaml.safe_dump(config) + _write(filename, config) + + start_daemon(ctx, "ewww", [_binary("ewww"), filename]) + + if not port_open("localhost", port, 5.0): + stderr = open(ctx["daemon"]["ewww"]["stderr"]).read() + logging.debug(f"Stderr from daemon: {stderr!r}") + + +# Wait for a port to be open +def port_open(host, port, timeout): + logging.debug(f"Waiting for port localhost:{port} to be available") + started = time.time() + while time.time() < started + timeout: + try: + socket.create_connection((host, port), timeout=timeout) + return True + except socket.error: + pass + logging.error(f"Port localhost:{port} is not open") + return False + + +# Stop previously started server. +def stop_server(ctx): + logging.debug("Stopping ewww") + stop_daemon(ctx, "ewww") + + +# Make an HTTP request. +def request(ctx, method=None, url=None): + logging.debug(f"Making HTTP request to ewww: {method} {url}") + url, host = _url(ctx, url) + http_request(ctx, host=host, method=method, url=url) diff --git a/subplot/ewww.yaml b/subplot/ewww.yaml new file mode 100644 index 0000000..375558d --- /dev/null +++ b/subplot/ewww.yaml @@ -0,0 +1,37 @@ +- given: a self-signed certificate as {cert}, using key {key} + function: copy_test_certificate + +- given: a running server using config file {filename} + function: start_server + cleanup: stop_server + +- given: "{count} files in {dirname}" + function: fixme + +- given: file (?P\S+) with "(?P.*)" + regex: true + function: create_file + +- then: I am redirected to {location} + function: fixme + +- then: I can do at least {number} requests per second + function: fixme + +- then: I get status code {code} + function: http_status_code_is + +- then: 'header (?P
\S+) is "(?P.+)"' + regex: true + function: http_header_is + +- then: 'body is "(?P.*)"' + regex: true + function: http_body_is + +- when: I request {method} {url} + function: request + +- when: I request files under {url} in random order {count} times + function: fixme + diff --git a/subplot/http.py b/subplot/http.py new file mode 100644 index 0000000..5cff887 --- /dev/null +++ b/subplot/http.py @@ -0,0 +1,47 @@ +############################################################################# +# Some helpers to make HTTP requests and examine responses + +import json +import logging +import os +import random +import re +import shutil +import signal +import subprocess +import time +import urllib.parse + +import yaml + + +# Make an HTTP request. +def http_request(ctx, host=None, method=None, url=None): + logging.debug(f"Make HTTP request: {method} {url}") + runcmd(ctx, ["curl", "-ksv", "-X", method, f"-HHost: {host}", url]) + exit_code_is(ctx, 0) + + +# Check status code of latest HTTP request. +def http_status_code_is(ctx, code=None): + logging.debug(f"Verifying status code of previous HTTP request is {code}") + logging.debug(f" stderr={ctx['stderr']}") + pattern = f"\n< HTTP/2 {code} " + assert_eq(pattern in ctx["stderr"], True) + + +# Check a HTTP response header for latest request has a given value. +def http_header_is(ctx, header=None, value=None): + logging.debug(f"Verifying response has header {header}: {value}") + s = ctx["stderr"] + pattern = f"\n< {header}: {value}" + assert_eq(pattern in s, True) + + +# Check a HTTP body response for latest request has a given value. +def http_body_is(ctx, body=None): + logging.debug(f"Verifying response body is {body!r}") + logging.debug(f" actual body={ctx['stdout']!r}") + s = ctx["stdout"] + body = body.encode("UTF8").decode("unicode-escape") + assert_eq(body, s) diff --git a/subplot/runcmd.py b/subplot/runcmd.py new file mode 100644 index 0000000..631d8c0 --- /dev/null +++ b/subplot/runcmd.py @@ -0,0 +1,73 @@ +# Some step implementations for running commands and capturing the result. + +import logging +import os +import subprocess + + +# Run a command, capture its stdout, stderr, and exit code in context. +def runcmd(ctx, argv, **kwargs): + logging.debug(f"Running program") + logging.debug(f" cwd={os.getcwd()}") + logging.debug(f" argv={argv}") + logging.debug(f" kwargs={argv}") + p = subprocess.Popen(argv, stdout=subprocess.PIPE, stderr=subprocess.PIPE, **kwargs) + stdout, stderr = p.communicate("") + ctx["argv"] = argv + ctx["stdout"] = stdout.decode("utf-8") + ctx["stderr"] = stderr.decode("utf-8") + ctx["exit"] = p.returncode + + +# Check that latest exit code captured by runcmd was a specific one. +def exit_code_is(ctx, wanted): + logging.debug(f"Verifying exit code is {wanted} (it is {ctx.get('exit')})") + assert_eq(ctx.get("exit"), wanted) + + +# Check that latest exit code captured by runcmd was not a specific one. +def exit_code_is_not(ctx, unwanted): + logging.debug(f"Verifying exit code is NOT {unwanted} (it is {ctx.get('exit')})") + assert_ne(ctx.get("exit"), wanted) + + +# Check that latest exit code captured by runcmd was zero. +def exit_code_zero(ctx): + exit_code_is(ctx, 0) + + +# Check that latest exit code captured by runcmd was not zero. +def exit_code_nonzero(ctx): + exit_code_is_not(ctx, 0) + + +# Check that stdout of latest runcmd contains a specific string. +def stdout_contains(ctx, pattern=None): + logging.debug(f"Verifying stdout contains {pattern}") + logging.debug(f" stdout is {ctx.get('stdout', '')})") + stdout = ctx.get("stdout", "") + assert_eq(pattern in stdout, True) + + +# Check that stdout of latest runcmd does not contain a specific string. +def stdout_does_not_contain(ctx, pattern=None): + logging.debug(f"Verifying stdout does NOT contain {pattern}") + logging.debug(f" stdout is {ctx.get('stdout', '')})") + stdout = ctx.get("stdout", "") + assert_eq(pattern not in stdout, True) + + +# Check that stderr of latest runcmd does contains a specific string. +def stderr_contains(ctx, pattern=None): + logging.debug(f"Verifying stderr contains {pattern}") + logging.debug(f" stderr is {ctx.get('stderr', '')})") + stderr = ctx.get("stderr", "") + assert_eq(pattern in stderr, True) + + +# Check that stderr of latest runcmd does not contain a specific string. +def stderr_does_not_contain(ctx, pattern=None): + logging.debug(f"Verifying stderr does NOT contain {pattern}") + logging.debug(f" stderr is {ctx.get('stderr', '')})") + stderr = ctx.get("stderr", "") + assert_eq(pattern not in stderr, True) diff --git a/subplot/runcmd.yaml b/subplot/runcmd.yaml new file mode 100644 index 0000000..02e5ee1 --- /dev/null +++ b/subplot/runcmd.yaml @@ -0,0 +1,13 @@ +- then: exit code is non-zero + function: exit_code_nonzero + +- then: output matches /(?P.+)/ + function: stdout_contains + regex: true + +- then: stderr matches /(?P.+)/ + function: stderr_contains + regex: true + +- then: program finished successfully + function: exit_code_zero -- cgit v1.2.1