diff options
-rw-r--r-- | .gitignore | 5 | ||||
-rw-r--r-- | Cargo.lock | 149 | ||||
-rw-r--r-- | Cargo.toml | 6 | ||||
-rw-r--r-- | NEWS | 3 | ||||
-rw-r--r-- | bumper.md | 99 | ||||
-rwxr-xr-x | check | 35 | ||||
-rw-r--r-- | src/bin/bumper.rs | 3 | ||||
-rw-r--r-- | src/lib.rs | 8 | ||||
-rw-r--r-- | subplot/bumper.py | 9 | ||||
-rw-r--r-- | subplot/bumper.yaml | 2 | ||||
-rw-r--r-- | subplot/vendored/files.py | 194 | ||||
-rw-r--r-- | subplot/vendored/files.yaml | 83 | ||||
-rw-r--r-- | subplot/vendored/runcmd.py | 252 | ||||
-rw-r--r-- | subplot/vendored/runcmd.yaml | 83 |
14 files changed, 923 insertions, 8 deletions
@@ -1,2 +1,5 @@ /target -Cargo.lock +*.pdf +*.html +test.log +test.py diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..1b89008 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,149 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "anyhow" +version = "1.0.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81cddc5f91628367664cc7c69714ff08deee8a3efc54623011c772544d7b2767" + +[[package]] +name = "either" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" + +[[package]] +name = "git" +version = "0.1.0" +dependencies = [ + "anyhow", + "pandoc", + "pandoc_ast", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "itertools" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f56a2d0bc861f9165be4eb3442afd3c236d8a98afd426f65d92324ae1091a484" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" + +[[package]] +name = "pandoc" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84060e442eb742cdba4038302c9924aecd67434073b9785dd9a6cec8a3f4065a" +dependencies = [ + "itertools", +] + +[[package]] +name = "pandoc_ast" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a940e63c65b34a7be2f847de6847b4bc9b68d74e3f7a5c648ca2fa5317f3bd06" +dependencies = [ + "serde", + "serde_derive", + "serde_json", +] + +[[package]] +name = "proc-macro2" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quote" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "ryu" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" + +[[package]] +name = "serde" +version = "1.0.124" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd761ff957cb2a45fbb9ab3da6512de9de55872866160b23c25f1a841e99d29f" + +[[package]] +name = "serde_derive" +version = "1.0.124" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1800f7693e94e186f5e25a28291ae1570da908aff7d97a095dec1e56ff99069b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799e97dc9fdae36a5c8b8f2cae9ce2ee9fdce2058c57a93e6099d919fd982f79" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "syn" +version = "1.0.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fd9d1e9976102a03c542daa2eff1b43f9d72306342f3f8b3ed5fb8908195d6f" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "thiserror" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0f4a65597094d4483ddaed134f409b2cb7c1beccf25201a9f73c719254fa98e" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7765189610d8241a44529806d6fd1f2e0a08734313a35d5b3a556f92b381f3c0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-xid" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" @@ -7,3 +7,9 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +anyhow = "1" +pandoc = "0.8.4" +pandoc_ast = "0.8.0" +serde = "1.0.124" +serde_json = "1.0.64" +thiserror = "1" @@ -0,0 +1,3 @@ +# Release notes for Bumper + +## Version 1.0, not yet released diff --git a/bumper.md b/bumper.md new file mode 100644 index 0000000..fcf4005 --- /dev/null +++ b/bumper.md @@ -0,0 +1,99 @@ +# Introduction + +Bumper is a small utility for updating the version number when making +a release of a software project. It updates the version number in the +various locations in which it is stored in the source tree, creates a +git tag for indicating the release, then updates the source tree again +with a version number that indicates an unreleased version. + +The tool is used like this: + +~~~{.numberLines} +$ cd ~/my-project +$ bumper 1.2.0 +Rust project my-project version set to 1.2.0 +Debian package version set to 1.2.0-1 +Release tagged as v1.2.0 +$ git push --tags +... +$ +~~~ + +In other words: you run Bumper and tell it the version of the release +you're making. Bumper sniffs out what kind of project it is, and sets +the version number in the right places. It then creates a git tag for +that release. It's up to you to push the changes and tag, or to build +the release: Bumper only makes local changes. + +# Overview + +Bumper makes several assumptions about the project and its source +tree, to keep things simple. + +* Version numbers for releases use a format of X.Y.Z, where each + component is an integer. For example, 12.7.999. There can be any + number of components. + +* The source is stored in git. No other version control systems are + supported, since the author uses nothing else. (If you would like + support for other systems, please help.) + +* Python projects store the version number in a file `version.py` in a + subdirectory. The project build system uses that file as the source + of the version number. + +* Rust projects store the version number in the `Cargo.toml` file. The + build system gets it from there. + +* Debian packages have a `debian/changelog`, which specifies the + package's version number. + +* Release notes are in a markdown file called `NEWS`. + +* Releases are marked with signed, annotated git tags named after the + release version with a `v` prefix. + +Bumper looks for all the files mentioned above, and updates or +overwrites them to set the version number, and then commits all +changes. It creates a release tag for the commit. It then updates the +same files to add a "`+git`" suffix to the version number, to help +distinguish release versions from development versions. + +# Acceptance criteria + +This chapter has scenarios to express acceptance criteria and their +verification. + +## Does nothing if there are no files to update + +This scenario verifies that Bumper doesn't change anything if it +doesn't find any files it knows about. + +~~~scenario +given an installed Bumper +given file random.txt +when I try to run bumper 1.2.3 +then command fails +then only files random.txt exist +~~~ + +~~~{#random.txt .file} +Some random text file Bumper knows nothing about. +~~~ + + + +--- +title: bumper – set version number for a project +author: Lars Wirzenius +documentclass: report +template: python +bindings: +- subplot/bumper.yaml +- subplot/vendored/files.yaml +- subplot/vendored/runcmd.yaml +functions: +- subplot/bumper.py +- subplot/vendored/files.py +- subplot/vendored/runcmd.py +... @@ -0,0 +1,35 @@ +#!/bin/sh +# +# Run the automated tests for the project. + +set -eu + +hideok=chronic +if [ "$#" -gt 0 ] +then + case "$1" in + verbose | -v | --verbose) + hideok= + shift + ;; + esac +fi + +got_cargo_cmd() +{ + cargo --list | grep " $1 " > /dev/null +} + +got_cargo_cmd clippy && cargo clippy -q +$hideok cargo build --all-targets +got_cargo_cmd fmt && $hideok cargo fmt -- --check +$hideok cargo test + +sp-docgen bumper.md -o bumper.html +sp-docgen bumper.md -o bumper.pdf + +sp-codegen bumper.md -o test.py +rm -f test.log +$hideok python3 test.py --log test.log "$@" + +echo "Everything seems to be in order." diff --git a/src/bin/bumper.rs b/src/bin/bumper.rs new file mode 100644 index 0000000..081ca86 --- /dev/null +++ b/src/bin/bumper.rs @@ -0,0 +1,3 @@ +fn main() { + std::process::exit(1); +} @@ -1,7 +1 @@ -#[cfg(test)] -mod tests { - #[test] - fn it_works() { - assert_eq!(2 + 2, 4); - } -} + diff --git a/subplot/bumper.py b/subplot/bumper.py new file mode 100644 index 0000000..66d2564 --- /dev/null +++ b/subplot/bumper.py @@ -0,0 +1,9 @@ +import os + + +def install_bumper(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")) diff --git a/subplot/bumper.yaml b/subplot/bumper.yaml new file mode 100644 index 0000000..f74c28e --- /dev/null +++ b/subplot/bumper.yaml @@ -0,0 +1,2 @@ +- given: "an installed Bumper" + function: install_bumper diff --git a/subplot/vendored/files.py b/subplot/vendored/files.py new file mode 100644 index 0000000..dd5b9f8 --- /dev/null +++ b/subplot/vendored/files.py @@ -0,0 +1,194 @@ +import logging +import os +import re +import shutil +import time + + +def files_create_from_embedded(ctx, filename=None): + files_make_directory(ctx, path=os.path.dirname(filename) or ".") + 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"] + + files_make_directory(ctx, path=os.path.dirname(filename_on_disk) or ".") + with open(filename_on_disk, "wb") as f: + f.write(get_file(embedded_filename)) + + +def files_create_from_text(ctx, filename=None, text=None): + files_make_directory(ctx, path=os.path.dirname(filename) or ".") + with open(filename, "w") as f: + f.write(text) + + +def files_make_directory(ctx, path=None): + path = "./" + path + if not os.path.exists(path): + os.makedirs(path) + + +def files_remove_directory(ctx, path=None): + path = "./" + path + shutil.rmtree(path) + + +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_directory_exists(ctx, path=None): + assert_eq = globals()["assert_eq"] + assert_eq(os.path.isdir(path), True) + + +def files_directory_does_not_exist(ctx, path=None): + assert_eq = globals()["assert_eq"] + assert_eq(os.path.isdir(path), False) + + +def files_directory_is_empty(ctx, path=None): + assert_eq = globals()["assert_eq"] + assert_eq(os.listdir(path), []) + + +def files_directory_is_not_empty(ctx, path=None): + assert_ne = globals()["assert_ne"] + assert_ne(os.listdir(path), 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 new file mode 100644 index 0000000..f18b8cd --- /dev/null +++ b/subplot/vendored/files.yaml @@ -0,0 +1,83 @@ +- 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 + +- given: a directory {path} + function: files_make_directory + +- when: I create directory {path} + function: files_make_directory + +- when: I remove directory {path} + function: files_remove_directory + +- then: directory {path} exists + function: files_directory_exists + +- then: directory {path} does not exist + function: files_directory_does_not_exist + +- then: directory {path} is empty + function: files_directory_is_empty + +- then: directory {path} is not empty + function: files_directory_is_not_empty diff --git a/subplot/vendored/runcmd.py b/subplot/vendored/runcmd.py new file mode 100644 index 0000000..a2564c6 --- /dev/null +++ b/subplot/vendored/runcmd.py @@ -0,0 +1,252 @@ +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 new file mode 100644 index 0000000..48dde90 --- /dev/null +++ b/subplot/vendored/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 |