summaryrefslogtreecommitdiff
path: root/subplot
diff options
context:
space:
mode:
authorLars Wirzenius <liw@liw.fi>2020-10-14 09:19:06 +0300
committerLars Wirzenius <liw@liw.fi>2020-10-14 09:19:27 +0300
commite34beeecfc807bd3afb9d5b6c2c764fd71027cde (patch)
tree935ab83af70bea67103576b8536d6c7cf59e274a /subplot
parent64a17b4927c4579d25692d15a8f0669adcf82fd2 (diff)
downloadewww-e34beeecfc807bd3afb9d5b6c2c764fd71027cde.tar.gz
refactor: move subplot files to subplot/
The root of the source tree was getting a little crowded.
Diffstat (limited to 'subplot')
-rw-r--r--subplot/daemon.py74
-rw-r--r--subplot/ewww.py106
-rw-r--r--subplot/ewww.yaml37
-rw-r--r--subplot/http.py47
-rw-r--r--subplot/runcmd.py73
-rw-r--r--subplot/runcmd.yaml13
6 files changed, 350 insertions, 0 deletions
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<filename>\S+) with "(?P<content>.*)"
+ 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<header>\S+) is "(?P<value>.+)"'
+ regex: true
+ function: http_header_is
+
+- then: 'body is "(?P<body>.*)"'
+ 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<pattern>.+)/
+ function: stdout_contains
+ regex: true
+
+- then: stderr matches /(?P<pattern>.+)/
+ function: stderr_contains
+ regex: true
+
+- then: program finished successfully
+ function: exit_code_zero