diff options
-rwxr-xr-x | check | 5 | ||||
-rw-r--r-- | ewww.md | 12 | ||||
-rw-r--r-- | runcmd.py | 73 | ||||
-rw-r--r-- | runcmd.yaml | 13 | ||||
-rw-r--r-- | subplot/daemon.py (renamed from daemon.py) | 7 | ||||
-rw-r--r-- | subplot/ewww.py (renamed from ewww.py) | 10 | ||||
-rw-r--r-- | subplot/ewww.yaml (renamed from ewww.yaml) | 0 | ||||
-rw-r--r-- | subplot/http.py (renamed from http.py) | 38 | ||||
-rw-r--r-- | subplot/runcmd.py | 243 | ||||
-rw-r--r-- | subplot/runcmd.yaml | 83 |
10 files changed, 365 insertions, 119 deletions
@@ -51,7 +51,10 @@ then cargo clippy $quiet fi $hideok cargo test $quiet -$hideok cargo fmt -- --check +if cargo fmt --help > /dev/null 2> /dev/null +then + $hideok cargo fmt -- --check +fi $hideok ./mktestcert test.key test.pem hunter2 @@ -144,13 +144,13 @@ and allow is "GET HEAD" title: "Ewww — a Web server for static sites" author: Lars Wirzenius bindings: - - ewww.yaml - - runcmd.yaml + - subplot/ewww.yaml + - subplot/runcmd.yaml functions: - - ewww.py - - daemon.py - - http.py - - runcmd.py + - subplot/ewww.py + - subplot/daemon.py + - subplot/http.py + - subplot/runcmd.py classes: - scenario-disabled ... diff --git a/runcmd.py b/runcmd.py deleted file mode 100644 index 631d8c0..0000000 --- a/runcmd.py +++ /dev/null @@ -1,73 +0,0 @@ -# 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/runcmd.yaml b/runcmd.yaml deleted file mode 100644 index 02e5ee1..0000000 --- a/runcmd.yaml +++ /dev/null @@ -1,13 +0,0 @@ -- 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 diff --git a/daemon.py b/subplot/daemon.py index 585fe5a..00c9d2d 100644 --- a/daemon.py +++ b/subplot/daemon.py @@ -9,6 +9,9 @@ import signal # Start a process in the background. def start_daemon(ctx, name, argv): + runcmd_run = globals()["runcmd_run"] + runcmd_exit_code_is = globals()["runcmd_exit_code_is"] + logging.debug(f"Starting daemon {name}") logging.debug(f" ctx={ctx.as_dict()}") logging.debug(f" name={name}") @@ -22,7 +25,7 @@ def start_daemon(ctx, name, argv): "stderr": f"{name}.stderr", "stdout": f"{name}.stdout", } - runcmd( + runcmd_run( ctx, [ "/usr/sbin/daemonize", @@ -37,7 +40,7 @@ def start_daemon(ctx, name, argv): ] + argv, ) - exit_code_is(ctx, 0) + runcmd_exit_code_is(ctx, 0) this["pid"] = int(open("ewww.pid").read().strip()) assert process_exists(this["pid"]) diff --git a/ewww.py b/subplot/ewww.py index 9282d64..43e5946 100644 --- a/ewww.py +++ b/subplot/ewww.py @@ -1,15 +1,11 @@ ############################################################################# # 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 @@ -18,6 +14,7 @@ import yaml # Name of Rust binary, debug-build. def _binary(name): + srcdir = globals()["srcdir"] return os.path.abspath(os.path.join(srcdir, "target", "debug", name)) @@ -56,6 +53,7 @@ def create_file(ctx, filename=None, content=None): # Copy test certificate from source tree, where it's been created previously by # ./check. def copy_test_certificate(ctx, cert=None, key=None): + srcdir = globals()["srcdir"] 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) @@ -63,6 +61,8 @@ def copy_test_certificate(ctx, cert=None, key=None): # Start server using named configuration file. def start_server(ctx, filename=None): + get_file = globals()["get_file"] + start_daemon = globals()["start_daemon"] logging.debug(f"Starting ewww with config file {filename}") config = get_file(filename).decode("UTF-8") config = yaml.safe_load(config) @@ -95,12 +95,14 @@ def port_open(host, port, timeout): # Stop previously started server. def stop_server(ctx): + stop_daemon = globals()["stop_daemon"] logging.debug("Stopping ewww") stop_daemon(ctx, "ewww") # Make an HTTP request. def request(ctx, method=None, url=None): + http_request = globals()["http_request"] 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/ewww.yaml b/subplot/ewww.yaml index 375558d..375558d 100644 --- a/ewww.yaml +++ b/subplot/ewww.yaml diff --git a/http.py b/subplot/http.py index 5cff887..cfe8968 100644 --- a/http.py +++ b/subplot/http.py @@ -1,47 +1,45 @@ ############################################################################# # 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): + runcmd_run = globals()["runcmd_run"] + runcmd_exit_code_is = globals()["runcmd_exit_code_is"] logging.debug(f"Make HTTP request: {method} {url}") - runcmd(ctx, ["curl", "-ksv", "-X", method, f"-HHost: {host}", url]) - exit_code_is(ctx, 0) + runcmd_run(ctx, ["curl", "-ksv", "-X", method, f"-HHost: {host}", url]) + runcmd_exit_code_is(ctx, 0) # Check status code of latest HTTP request. def http_status_code_is(ctx, code=None): + assert_eq = globals()["assert_eq"] + runcmd_get_stderr = globals()["runcmd_get_stderr"] + stderr = runcmd_get_stderr(ctx) logging.debug(f"Verifying status code of previous HTTP request is {code}") - logging.debug(f" stderr={ctx['stderr']}") + logging.debug(f" stderr={stderr}") pattern = f"\n< HTTP/2 {code} " - assert_eq(pattern in ctx["stderr"], True) + assert_eq(pattern in stderr, True) # Check a HTTP response header for latest request has a given value. def http_header_is(ctx, header=None, value=None): + assert_eq = globals()["assert_eq"] + runcmd_get_stderr = globals()["runcmd_get_stderr"] + stderr = runcmd_get_stderr(ctx) logging.debug(f"Verifying response has header {header}: {value}") - s = ctx["stderr"] pattern = f"\n< {header}: {value}" - assert_eq(pattern in s, True) + assert_eq(pattern in stderr, True) # Check a HTTP body response for latest request has a given value. def http_body_is(ctx, body=None): + assert_eq = globals()["assert_eq"] + runcmd_get_stdout = globals()["runcmd_get_stdout"] + stdout = runcmd_get_stdout(ctx) logging.debug(f"Verifying response body is {body!r}") - logging.debug(f" actual body={ctx['stdout']!r}") - s = ctx["stdout"] + logging.debug(f" actual body={stdout!r}") body = body.encode("UTF8").decode("unicode-escape") - assert_eq(body, s) + assert_eq(body, stdout) diff --git a/subplot/runcmd.py b/subplot/runcmd.py new file mode 100644 index 0000000..532b60b --- /dev/null +++ b/subplot/runcmd.py @@ -0,0 +1,243 @@ +import logging +import os +import re +import shlex +import subprocess + + +# +# Helper functions. +# + +# Get exit code or other stored data about the latest command run by +# runcmd_run. + + +def _runcmd_get(ctx, name): + ns = ctx.declare("_runcmd") + return ns[name] + + +def runcmd_get_exit_code(ctx): + return _runcmd_get(ctx, "exit") + + +def runcmd_get_stdout(ctx): + return _runcmd_get(ctx, "stdout") + + +def runcmd_get_stdout_raw(ctx): + return _runcmd_get(ctx, "stdout.raw") + + +def runcmd_get_stderr(ctx): + return _runcmd_get(ctx, "stderr") + + +def runcmd_get_stderr_raw(ctx): + return _runcmd_get(ctx, "stderr.raw") + + +def runcmd_get_argv(ctx): + return _runcmd_get(ctx, "argv") + + +# Run a command, given an argv and other arguments for subprocess.Popen. +# +# This is meant to be a helper function, not bound directly to a step. The +# stdout, stderr, and exit code are stored in the "_runcmd" namespace in the +# ctx context. +def runcmd_run(ctx, argv, **kwargs): + ns = ctx.declare("_runcmd") + env = dict(os.environ) + pp = ns.get("path-prefix") + if pp: + env["PATH"] = pp + ":" + env["PATH"] + + logging.debug(f"runcmd_run") + logging.debug(f" argv: {argv}") + logging.debug(f" env: {env}") + p = subprocess.Popen( + argv, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env, **kwargs + ) + stdout, stderr = p.communicate("") + ns["argv"] = argv + ns["stdout.raw"] = stdout + ns["stderr.raw"] = stderr + ns["stdout"] = stdout.decode("utf-8") + ns["stderr"] = stderr.decode("utf-8") + ns["exit"] = p.returncode + logging.debug(f" ctx: {ctx}") + logging.debug(f" ns: {ns}") + + +# Step: prepend srcdir to PATH whenever runcmd runs a command. +def runcmd_helper_srcdir_path(ctx): + srcdir = globals()["srcdir"] + runcmd_prepend_to_path(ctx, srcdir) + + +# Step: This creates a helper script. +def runcmd_helper_script(ctx, filename=None): + get_file = globals()["get_file"] + with open(filename, "wb") as f: + f.write(get_file(filename)) + + +# +# Step functions for running commands. +# + + +def runcmd_prepend_to_path(ctx, dirname=None): + ns = ctx.declare("_runcmd") + pp = ns.get("path-prefix", "") + if pp: + pp = f"{pp}:{dirname}" + else: + pp = dirname + ns["path-prefix"] = pp + + +def runcmd_step(ctx, argv0=None, args=None): + runcmd_try_to_run(ctx, argv0=argv0, args=args) + runcmd_exit_code_is_zero(ctx) + + +def runcmd_try_to_run(ctx, argv0=None, args=None): + argv = [shlex.quote(argv0)] + shlex.split(args) + runcmd_run(ctx, argv) + + +# +# Step functions for examining exit codes. +# + + +def runcmd_exit_code_is_zero(ctx): + runcmd_exit_code_is(ctx, exit=0) + + +def runcmd_exit_code_is(ctx, exit=None): + assert_eq = globals()["assert_eq"] + assert_eq(runcmd_get_exit_code(ctx), int(exit)) + + +def runcmd_exit_code_is_nonzero(ctx): + runcmd_exit_code_is_not(ctx, exit=0) + + +def runcmd_exit_code_is_not(ctx, exit=None): + assert_ne = globals()["assert_ne"] + assert_ne(runcmd_get_exit_code(ctx), int(exit)) + + +# +# Step functions and helpers for examining output in various ways. +# + + +def runcmd_stdout_is(ctx, text=None): + _runcmd_output_is(runcmd_get_stdout(ctx), text) + + +def runcmd_stdout_isnt(ctx, text=None): + _runcmd_output_isnt(runcmd_get_stdout(ctx), text) + + +def runcmd_stderr_is(ctx, text=None): + _runcmd_output_is(runcmd_get_stderr(ctx), text) + + +def runcmd_stderr_isnt(ctx, text=None): + _runcmd_output_isnt(runcmd_get_stderr(ctx), text) + + +def _runcmd_output_is(actual, wanted): + assert_eq = globals()["assert_eq"] + wanted = bytes(wanted, "utf8").decode("unicode_escape") + logging.debug("_runcmd_output_is:") + logging.debug(f" actual: {actual!r}") + logging.debug(f" wanted: {wanted!r}") + assert_eq(actual, wanted) + + +def _runcmd_output_isnt(actual, wanted): + assert_ne = globals()["assert_ne"] + wanted = bytes(wanted, "utf8").decode("unicode_escape") + logging.debug("_runcmd_output_isnt:") + logging.debug(f" actual: {actual!r}") + logging.debug(f" wanted: {wanted!r}") + assert_ne(actual, wanted) + + +def runcmd_stdout_contains(ctx, text=None): + _runcmd_output_contains(runcmd_get_stdout(ctx), text) + + +def runcmd_stdout_doesnt_contain(ctx, text=None): + _runcmd_output_doesnt_contain(runcmd_get_stdout(ctx), text) + + +def runcmd_stderr_contains(ctx, text=None): + _runcmd_output_contains(runcmd_get_stderr(ctx), text) + + +def runcmd_stderr_doesnt_contain(ctx, text=None): + _runcmd_output_doesnt_contain(runcmd_get_stderr(ctx), text) + + +def _runcmd_output_contains(actual, wanted): + assert_eq = globals()["assert_eq"] + wanted = bytes(wanted, "utf8").decode("unicode_escape") + logging.debug("_runcmd_output_contains:") + logging.debug(f" actual: {actual!r}") + logging.debug(f" wanted: {wanted!r}") + assert_eq(wanted in actual, True) + + +def _runcmd_output_doesnt_contain(actual, wanted): + assert_ne = globals()["assert_ne"] + wanted = bytes(wanted, "utf8").decode("unicode_escape") + logging.debug("_runcmd_output_doesnt_contain:") + logging.debug(f" actual: {actual!r}") + logging.debug(f" wanted: {wanted!r}") + assert_ne(wanted in actual, True) + + +def runcmd_stdout_matches_regex(ctx, regex=None): + _runcmd_output_matches_regex(runcmd_get_stdout(ctx), regex) + + +def runcmd_stdout_doesnt_match_regex(ctx, regex=None): + _runcmd_output_doesnt_match_regex(runcmd_get_stdout(ctx), regex) + + +def runcmd_stderr_matches_regex(ctx, regex=None): + _runcmd_output_matches_regex(runcmd_get_stderr(ctx), regex) + + +def runcmd_stderr_doesnt_match_regex(ctx, regex=None): + _runcmd_output_doesnt_match_regex(runcmd_get_stderr(ctx), regex) + + +def _runcmd_output_matches_regex(actual, regex): + assert_ne = globals()["assert_ne"] + r = re.compile(regex) + m = r.search(actual) + logging.debug("_runcmd_output_matches_regex:") + logging.debug(f" actual: {actual!r}") + logging.debug(f" regex: {regex!r}") + logging.debug(f" match: {m}") + assert_ne(m, None) + + +def _runcmd_output_doesnt_match_regex(actual, regex): + assert_eq = globals()["assert_eq"] + r = re.compile(regex) + m = r.search(actual) + logging.debug("_runcmd_output_doesnt_match_regex:") + logging.debug(f" actual: {actual!r}") + logging.debug(f" regex: {regex!r}") + logging.debug(f" match: {m}") + assert_eq(m, None) diff --git a/subplot/runcmd.yaml b/subplot/runcmd.yaml new file mode 100644 index 0000000..48dde90 --- /dev/null +++ b/subplot/runcmd.yaml @@ -0,0 +1,83 @@ +# Steps to run commands. + +- given: helper script {filename} for runcmd + function: runcmd_helper_script + +- given: srcdir is in the PATH + function: runcmd_helper_srcdir_path + +- when: I run (?P<argv0>\S+)(?P<args>.*) + regex: true + function: runcmd_step + +- when: I try to run (?P<argv0>\S+)(?P<args>.*) + regex: true + function: runcmd_try_to_run + +# Steps to examine exit code of latest command. + +- then: exit code is {exit} + function: runcmd_exit_code_is + +- then: exit code is not {exit} + function: runcmd_exit_code_is_not + +- then: command is successful + function: runcmd_exit_code_is_zero + +- then: command fails + function: runcmd_exit_code_is_nonzero + +# Steps to examine stdout/stderr for exact content. + +- then: stdout is exactly "(?P<text>.*)" + regex: true + function: runcmd_stdout_is + +- then: "stdout isn't exactly \"(?P<text>.*)\"" + regex: true + function: runcmd_stdout_isnt + +- then: stderr is exactly "(?P<text>.*)" + regex: true + function: runcmd_stderr_is + +- then: "stderr isn't exactly \"(?P<text>.*)\"" + regex: true + function: runcmd_stderr_isnt + +# Steps to examine stdout/stderr for sub-strings. + +- then: stdout contains "(?P<text>.*)" + regex: true + function: runcmd_stdout_contains + +- then: "stdout doesn't contain \"(?P<text>.*)\"" + regex: true + function: runcmd_stdout_doesnt_contain + +- then: stderr contains "(?P<text>.*)" + regex: true + function: runcmd_stderr_contains + +- then: "stderr doesn't contain \"(?P<text>.*)\"" + regex: true + function: runcmd_stderr_doesnt_contain + +# Steps to match stdout/stderr against regular expressions. + +- then: stdout matches regex (?P<regex>.*) + regex: true + function: runcmd_stdout_matches_regex + +- then: stdout doesn't match regex (?P<regex>.*) + regex: true + function: runcmd_stdout_doesnt_match_regex + +- then: stderr matches regex (?P<regex>.*) + regex: true + function: runcmd_stderr_matches_regex + +- then: stderr doesn't match regex (?P<regex>.*) + regex: true + function: runcmd_stderr_doesnt_match_regex |