diff options
Diffstat (limited to 'subplot')
-rw-r--r-- | subplot/client.py | 76 | ||||
-rw-r--r-- | subplot/client.yaml | 66 | ||||
-rw-r--r-- | subplot/data.py | 170 | ||||
-rw-r--r-- | subplot/data.yaml | 98 | ||||
-rw-r--r-- | subplot/server.py | 69 | ||||
-rw-r--r-- | subplot/server.yaml | 99 | ||||
-rw-r--r-- | subplot/vendored/daemon.md | 38 | ||||
-rw-r--r-- | subplot/vendored/daemon.py | 139 | ||||
-rw-r--r-- | subplot/vendored/daemon.yaml | 17 | ||||
-rw-r--r-- | subplot/vendored/files.md | 82 | ||||
-rw-r--r-- | subplot/vendored/files.py | 158 | ||||
-rw-r--r-- | subplot/vendored/files.yaml | 62 | ||||
-rw-r--r-- | subplot/vendored/runcmd.md | 170 | ||||
-rw-r--r-- | subplot/vendored/runcmd.py | 252 | ||||
-rw-r--r-- | subplot/vendored/runcmd.yaml | 83 |
15 files changed, 483 insertions, 1096 deletions
diff --git a/subplot/client.py b/subplot/client.py index d450b4c..09b1556 100644 --- a/subplot/client.py +++ b/subplot/client.py @@ -3,15 +3,35 @@ import os import yaml +def start_obnam(ctx): + start_chunk_server = globals()["start_chunk_server"] + install_obnam(ctx) + start_chunk_server(ctx) + + +def stop_obnam(ctx): + stop_chunk_server = globals()["stop_chunk_server"] + stop_chunk_server(ctx) + uninstall_obnam(ctx) + + def install_obnam(ctx): runcmd_prepend_to_path = globals()["runcmd_prepend_to_path"] srcdir = globals()["srcdir"] # Add the directory with built Rust binaries to the path. - runcmd_prepend_to_path(ctx, dirname=os.path.join(srcdir, "target", "debug")) + default_target = os.path.join(srcdir, "target") + target = os.environ.get("CARGO_TARGET_DIR", default_target) + runcmd_prepend_to_path(ctx, dirname=os.path.join(target, "debug")) + ctx["server-binary"] = os.path.join(target, "debug", "obnam-server") -def configure_client(ctx, filename=None): +def uninstall_obnam(ctx): + runcmd_run = globals()["runcmd_run"] + runcmd_run(ctx, ["chmod", "-R", "u+rwX", "."]) + + +def configure_client_without_init(ctx, filename=None): get_file = globals()["get_file"] assert ctx.get("server_url") is not None @@ -20,42 +40,37 @@ def configure_client(ctx, filename=None): config = yaml.safe_load(config) config["server_url"] = ctx["server_url"] + logging.debug(f"client config {filename}: {config}") + dirname = os.path.expanduser("~/.config/obnam") + if not os.path.exists(dirname): + os.makedirs(dirname) + filename = os.path.join(dirname, "obnam.yaml") + logging.debug(f"configure_client: filename={filename}") with open(filename, "w") as f: yaml.safe_dump(config, stream=f) -def run_obnam_restore(ctx, filename=None, genid=None, todir=None): - genid = ctx["vars"][genid] - run_obnam_restore_with_genref(ctx, filename=filename, genref=genid, todir=todir) - +def configure_client_with_init(ctx, filename=None): + runcmd_run = globals()["runcmd_run"] + runcmd_exit_code_is_zero = globals()["runcmd_exit_code_is_zero"] -def run_obnam_restore_latest(ctx, filename=None, todir=None): - run_obnam_restore_with_genref(ctx, filename=filename, genref="latest", todir=todir) + configure_client_without_init(ctx, filename=filename) + runcmd_run(ctx, ["obnam", "init", "--insecure-passphrase=hunter2"]) + runcmd_exit_code_is_zero(ctx) -def run_obnam_restore_with_genref(ctx, filename=None, genref=None, todir=None): +def run_obnam_restore(ctx, genid=None, todir=None): runcmd_run = globals()["runcmd_run"] - runcmd_run( - ctx, - [ - "env", - "RUST_LOG=obnam", - "obnam", - "--config", - filename, - "restore", - genref, - todir, - ], - ) - - -def run_obnam_get_chunk(ctx, filename=None, gen_id=None, todir=None): + genref = ctx["vars"][genid] + runcmd_run(ctx, ["env", "RUST_LOG=obnam", "obnam", "restore", genref, todir]) + + +def run_obnam_get_chunk(ctx, gen_id=None, todir=None): runcmd_run = globals()["runcmd_run"] gen_id = ctx["vars"][gen_id] logging.debug(f"run_obnam_get_chunk: gen_id={gen_id}") - runcmd_run(ctx, ["obnam", "--config", filename, "get-chunk", gen_id]) + runcmd_run(ctx, ["obnam", "get-chunk", gen_id]) def capture_generation_id(ctx, varname=None): @@ -111,3 +126,12 @@ def stdout_matches_file(ctx, filename=None): stdout = runcmd_get_stdout(ctx) data = open(filename).read() assert_eq(stdout, data) + + +def stdout_contains_home_dir_path(ctx, path=None): + runcmd_get_stdout = globals()["runcmd_get_stdout"] + stdout = runcmd_get_stdout(ctx) + wanted = os.path.abspath(os.path.normpath("./" + path)) + logging.debug(f"stdout_contains_home_dir_path: stdout={stdout!r}") + logging.debug(f"stdout_contains_home_dir_path: wanted={wanted!r}") + assert wanted in stdout diff --git a/subplot/client.yaml b/subplot/client.yaml index b1f9b19..104f479 100644 --- a/subplot/client.yaml +++ b/subplot/client.yaml @@ -1,32 +1,70 @@ +- given: "a working Obnam system" + impl: + python: + function: start_obnam + cleanup: stop_obnam + - given: "an installed obnam" - function: install_obnam + impl: + python: + function: install_obnam + cleanup: uninstall_obnam - given: "a client config based on {filename}" - function: configure_client + impl: + python: + function: configure_client_with_init + types: + filename: file -- when: "I invoke obnam --config {filename} restore <{genid}> {todir}" - function: run_obnam_restore +- given: "a client config, without passphrase, based on {filename}" + impl: + python: + function: configure_client_without_init + types: + filename: file -- when: "I invoke obnam --config {filename} restore latest {todir}" - function: run_obnam_restore_latest +- when: "I invoke obnam restore <{genid}> {todir}" + impl: + python: + function: run_obnam_restore -- when: "I invoke obnam --config {filename} get-chunk <{gen_id}>" - function: run_obnam_get_chunk +- when: "I invoke obnam get-chunk <{gen_id}>" + impl: + python: + function: run_obnam_get_chunk - then: "backup generation is {varname}" - function: capture_generation_id + impl: + python: + function: capture_generation_id - then: "generation list contains <{gen_id}>" - function: generation_list_contains + impl: + python: + function: generation_list_contains - then: "file {filename} was backed up because it was new" - function: file_was_new + impl: + python: + function: file_was_new - then: "file {filename} was backed up because it was changed" - function: file_was_changed + impl: + python: + function: file_was_changed - then: "file {filename} was not backed up because it was unchanged" - function: file_was_unchanged + impl: + python: + function: file_was_unchanged - then: "stdout matches file {filename}" - function: stdout_matches_file + impl: + python: + function: stdout_matches_file + +- then: "stdout contains home directory followed by {path}" + impl: + python: + function: stdout_contains_home_dir_path diff --git a/subplot/data.py b/subplot/data.py index a24cd0c..583e52d 100644 --- a/subplot/data.py +++ b/subplot/data.py @@ -1,16 +1,41 @@ +import json import logging import os import random +import socket +import stat +import yaml -def create_file_with_random_data(ctx, filename=None): - N = 128 - data = "".join(chr(random.randint(0, 255)) for i in range(N)).encode("UTF-8") +def create_file_with_given_data(ctx, filename=None, data=None): + logging.debug(f"creating file {filename} with {data!r}") dirname = os.path.dirname(filename) or "." - logging.debug(f"create_file_with_random_data: dirname={dirname}") os.makedirs(dirname, exist_ok=True) with open(filename, "wb") as f: - f.write(data) + f.write(data.encode("UTF-8")) + + +def create_file_with_random_data(ctx, filename=None): + N = 128 + data = "".join(chr(random.randint(0, 255)) for i in range(N)) + create_file_with_given_data(ctx, filename=filename, data=data) + + +def create_unix_socket(ctx, filename=None): + fd = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + fd.bind(filename) + + +def create_fifo(ctx, filename=None): + os.mkfifo(filename) + + +def create_cachedir_tag_in(ctx, dirpath=None): + filepath = f"{dirpath}/CACHEDIR.TAG" + logging.debug(f"creating {filepath}") + os.makedirs(dirpath, exist_ok=True) + with open(filepath, "w") as f: + f.write("Signature: 8a477f597d28d172789f06886806bc55") def create_nonutf8_filename(ctx, dirname=None): @@ -24,7 +49,7 @@ def chmod_file(ctx, filename=None, mode=None): def create_symlink(ctx, linkname=None, target=None): - os.symlink(linkname, target) + os.symlink(target, linkname) def create_manifest_of_live(ctx, dirname=None, manifest=None): @@ -46,7 +71,26 @@ def _create_manifest_of_directory(ctx, dirname=None, manifest=None): runcmd_run(ctx, ["find", "-exec", "summain", "{}", "+"], cwd=dirname) assert runcmd_get_exit_code(ctx) == 0 stdout = runcmd_get_stdout(ctx) - open(manifest, "w").write(stdout) + with open(manifest, "w") as f: + f.write(stdout) + + +def file_is_restored(ctx, filename=None, restored=None): + filename = os.path.join(restored, "./" + filename) + exists = os.path.exists(filename) + logging.debug(f"restored? {filename} {exists}") + assert exists + + +def file_is_not_restored(ctx, filename=None, restored=None): + filename = os.path.join(restored, "./" + filename) + logging.info(r"verifying that {filename} does not exist") + try: + exists = os.path.exists(filename) + except PermissionError: + exists = False + logging.debug(f"restored? {filename} {exists}") + assert not exists def files_match(ctx, first=None, second=None): @@ -57,3 +101,115 @@ def files_match(ctx, first=None, second=None): logging.debug(f"files_match: f:\n{f}") logging.debug(f"files_match: s:\n{s}") assert_eq(f, s) + + +def convert_yaml_to_json(ctx, yaml_name=None, json_name=None): + with open(yaml_name) as f: + obj = yaml.safe_load(f) + with open(json_name, "w") as f: + json.dump(obj, f) + + +def match_stdout_to_json_file_superset(ctx, filename=None): + runcmd_get_stdout = globals()["runcmd_get_stdout"] + assert_eq = globals()["assert_eq"] + assert_dict_eq = globals()["assert_dict_eq"] + + stdout = runcmd_get_stdout(ctx) + stdout = json.loads(stdout.strip()) + obj = json.load(open(filename)) + logging.debug(f"match_stdout_to_json_file: stdout={stdout!r}") + logging.debug(f"match_stdout_to_json_file: file={obj!r}") + + if isinstance(obj, dict): + stdout = {key: value for key, value in stdout.items() if key in obj} + assert_dict_eq(obj, stdout) + elif isinstance(obj, list): + obj = {"key": obj} + stdout = {"key": stdout} + assert_dict_eq(obj, stdout) + assert_dict_eq(obj, stdout) + else: + assert_eq(obj, stdout) + + +def match_stdout_to_json_file_exactly(ctx, filename=None): + runcmd_get_stdout = globals()["runcmd_get_stdout"] + assert_eq = globals()["assert_eq"] + assert_dict_eq = globals()["assert_dict_eq"] + + stdout = runcmd_get_stdout(ctx) + stdout = json.loads(stdout.strip()) + obj = json.load(open(filename)) + logging.debug(f"match_stdout_to_json_file: stdout={stdout!r}") + logging.debug(f"match_stdout_to_json_file: file={obj!r}") + + if isinstance(obj, list): + obj = {"key": obj} + stdout = {"key": stdout} + assert_dict_eq(obj, stdout) + elif isinstance(obj, dict): + assert_dict_eq(obj, stdout) + else: + assert_eq(obj, stdout) + + +def manifests_match(ctx, expected=None, actual=None): + assert_eq = globals()["assert_eq"] + assert_dict_eq = globals()["assert_dict_eq"] + + logging.debug(f"comparing manifests {expected} and {actual}") + + expected_objs = list(yaml.safe_load_all(open(expected))) + actual_objs = list(yaml.safe_load_all(open(actual))) + + logging.debug(f"there are {len(expected_objs)} and {len(actual_objs)} objects") + + i = 0 + while expected_objs and actual_objs: + e = expected_objs.pop(0) + a = actual_objs.pop(0) + + logging.debug(f"comparing manifest objects at index {i}:") + logging.debug(f" expected: {e}") + logging.debug(f" actual : {a}") + assert_dict_eq(e, a) + + i += 1 + + logging.debug(f"remaining expected objecvts: {expected_objs}") + logging.debug(f"remaining actual objecvts : {actual_objs}") + assert_eq(expected_objs, []) + assert_eq(actual_objs, []) + + logging.debug(f"manifests {expected} and {actual} match") + + +def file_is_readable_by_owner(ctx, filename=None): + assert_eq = globals()["assert_eq"] + + st = os.lstat(filename) + mode = stat.S_IMODE(st.st_mode) + logging.debug("file mode: %o", mode) + assert_eq(mode, 0o400) + + +def file_does_not_contain(ctx, filename=None, pattern=None): + data = open(filename).read() + assert pattern not in data + + +def files_are_different(ctx, filename1=None, filename2=None): + assert_ne = globals()["assert_ne"] + + data1 = open(filename1, "rb").read() + data2 = open(filename2, "rb").read() + assert_ne(data1, data2) + + +def files_are_identical(ctx, filename1=None, filename2=None): + assert_eq = globals()["assert_eq"] + + data1 = open(filename1, "rb").read() + data2 = open(filename2, "rb").read() + assert_eq(data1, data2) diff --git a/subplot/data.yaml b/subplot/data.yaml index 7659319..533237f 100644 --- a/subplot/data.yaml +++ b/subplot/data.yaml @@ -1,25 +1,99 @@ -- given: > - a file (?P<filename>\\S+) containing "(?P<data>.*)" - regex: true - function: create_file_with_given_data +- given: a file {filename} containing "{data:text}" + impl: + python: + function: create_file_with_given_data - given: "a file {filename} containing some random data" - function: create_file_with_random_data + impl: + python: + function: create_file_with_random_data + +- given: "a Unix socket {filename}" + impl: + python: + function: create_unix_socket + +- given: "a named pipe {filename}" + impl: + python: + function: create_fifo + +- given: a cache directory tag in {dirpath} + impl: + python: + function: create_cachedir_tag_in - given: "a file in {dirname} with a non-UTF8 filename" - function: create_nonutf8_filename + impl: + python: + function: create_nonutf8_filename - given: file {filename} has mode {mode} - function: chmod_file + impl: + python: + function: chmod_file - given: symbolink link {linkname} that points at {target} - function: create_symlink + impl: + python: + function: create_symlink - given: a manifest of the directory {dirname} in {manifest} - function: create_manifest_of_live + impl: + python: + function: create_manifest_of_live - given: a manifest of the directory {dirname} restored in {restored} in {manifest} - function: create_manifest_of_restored + impl: + python: + function: create_manifest_of_restored + +- given: "JSON file {json_name} converted from YAML file {yaml_name}" + impl: + python: + function: convert_yaml_to_json + +- then: "stdout, as JSON, exactly matches file {filename}" + impl: + python: + function: match_stdout_to_json_file_exactly + +- then: "stdout, as JSON, has all the values in file {filename}" + impl: + python: + function: match_stdout_to_json_file_superset + +- then: "file {filename} is restored to {restored}" + impl: + python: + function: file_is_restored + +- then: "file {filename} is not restored to {restored}" + impl: + python: + function: file_is_not_restored + +- then: "manifests {expected} and {actual} match" + impl: + python: + function: manifests_match + +- then: "file {filename} is only readable by owner" + impl: + python: + function: file_is_readable_by_owner + +- then: "file {filename} does not contain \"{pattern:text}\"" + impl: + python: + function: file_does_not_contain + +- then: "files {filename1} and {filename2} are different" + impl: + python: + function: files_are_different -- then: files {first} and {second} match - function: files_match +- then: "files {filename1} and {filename2} are identical" + impl: + python: + function: files_are_identical diff --git a/subplot/server.py b/subplot/server.py index 289e181..a604733 100644 --- a/subplot/server.py +++ b/subplot/server.py @@ -5,8 +5,6 @@ import random import re import requests import shutil -import socket -import time import urllib3 import yaml @@ -14,7 +12,7 @@ import yaml urllib3.disable_warnings() -def start_chunk_server(ctx): +def start_chunk_server(ctx, env=None): daemon_start_on_port = globals()["daemon_start_on_port"] srcdir = globals()["srcdir"] @@ -35,7 +33,7 @@ def start_chunk_server(ctx): "address": f"localhost:{port}", } - server_binary = os.path.abspath(os.path.join(srcdir, "target", "debug", "obnam-server")) + server_binary = ctx["server-binary"] filename = "config.yaml" yaml.safe_dump(config, stream=open(filename, "w")) @@ -44,22 +42,18 @@ def start_chunk_server(ctx): ctx["server_url"] = f"https://{config['address']}" daemon_start_on_port( - ctx, - name="obnam-server", - path=server_binary, - args=filename, - port=port, + ctx, name="obnam-server", path=server_binary, args=filename, port=port, env=env ) -def stop_chunk_server(ctx): +def stop_chunk_server(ctx, env=None): logging.debug("Stopping obnam-server") daemon_stop = globals()["daemon_stop"] daemon_stop(ctx, name="obnam-server") def post_file(ctx, filename=None, path=None, header=None, json=None): - url = f"{ctx['server_url']}/chunks" + url = f"{ctx['server_url']}/v1/chunks" headers = {header: json} data = open(filename, "rb").read() _request(ctx, requests.post, url, headers=headers, data=data) @@ -71,12 +65,12 @@ def get_chunk_via_var(ctx, var=None): def get_chunk_by_id(ctx, chunk_id=None): - url = f"{ctx['server_url']}/chunks/{chunk_id}" + url = f"{ctx['server_url']}/v1/chunks/{chunk_id}" _request(ctx, requests.get, url) -def find_chunks_with_sha(ctx, sha=None): - url = f"{ctx['server_url']}/chunks?sha256={sha}" +def find_chunks_with_label(ctx, sha=None): + url = f"{ctx['server_url']}/v1/chunks?label={sha}" _request(ctx, requests.get, url) @@ -86,14 +80,16 @@ def delete_chunk_via_var(ctx, var=None): def delete_chunk_by_id(ctx, chunk_id=None): - url = f"{ctx['server_url']}/chunks/{chunk_id}" + url = f"{ctx['server_url']}/v1/chunks/{chunk_id}" _request(ctx, requests.delete, url) def make_chunk_file_be_empty(ctx, chunk_id=None): chunk_id = ctx["vars"][chunk_id] chunks = ctx["config"]["chunks"] - for (dirname, _, _) in os.walk(chunks): + logging.debug(f"trying to empty chunk {chunk_id}") + for (dirname, _, filenames) in os.walk(chunks): + logging.debug(f"found directory {dirname}, with {filenames}") filename = os.path.join(dirname, chunk_id + ".data") if os.path.exists(filename): logging.debug(f"emptying chunk file {filename}") @@ -138,6 +134,33 @@ def json_body_matches(ctx, wanted=None): assert_eq(body.get(key, "not.there"), wanted[key]) +def server_has_n_chunks(ctx, n=None): + assert_eq = globals()["assert_eq"] + n = int(n) + files = find_files(ctx["config"]["chunks"]) + files = [x for x in files if x.endswith(".data")] + logging.debug(f"server_has_n_file_chunks: n={n}") + logging.debug(f"server_has_n_file_chunks: len(files)={len(files)}") + logging.debug(f"server_has_n_file_chunks: files={files}") + assert_eq(n, len(files)) + + +def server_stderr_contains(ctx, wanted=None): + assert_eq = globals()["assert_eq"] + assert_eq(_server_stderr_contains(ctx, wanted), True) + + +def server_stderr_doesnt_contain(ctx, wanted=None): + assert_eq = globals()["assert_eq"] + assert_eq(_server_stderr_contains(ctx, wanted), False) + + +def find_files(root): + for dirname, _, names in os.walk(root): + for name in names: + yield os.path.join(dirname, name) + + # Make an HTTP request. def _request(ctx, method, url, headers=None, data=None): r = method(url, headers=headers, data=data, verify=False) @@ -177,3 +200,17 @@ def _expand_vars(ctx, s): result.append(value) s = s[m.end() :] return "".join(result) + + +def _server_stderr_contains(ctx, wanted): + daemon_get_stderr = globals()["daemon_get_stderr"] + + wanted = _expand_vars(ctx, wanted) + + stderr = daemon_get_stderr(ctx, "obnam-server") + + logging.debug(f"_server_stderr_contains:") + logging.debug(f" wanted: {wanted}") + logging.debug(f" stderr: {stderr}") + + return wanted in stderr diff --git a/subplot/server.yaml b/subplot/server.yaml index 68f8f0c..cf57931 100644 --- a/subplot/server.yaml +++ b/subplot/server.yaml @@ -1,45 +1,104 @@ - given: "a running chunk server" - function: start_chunk_server - cleanup: stop_chunk_server + impl: + python: + function: start_chunk_server + cleanup: stop_chunk_server + +- given: "a running chunk server with environment {env:text}" + impl: + python: + function: start_chunk_server + cleanup: stop_chunk_server - when: "the chunk server is stopped" - function: stop_chunk_server + impl: + python: + function: stop_chunk_server - when: "I POST (?P<filename>\\S+) to (?P<path>\\S+), with (?P<header>\\S+): (?P<json>.*)" regex: true - function: post_file + types: + filename: word + path: word + header: word + json: text + impl: + python: + function: post_file -- when: "I GET /chunks/<{var}>" - function: get_chunk_via_var +- when: "I GET /v1/chunks/<{var}>" + impl: + python: + function: get_chunk_via_var -- when: "I try to GET /chunks/{chunk_id}" - function: get_chunk_by_id +- when: "I try to GET /v1/chunks/{chunk_id}" + impl: + python: + function: get_chunk_by_id -- when: "I GET /chunks?sha256={sha}" +- when: "I GET /v1/chunks?label={sha}" regex: false - function: find_chunks_with_sha + impl: + python: + function: find_chunks_with_label -- when: "I DELETE /chunks/<{var}>" - function: delete_chunk_via_var +- when: "I DELETE /v1/chunks/<{var}>" + impl: + python: + function: delete_chunk_via_var -- when: "I try to DELETE /chunks/{chunk_id}" - function: delete_chunk_by_id +- when: "I try to DELETE /v1/chunks/{chunk_id}" + impl: + python: + function: delete_chunk_by_id - when: "chunk <{chunk_id}> on chunk server is replaced by an empty file" - function: make_chunk_file_be_empty + impl: + python: + function: make_chunk_file_be_empty - then: "HTTP status code is {status}" - function: status_code_is + impl: + python: + function: status_code_is - then: "{header} is {value}" - function: header_is + impl: + python: + function: header_is - then: "the JSON body has a field {field}, henceforth {var}" - function: remember_json_field + types: + field: word + var: word + impl: + python: + function: remember_json_field - then: "the JSON body matches (?P<wanted>.*)" regex: true - function: json_body_matches + types: + wanted: text + impl: + python: + function: json_body_matches - then: "the body matches file {filename}" - function: body_matches_file + impl: + python: + function: body_matches_file + +- then: "server has {n:int} chunks" + impl: + python: + function: server_has_n_chunks + +- then: chunk server's stderr contains "{wanted:text}" + impl: + python: + function: server_stderr_contains + +- then: chunk server's stderr doesn't contain "{wanted:text}" + impl: + python: + function: server_stderr_doesnt_contain diff --git a/subplot/vendored/daemon.md b/subplot/vendored/daemon.md deleted file mode 100644 index 131dcb1..0000000 --- a/subplot/vendored/daemon.md +++ /dev/null @@ -1,38 +0,0 @@ -# Introduction - -The [Subplot][] library `daemon` for Python provides scenario steps -and their implementations for running a background process and -terminating at the end of the scenario. - -[Subplot]: https://subplot.liw.fi/ - -This document explains the acceptance criteria for the library and how -they're verified. It uses the steps and functions from the -`lib/daemon` library. The scenarios all have the same structure: run a -command, then examine the exit code, verify the process is running. - -# Daemon is started and terminated - -This scenario starts a background process, verifies it's started, and -verifies it's terminated after the scenario ends. - -~~~scenario -given there is no "/bin/sleep 12765" process -when I start "/bin/sleep 12765" as a background process as sleepyhead -then a process "/bin/sleep 12765" is running -when I stop background process sleepyhead -then there is no "/bin/sleep 12765" process -~~~ - - - ---- -title: Acceptance criteria for the lib/daemon Subplot library -author: The Subplot project -bindings: -- daemon.yaml -template: python -functions: -- daemon.py -- runcmd.py -... diff --git a/subplot/vendored/daemon.py b/subplot/vendored/daemon.py deleted file mode 100644 index febf392..0000000 --- a/subplot/vendored/daemon.py +++ /dev/null @@ -1,139 +0,0 @@ -import logging -import os -import signal -import socket -import subprocess -import time - - -# Start a daemon that will open a port on localhost. -def daemon_start_on_port(ctx, path=None, args=None, name=None, port=None): - daemon_start(ctx, path=path, args=args, name=name) - daemon_wait_for_port("localhost", port) - - -# Start a daeamon, get its PID. Don't wait for a port or anything. This is -# meant for background processes that don't have port. Useful for testing the -# lib/daemon library of Subplot, but not much else. -def daemon_start(ctx, path=None, args=None, name=None): - runcmd_run = globals()["runcmd_run"] - runcmd_exit_code_is = globals()["runcmd_exit_code_is"] - runcmd_get_exit_code = globals()["runcmd_get_exit_code"] - runcmd_get_stderr = globals()["runcmd_get_stderr"] - runcmd_prepend_to_path = globals()["runcmd_prepend_to_path"] - - argv = [path] + args.split() - - logging.debug(f"Starting daemon {name}") - logging.debug(f" ctx={ctx.as_dict()}") - logging.debug(f" name={name}") - logging.debug(f" path={path}") - logging.debug(f" args={args}") - logging.debug(f" argv={argv}") - - ns = ctx.declare("_daemon") - - this = ns[name] = { - "pid-file": f"{name}.pid", - "stderr": f"{name}.stderr", - "stdout": f"{name}.stdout", - } - - # Debian installs `daemonize` to /usr/sbin, which isn't part of the minimal - # environment that Subplot sets up. So we add /usr/sbin to the PATH. - runcmd_prepend_to_path(ctx, "/usr/sbin") - runcmd_run( - ctx, - [ - "daemonize", - "-c", - os.getcwd(), - "-p", - this["pid-file"], - "-e", - this["stderr"], - "-o", - this["stdout"], - ] - + argv, - ) - - # Check that daemonize has exited OK. If it hasn't, it didn't start the - # background process at all. If so, log the stderr in case there was - # something useful there for debugging. - exit = runcmd_get_exit_code(ctx) - if exit != 0: - stderr = runcmd_get_stderr(ctx) - logging.error(f"daemon {name} stderr: {stderr}") - runcmd_exit_code_is(ctx, 0) - - # Get the pid of the background process, from the pid file created by - # daemonize. We don't need to wait for it, since we know daemonize already - # exited. If it isn't there now, it's won't appear later. - if not os.path.exists(this["pid-file"]): - raise Exception("daemonize didn't create a PID file") - - this["pid"] = _daemon_wait_for_pid(this["pid-file"], 10.0) - - logging.debug(f"Started daemon {name}") - logging.debug(f" pid={this['pid']}") - logging.debug(f" ctx={ctx.as_dict()}") - - -def _daemon_wait_for_pid(filename, timeout): - start = time.time() - while time.time() < start + timeout: - with open(filename) as f: - data = f.read().strip() - if data: - return int(data) - raise Exception("daemonize created a PID file without a PID") - - -def daemon_wait_for_port(host, port, timeout=3.0): - addr = (host, port) - until = time.time() + timeout - while True: - try: - s = socket.create_connection(addr, timeout=timeout) - s.close() - return - except socket.timeout: - logging.error(f"daemon did not respond at port {port} within {timeout} seconds") - raise - except socket.error as e: - logging.info(f"could not connect to daemon at {port}: {e}") - pass - if time.time() >= until: - logging.error(f"could not connect to daemon at {port} within {timeout} seconds") - raise ConnectionRefusedError() - - -# Stop a daemon. -def daemon_stop(ctx, name=None): - logging.debug(f"Stopping daemon {name}") - ns = ctx.declare("_daemon") - logging.debug(f" ns={ns}") - pid = ns[name]["pid"] - signo = signal.SIGKILL - - logging.debug(f"Terminating process {pid} with signal {signo}") - try: - os.kill(pid, signo) - except ProcessLookupError: - logging.warning("Process did not actually exist (anymore?)") - - -def daemon_no_such_process(ctx, args=None): - assert not _daemon_pgrep(args) - - -def daemon_process_exists(ctx, args=None): - assert _daemon_pgrep(args) - - -def _daemon_pgrep(pattern): - logging.info(f"checking if process exists: pattern={pattern}") - exit = subprocess.call(["pgrep", "-laf", pattern]) - logging.info(f"exit code: {exit}") - return exit == 0 diff --git a/subplot/vendored/daemon.yaml b/subplot/vendored/daemon.yaml deleted file mode 100644 index 6165c62..0000000 --- a/subplot/vendored/daemon.yaml +++ /dev/null @@ -1,17 +0,0 @@ -- given: there is no "{args:text}" process - function: daemon_no_such_process - -- when: I start "{path}{args:text}" as a background process as {name}, on port {port} - function: daemon_start_on_port - -- when: I start "{path}{args:text}" as a background process as {name} - function: daemon_start - -- when: I stop background process {name} - function: daemon_stop - -- then: a process "{args:text}" is running - function: daemon_process_exists - -- then: there is no "{args:text}" process - function: daemon_no_such_process diff --git a/subplot/vendored/files.md b/subplot/vendored/files.md deleted file mode 100644 index 68ef1ac..0000000 --- a/subplot/vendored/files.md +++ /dev/null @@ -1,82 +0,0 @@ -# Introduction - -The [Subplot][] library `files` provides scenario steps and their -implementations for managing files on the file system during tests. -The library consists of a bindings file `lib/files.yaml` and -implementations in Python in `lib/files.py`. - -[Subplot]: https://subplot.liw.fi/ - -This document explains the acceptance criteria for the library and how -they're verified. It uses the steps and functions from the `files` -library. - -# Create on-disk files from embedded files - -Subplot allows the source document to embed test files, and the -`files` library provides steps to create real, on-disk files from -the embedded files. - -~~~scenario -given file hello.txt -then file hello.txt exists -and file hello.txt contains "hello, world" -and file other.txt does not exist -given file other.txt from hello.txt -then file other.txt exists -and files hello.txt and other.txt match -and only files hello.txt, other.txt exist -~~~ - -~~~{#hello.txt .file .numberLines} -hello, world -~~~ - - -# File metadata - -These steps create files and manage their metadata. - -~~~scenario -given file hello.txt -when I remember metadata for file hello.txt -then file hello.txt has same metadata as before - -when I write "yo" to file hello.txt -then file hello.txt has different metadata from before -~~~ - -# File modification time - -These steps manipulate and test file modification times. - -~~~scenario -given file foo.dat has modification time 1970-01-02 03:04:05 -then file foo.dat has a very old modification time - -when I touch file foo.dat -then file foo.dat has a very recent modification time -~~~ - - -# File contents - -These steps verify contents of files. - -~~~scenario -given file hello.txt -then file hello.txt contains "hello, world" -and file hello.txt matches regex "hello, .*" -and file hello.txt matches regex /hello, .*/ -~~~ - - ---- -title: Acceptance criteria for the files Subplot library -author: The Subplot project -template: python -bindings: -- files.yaml -functions: -- files.py -... diff --git a/subplot/vendored/files.py b/subplot/vendored/files.py deleted file mode 100644 index ec37b9d..0000000 --- a/subplot/vendored/files.py +++ /dev/null @@ -1,158 +0,0 @@ -import logging -import os -import re -import time - - -def files_create_from_embedded(ctx, filename=None): - files_create_from_embedded_with_other_name( - ctx, filename_on_disk=filename, embedded_filename=filename - ) - - -def files_create_from_embedded_with_other_name( - ctx, filename_on_disk=None, embedded_filename=None -): - get_file = globals()["get_file"] - with open(filename_on_disk, "wb") as f: - f.write(get_file(embedded_filename)) - - -def files_create_from_text(ctx, filename=None, text=None): - with open(filename, "w") as f: - f.write(text) - - -def files_file_exists(ctx, filename=None): - assert_eq = globals()["assert_eq"] - assert_eq(os.path.exists(filename), True) - - -def files_file_does_not_exist(ctx, filename=None): - assert_eq = globals()["assert_eq"] - assert_eq(os.path.exists(filename), False) - - -def files_only_these_exist(ctx, filenames=None): - assert_eq = globals()["assert_eq"] - filenames = filenames.replace(",", "").split() - assert_eq(set(os.listdir(".")), set(filenames)) - - -def files_file_contains(ctx, filename=None, data=None): - assert_eq = globals()["assert_eq"] - with open(filename, "rb") as f: - actual = f.read() - actual = actual.decode("UTF-8") - assert_eq(data in actual, True) - - -def files_file_matches_regex(ctx, filename=None, regex=None): - assert_eq = globals()["assert_eq"] - with open(filename) as f: - content = f.read() - m = re.search(regex, content) - if m is None: - logging.debug(f"files_file_matches_regex: no match") - logging.debug(f" filenamed: {filename}") - logging.debug(f" regex: {regex}") - logging.debug(f" content: {regex}") - logging.debug(f" match: {m}") - assert_eq(bool(m), True) - - -def files_match(ctx, filename1=None, filename2=None): - assert_eq = globals()["assert_eq"] - with open(filename1, "rb") as f: - data1 = f.read() - with open(filename2, "rb") as f: - data2 = f.read() - assert_eq(data1, data2) - - -def files_touch_with_timestamp( - ctx, - filename=None, - year=None, - month=None, - day=None, - hour=None, - minute=None, - second=None, -): - t = ( - int(year), - int(month), - int(day), - int(hour), - int(minute), - int(second), - -1, - -1, - -1, - ) - ts = time.mktime(t) - _files_touch(filename, ts) - - -def files_touch(ctx, filename=None): - _files_touch(filename, None) - - -def _files_touch(filename, ts): - if not os.path.exists(filename): - open(filename, "w").close() - times = None - if ts is not None: - times = (ts, ts) - os.utime(filename, times=times) - - -def files_mtime_is_recent(ctx, filename=None): - st = os.stat(filename) - age = abs(st.st_mtime - time.time()) - assert age < 1.0 - - -def files_mtime_is_ancient(ctx, filename=None): - st = os.stat(filename) - age = abs(st.st_mtime - time.time()) - year = 365 * 24 * 60 * 60 - required = 39 * year - logging.debug(f"ancient? mtime={st.st_mtime} age={age} required={required}") - assert age > required - - -def files_remember_metadata(ctx, filename=None): - meta = _files_remembered(ctx) - meta[filename] = _files_get_metadata(filename) - logging.debug("files_remember_metadata:") - logging.debug(f" meta: {meta}") - logging.debug(f" ctx: {ctx}") - - -# Check that current metadata of a file is as stored in the context. -def files_has_remembered_metadata(ctx, filename=None): - assert_eq = globals()["assert_eq"] - meta = _files_remembered(ctx) - logging.debug("files_has_remembered_metadata:") - logging.debug(f" meta: {meta}") - logging.debug(f" ctx: {ctx}") - assert_eq(meta[filename], _files_get_metadata(filename)) - - -def files_has_different_metadata(ctx, filename=None): - assert_ne = globals()["assert_ne"] - meta = _files_remembered(ctx) - assert_ne(meta[filename], _files_get_metadata(filename)) - - -def _files_remembered(ctx): - ns = ctx.declare("_files") - return ns.get("remembered-metadata", {}) - - -def _files_get_metadata(filename): - st = os.lstat(filename) - keys = ["st_dev", "st_gid", "st_ino", "st_mode", "st_mtime", "st_size", "st_uid"] - return {key: getattr(st, key) for key in keys} diff --git a/subplot/vendored/files.yaml b/subplot/vendored/files.yaml deleted file mode 100644 index be69920..0000000 --- a/subplot/vendored/files.yaml +++ /dev/null @@ -1,62 +0,0 @@ -- given: file {filename} - function: files_create_from_embedded - types: - filename: file - -- given: file {filename_on_disk} from {embedded_filename} - function: files_create_from_embedded_with_other_name - types: - embedded_filename: file - -- given: file {filename} has modification time {year}-{month}-{day} {hour}:{minute}:{second} - function: files_touch_with_timestamp - -- when: I write "(?P<text>.*)" to file (?P<filename>\S+) - regex: true - function: files_create_from_text - -- when: I remember metadata for file {filename} - function: files_remember_metadata - -- when: I touch file {filename} - function: files_touch - -- then: file {filename} exists - function: files_file_exists - -- then: file {filename} does not exist - function: files_file_does_not_exist - -- then: only files (?P<filenames>.+) exist - function: files_only_these_exist - regex: true - -- then: file (?P<filename>\S+) contains "(?P<data>.*)" - regex: true - function: files_file_contains - -- then: file (?P<filename>\S+) matches regex /(?P<regex>.*)/ - regex: true - function: files_file_matches_regex - -- then: file (?P<filename>\S+) matches regex "(?P<regex>.*)" - regex: true - function: files_file_matches_regex - -- then: files {filename1} and {filename2} match - function: files_match - -- then: file {filename} has same metadata as before - function: files_has_remembered_metadata - -- then: file {filename} has different metadata from before - function: files_has_different_metadata - -- then: file {filename} has changed from before - function: files_has_different_metadata - -- then: file {filename} has a very recent modification time - function: files_mtime_is_recent - -- then: file {filename} has a very old modification time - function: files_mtime_is_ancient diff --git a/subplot/vendored/runcmd.md b/subplot/vendored/runcmd.md deleted file mode 100644 index a9d4ed4..0000000 --- a/subplot/vendored/runcmd.md +++ /dev/null @@ -1,170 +0,0 @@ -# Introduction - -The [Subplot][] library `runcmd` for Python provides scenario steps -and their implementations for running Unix commands and examining the -results. The library consists of a bindings file `lib/runcmd.yaml` and -implementations in Python in `lib/runcmd.py`. There is no Bash -version. - -[Subplot]: https://subplot.liw.fi/ - -This document explains the acceptance criteria for the library and how -they're verified. It uses the steps and functions from the -`lib/runcmd` library. The scenarios all have the same structure: run a -command, then examine the exit code, standard output (stdout for -short), or standard error output (stderr) of the command. - -The scenarios use the Unix commands `/bin/true` and `/bin/false` to -generate exit codes, and `/bin/echo` to produce stdout. To generate -stderr, they use the little helper script below. - -~~~{#err.sh .file .sh .numberLines} -#!/bin/sh -echo "$@" 1>&2 -~~~ - -# Check exit code - -These scenarios verify the exit code. To make it easier to write -scenarios in language that flows more naturally, there are a couple of -variations. - -## Successful execution - -~~~scenario -when I run /bin/true -then exit code is 0 -and command is successful -~~~ - -## Failed execution - -~~~scenario -when I try to run /bin/false -then exit code is not 0 -and command fails -~~~ - -# Check output has what we want - -These scenarios verify that stdout or stderr do have something we want -to have. - -## Check stdout is exactly as wanted - -Note that the string is surrounded by double quotes to make it clear -to the reader what's inside. Also, C-style string escapes are -understood. - -~~~scenario -when I run /bin/echo hello, world -then stdout is exactly "hello, world\n" -~~~ - -## Check stderr is exactly as wanted - -~~~scenario -given helper script err.sh for runcmd -when I run sh err.sh hello, world -then stderr is exactly "hello, world\n" -~~~ - -## Check stdout using sub-string search - -Exact string comparisons are not always enough, so we can verify a -sub-string is in output. - -~~~scenario -when I run /bin/echo hello, world -then stdout contains "world\n" -and exit code is 0 -~~~ - -## Check stderr using sub-string search - -~~~scenario -given helper script err.sh for runcmd -when I run sh err.sh hello, world -then stderr contains "world\n" -~~~ - -## Check stdout using regular expressions - -Fixed strings are not always enough, so we can verify output matches a -regular expression. Note that the regular expression is not delimited -and does not get any C-style string escaped decoded. - -~~~scenario -when I run /bin/echo hello, world -then stdout matches regex world$ -~~~ - -## Check stderr using regular expressions - -~~~scenario -given helper script err.sh for runcmd -when I run sh err.sh hello, world -then stderr matches regex world$ -~~~ - -# Check output doesn't have what we want to avoid - -These scenarios verify that the stdout or stderr do not -have something we want to avoid. - -## Check stdout is not exactly something - -~~~scenario -when I run /bin/echo hi -then stdout isn't exactly "hello, world\n" -~~~ - -## Check stderr is not exactly something - -~~~scenario -given helper script err.sh for runcmd -when I run sh err.sh hi -then stderr isn't exactly "hello, world\n" -~~~ - -## Check stdout doesn't contain sub-string - -~~~scenario -when I run /bin/echo hi -then stdout doesn't contain "world" -~~~ - -## Check stderr doesn't contain sub-string - -~~~scenario -given helper script err.sh for runcmd -when I run sh err.sh hi -then stderr doesn't contain "world" -~~~ - -## Check stdout doesn't match regular expression - -~~~scenario -when I run /bin/echo hi -then stdout doesn't match regex world$ - -~~~ - -## Check stderr doesn't match regular expressions - -~~~scenario -given helper script err.sh for runcmd -when I run sh err.sh hi -then stderr doesn't match regex world$ -~~~ - - ---- -title: Acceptance criteria for the lib/runcmd Subplot library -author: The Subplot project -template: python -bindings: -- runcmd.yaml -functions: -- runcmd.py -... diff --git a/subplot/vendored/runcmd.py b/subplot/vendored/runcmd.py deleted file mode 100644 index a2564c6..0000000 --- a/subplot/vendored/runcmd.py +++ /dev/null @@ -1,252 +0,0 @@ -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") - - # The Subplot Python template empties os.environ at startup, modulo a small - # number of variables with carefully chosen values. Here, we don't need to - # care about what those variables are, but we do need to not overwrite - # them, so we just add anything in the env keyword argument, if any, to - # os.environ. - env = dict(os.environ) - for key, arg in kwargs.pop("env", {}).items(): - env[key] = arg - - 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/vendored/runcmd.yaml b/subplot/vendored/runcmd.yaml deleted file mode 100644 index 48dde90..0000000 --- a/subplot/vendored/runcmd.yaml +++ /dev/null @@ -1,83 +0,0 @@ -# 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 |