summaryrefslogtreecommitdiff
path: root/subplotlib
diff options
context:
space:
mode:
authorDaniel Silverstone <dsilvers@digital-scurf.org>2022-02-26 11:04:04 +0000
committerDaniel Silverstone <dsilvers@digital-scurf.org>2022-02-26 11:04:04 +0000
commit791ed213a6e913d0750740a7be1dc96645584724 (patch)
treee174b23503ff7598c555368a0c0b38e33b5d98ce /subplotlib
parent7edacdf1a9362983492c19a6aff4622b84557822 (diff)
downloadsubplot-791ed213a6e913d0750740a7be1dc96645584724.tar.gz
(subplotlib): Improve step library documentation
Signed-off-by: Daniel Silverstone <dsilvers@digital-scurf.org>
Diffstat (limited to 'subplotlib')
-rw-r--r--subplotlib/src/steplibrary/datadir.rs21
-rw-r--r--subplotlib/src/steplibrary/files.rs129
-rw-r--r--subplotlib/src/steplibrary/runcmd.rs146
3 files changed, 296 insertions, 0 deletions
diff --git a/subplotlib/src/steplibrary/datadir.rs b/subplotlib/src/steplibrary/datadir.rs
index 88e6375..91f5603 100644
--- a/subplotlib/src/steplibrary/datadir.rs
+++ b/subplotlib/src/steplibrary/datadir.rs
@@ -10,6 +10,14 @@ use std::path::{Component, Path, PathBuf};
pub use crate::prelude::*;
+/// The `Datadir` is the context type which provides a directory for each scenario
+/// and allows for the creation and testing of files within that directory.
+///
+/// A few steps are provided as part of this step library, though in reality the
+/// majority of steps which interact with the data directory are in the
+/// [files][`crate::steplibrary::files`] step library, and commands which interact
+/// with stuff in here are in the [runcmd][`crate::steplibrary::runcmd`] step library.
+///
#[derive(Default)]
pub struct Datadir {
inner: Option<DatadirInner>,
@@ -57,11 +65,16 @@ impl Datadir {
/// Retrieve the base data directory path which can be used to store
/// files etc. for this step.
+ ///
+ /// This is used by steps wishing to manipulate the content of the data directory.
pub fn base_path(&self) -> &Path {
self.inner().base.path()
}
/// Canonicalise a subpath into this dir
+ ///
+ /// This step **safely** joins the base path to the given subpath. This ensures that,
+ /// for example, the subpath is relative, does not contain `..` elements, etc.
#[throws(StepError)]
pub fn canonicalise_filename<S: AsRef<Path>>(&self, subpath: S) -> PathBuf {
let mut ret = self.base_path().to_path_buf();
@@ -81,6 +94,8 @@ impl Datadir {
}
/// Open a file for writing
+ ///
+ /// This is a convenience function to open a file for writing at the given subpath.
#[throws(StepError)]
pub fn open_write<S: AsRef<Path>>(&self, subpath: S) -> File {
let full_path = self.canonicalise_filename(subpath)?;
@@ -92,6 +107,8 @@ impl Datadir {
}
/// Open a file for reading
+ ///
+ /// This is a convenience function to open a file for reading from the given subpath
#[throws(StepError)]
pub fn open_read<S: AsRef<Path>>(&self, subpath: S) -> File {
let full_path = self.canonicalise_filename(subpath)?;
@@ -102,6 +119,10 @@ impl Datadir {
.open(full_path)?
}
+ /// Make a directory tree
+ ///
+ /// Equivalent to `mkdir -p` this will create the full path to the given subpath
+ /// allowing subsequent step code to use that directory.
#[throws(StepError)]
pub fn create_dir_all<S: AsRef<Path>>(&self, subpath: S) {
let full_path = self.canonicalise_filename(subpath)?;
diff --git a/subplotlib/src/steplibrary/files.rs b/subplotlib/src/steplibrary/files.rs
index 35f841a..a49add1 100644
--- a/subplotlib/src/steplibrary/files.rs
+++ b/subplotlib/src/steplibrary/files.rs
@@ -26,6 +26,9 @@ pub use super::datadir::Datadir;
///
/// This context depends on, and will automatically register, the context for
/// the [`datadir`][crate::steplibrary::datadir] step library.
+///
+/// Because files can typically only be named in Subplot documents, we assume they
+/// all have names which can be rendered as utf-8 strings.
pub struct Files {
metadata: HashMap<String, Metadata>,
}
@@ -75,6 +78,12 @@ pub fn create_from_embedded_with_other_name(
.write_all(embedded_file.data())?;
}
+/// Touch a file to have a specific timestamp as its modified time
+///
+/// # `given file (?P<filename>\S+) has modification time (?P<mtime>\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})`
+///
+/// Sets the modification time for the given filename to the provided mtime.
+/// If the file does not exist, it will be created.
#[step]
pub fn touch_with_timestamp(context: &Datadir, filename: &str, mtime: &str) {
let ts = Utc.datetime_from_str(mtime, "%Y-%m-%d %H:%M:%S")?;
@@ -92,11 +101,22 @@ pub fn touch_with_timestamp(context: &Datadir, filename: &str, mtime: &str) {
filetime::set_file_mtime(full_path, mtime)?;
}
+/// Create a file with some given text as its content
+///
+/// # `when I write "(?P<text>.*)" to file (?P<filename>\S+)`
+///
+/// Create/replace the given file with the given content.
#[step]
pub fn create_from_text(context: &Datadir, text: &str, filename: &str) {
context.open_write(filename)?.write_all(text.as_bytes())?;
}
+/// Examine the given file and remember its metadata for later
+///
+/// # `when I remember metadata for file {filename}`
+///
+/// This step stores the metadata (mtime etc) for the given file into the
+/// context so that it can be retrieved later for testing against.
#[step]
#[context(Datadir)]
#[context(Files)]
@@ -115,6 +135,12 @@ pub fn remember_metadata(context: &ScenarioContext, filename: &str) {
)
}
+/// Touch a given file
+///
+/// # `when I touch file {filename}`
+///
+/// This will create the named file if it does not exist, and then it will ensure that the
+/// file's modification time is set to the current time.
#[step]
pub fn touch(context: &Datadir, filename: &str) {
let full_path = context.canonicalise_filename(filename)?;
@@ -130,6 +156,11 @@ pub fn touch(context: &Datadir, filename: &str) {
filetime::set_file_mtime(full_path, now)?;
}
+/// Check for a file
+///
+/// # `then file {filename} exists`
+///
+/// This simple step will succeed if the given filename exists in some sense.
#[step]
pub fn file_exists(context: &Datadir, filename: &str) {
let full_path = context.canonicalise_filename(filename)?;
@@ -145,6 +176,11 @@ pub fn file_exists(context: &Datadir, filename: &str) {
}
}
+/// Check for absence of a file
+///
+/// # `then file {filename} does not exist`
+///
+/// This simple step will succeed if the given filename does not exist in any sense.
#[step]
pub fn file_does_not_exist(context: &Datadir, filename: &str) {
let full_path = context.canonicalise_filename(filename)?;
@@ -160,6 +196,14 @@ pub fn file_does_not_exist(context: &Datadir, filename: &str) {
}
}
+/// Check if a set of files are the only files in the datadir
+///
+/// # `then only files (?P<filenames>.+) exist`
+///
+/// This step iterates the data directory and checks that **only** the named files exist.
+///
+/// Note: `filenames` is whitespace-separated, though any commas are removed as well.
+/// As such you cannot use this to test for filenames which contain commas.
#[step]
pub fn only_these_exist(context: &Datadir, filenames: &str) {
let filenames: HashSet<OsString> = filenames
@@ -173,6 +217,12 @@ pub fn only_these_exist(context: &Datadir, filenames: &str) {
assert_eq!(filenames, fnames);
}
+/// Check if a file contains a given sequence of characters
+///
+/// # `then file (?P<filename>\S+) contains "(?P<data>.*)"`
+///
+/// This will load the content of the named file and ensure it contains the given string.
+/// Note: this assumes everything is utf-8 encoded. If not, things will fail.
#[step]
pub fn file_contains(context: &Datadir, filename: &str, data: &str) {
let full_path = context.canonicalise_filename(filename)?;
@@ -182,6 +232,13 @@ pub fn file_contains(context: &Datadir, filename: &str, data: &str) {
}
}
+/// Check if a file's content matches the given regular expression
+///
+/// # `then file (?P<filename>\S+) matches regex /(?P<regex>.*)/`
+///
+/// This will load the content of th enamed file and ensure it contains data which
+/// matches the given regular expression. This step will fail if the file is not utf-8
+/// encoded, or if the regex fails to compile
#[step]
pub fn file_matches_regex(context: &Datadir, filename: &str, regex: &str) {
let full_path = context.canonicalise_filename(filename)?;
@@ -192,6 +249,11 @@ pub fn file_matches_regex(context: &Datadir, filename: &str, regex: &str) {
}
}
+/// Check if two files match
+///
+/// # `then files {filename1} and {filename2} match`
+///
+/// This loads the content of the given two files as **bytes** and checks they mach.
#[step]
pub fn file_match(context: &Datadir, filename1: &str, filename2: &str) {
let full_path1 = context.canonicalise_filename(filename1)?;
@@ -203,6 +265,17 @@ pub fn file_match(context: &Datadir, filename1: &str, filename2: &str) {
}
}
+/// Check if a given file's metadata matches our memory of it
+///
+/// # `then file {filename} has same metadata as before`
+///
+/// This confirms that the metadata we remembered for the given filename
+/// matches. Specifically this checks:
+///
+/// * Are the permissions the same
+/// * Are the modification times the same
+/// * Is the file's length the same
+/// * Is the file's type (file/dir) the same
#[step]
#[context(Datadir)]
#[context(Files)]
@@ -228,6 +301,17 @@ pub fn has_remembered_metadata(context: &ScenarioContext, filename: &str) {
}
}
+/// Check that a given file's metadata has changed since we remembered it
+///
+/// # `then file {filename} has different metadata from before`
+///
+/// This confirms that the metadata we remembered for the given filename
+/// does not matche. Specifically this checks:
+///
+/// * Are the permissions the same
+/// * Are the modification times the same
+/// * Is the file's length the same
+/// * Is the file's type (file/dir) the same
#[step]
#[context(Datadir)]
#[context(Files)]
@@ -253,6 +337,11 @@ pub fn has_different_metadata(context: &ScenarioContext, filename: &str) {
}
}
+/// Check if the given file has been modified "recently"
+///
+/// # `then file {filename} has a very recent modification time`
+///
+/// Specifically this checks that the given file has been modified in the past 5 seconds.
#[step]
pub fn mtime_is_recent(context: &Datadir, filename: &str) {
let full_path = context.canonicalise_filename(filename)?;
@@ -264,6 +353,11 @@ pub fn mtime_is_recent(context: &Datadir, filename: &str) {
}
}
+/// Check if the given file is very old
+///
+/// # `then file {filename} has a very old modification time`
+///
+/// Specifically this checks that the file was modified at least 39 years ago.
#[step]
pub fn mtime_is_ancient(context: &Datadir, filename: &str) {
let full_path = context.canonicalise_filename(filename)?;
@@ -275,17 +369,33 @@ pub fn mtime_is_ancient(context: &Datadir, filename: &str) {
}
}
+/// Make a directory
+///
+/// # `given a directory {path}`
+///
+/// This is the equivalent of `mkdir -p` within the data directory for the scenario.
#[step]
pub fn make_directory(context: &Datadir, path: &str) {
context.create_dir_all(path)?;
}
+/// Remove a directory
+///
+/// # `when I remove directory {path}`
+///
+/// This is the equivalent of `rm -rf` within the data directory for the scenario.
#[step]
pub fn remove_directory(context: &Datadir, path: &str) {
let full_path = context.canonicalise_filename(path)?;
remove_dir_all::remove_dir_all(full_path)?;
}
+/// Check that a directory exists
+///
+/// # `then directory {path} exists`
+///
+/// This ensures that the given path exists in the data directory for the scenario and
+/// that it is a directory itself.
#[step]
pub fn path_exists(context: &Datadir, path: &str) {
let full_path = context.canonicalise_filename(path)?;
@@ -297,6 +407,12 @@ pub fn path_exists(context: &Datadir, path: &str) {
}
}
+/// Check that a directory does not exist
+///
+/// # `then directory {path} does not exist`
+///
+/// This ensures that the given path does not exist in the data directory. If it exists
+/// and is not a directory, then this will also fail.
#[step]
pub fn path_does_not_exist(context: &Datadir, path: &str) {
let full_path = context.canonicalise_filename(path)?;
@@ -310,6 +426,12 @@ pub fn path_does_not_exist(context: &Datadir, path: &str) {
};
}
+/// Check that a directory exists and is empty
+///
+/// # `then directory {path} is empty`
+///
+/// This checks that the given path inside the data directory exists and is an
+/// empty directory itself.
#[step]
pub fn path_is_empty(context: &Datadir, path: &str) {
let full_path = context.canonicalise_filename(path)?;
@@ -321,6 +443,13 @@ pub fn path_is_empty(context: &Datadir, path: &str) {
}
}
+/// Check that a directory exists and is not empty
+///
+/// # `then directory {path} is not empty`
+///
+/// This checks that the given path inside the data directory exists and is a
+/// directory itself. The step also asserts that the given directory contains at least
+/// one entry.
#[step]
pub fn path_is_not_empty(context: &Datadir, path: &str) {
let full_path = context.canonicalise_filename(path)?;
diff --git a/subplotlib/src/steplibrary/runcmd.rs b/subplotlib/src/steplibrary/runcmd.rs
index 5fc12eb..aee675e 100644
--- a/subplotlib/src/steplibrary/runcmd.rs
+++ b/subplotlib/src/steplibrary/runcmd.rs
@@ -12,6 +12,10 @@ use std::io::Write;
use std::path::PathBuf;
use std::process::{Command, Stdio};
+/// The Runcmd context gives a step function access to the ability to run
+/// subprocesses as part of a scenario. These subprocesses are run with
+/// various environment variables set, and we record the stdout/stderr
+/// of the most recent-to-run command for testing purposes.
#[derive(Default)]
pub struct Runcmd {
env: HashMap<OsString, OsString>,
@@ -87,6 +91,14 @@ impl Runcmd {
}
}
+/// Ensure the given data file is available as a script in the data dir
+///
+/// # `given helper script {script} for runcmd`
+///
+/// ## Note
+///
+/// Currently this does not make the script file executable, so you will
+/// need to invoke it by means of an interpreter
#[step]
pub fn helper_script(context: &Datadir, script: SubplotDataFile) {
context
@@ -94,11 +106,26 @@ pub fn helper_script(context: &Datadir, script: SubplotDataFile) {
.write_all(script.data())?;
}
+/// Ensure that the base source directory is in the `PATH` for subsequent
+/// commands being run.
+///
+/// # `given srcdir is in the PATH`
+///
+/// This inserts the `CARGO_MANIFEST_DIR` into the `PATH` environment
+/// variable at the front.
#[step]
pub fn helper_srcdir_path(context: &mut Runcmd) {
context.prepend_to_path(env!("CARGO_MANIFEST_DIR"));
}
+/// Run the given command, ensuring it succeeds
+///
+/// # `when I run {argv0}{args:text}`
+///
+/// This will run the given command, with the given arguments,
+/// in the "current" directory (from where the tests were invoked)
+/// Once the command completes, this will check that it exited with
+/// a zero code (success).
#[step]
#[context(Datadir)]
#[context(Runcmd)]
@@ -107,6 +134,15 @@ pub fn run(context: &ScenarioContext, argv0: &str, args: &str) {
exit_code_is::call(context, 0)?;
}
+/// Run the given command in the given subpath of the data directory,
+/// ensuring that it succeeds.
+///
+/// # `when I run, in {dirname}, {argv0}{args:text}`
+///
+/// Like `run`, this will execute the given command, but this time it
+/// will set the working directory to the given subpath of the data dir.
+/// Once the command completes, this will check that it exited with
+/// a zero code (success)
#[step]
#[context(Datadir)]
#[context(Runcmd)]
@@ -115,6 +151,12 @@ pub fn run_in(context: &ScenarioContext, dirname: &str, argv0: &str, args: &str)
exit_code_is::call(context, 0)?;
}
+/// Run the given command
+///
+/// # `when I try to run {argv0}{args:text}`
+///
+/// This will run the given command, with the given arguments,
+/// in the "current" directory (from where the tests were invoked)
#[step]
#[context(Datadir)]
#[context(Runcmd)]
@@ -122,6 +164,12 @@ pub fn try_to_run(context: &ScenarioContext, argv0: &str, args: &str) {
try_to_run_in::call(context, USE_CWD, argv0, args)?;
}
+/// Run the given command in the given subpath of the data directory
+///
+/// # `when I try to run, in {dirname}, {argv0}{args:text}`
+///
+/// Like `try_to_run`, this will execute the given command, but this time it
+/// will set the working directory to the given subpath of the data dir.
#[step]
#[context(Datadir)]
#[context(Runcmd)]
@@ -193,6 +241,12 @@ pub fn try_to_run_in(context: &ScenarioContext, dirname: &str, argv0: &str, args
)?;
}
+/// Check that an executed command returns a specific exit code
+///
+/// # `then exit code is {exit}`
+///
+/// Check that the exit code of the previously run command matches
+/// the given value. Typically zero is success.
#[step]
pub fn exit_code_is(context: &Runcmd, exit: i32) {
if context.exitcode != Some(exit) {
@@ -203,6 +257,12 @@ pub fn exit_code_is(context: &Runcmd, exit: i32) {
}
}
+/// Check that an executed command returns a specific exit code
+///
+/// # `then exit code is not {exit}`
+///
+/// Check that the exit code of the previously run command does not
+/// matche the given value.
#[step]
pub fn exit_code_is_not(context: &Runcmd, exit: i32) {
if context.exitcode.is_none() || context.exitcode == Some(exit) {
@@ -210,12 +270,22 @@ pub fn exit_code_is_not(context: &Runcmd, exit: i32) {
}
}
+/// Check that an executed command succeeded
+///
+/// # `then command is successful`
+///
+/// This is equivalent to `then exit code is 0`
#[step]
#[context(Runcmd)]
pub fn exit_code_is_zero(context: &ScenarioContext) {
exit_code_is::call(context, 0)
}
+/// Check that an executed command did not succeed
+///
+/// # `then command fails`
+///
+/// This is equivalent to `then exit code is not 0`
#[step]
#[context(Runcmd)]
pub fn exit_code_is_nonzero(context: &ScenarioContext) {
@@ -256,6 +326,12 @@ fn check_matches(runcmd: &Runcmd, which: Stream, how: MatchKind, against: &str)
}
}
+/// Check that the stdout of the command matches exactly
+///
+/// # `then stdout is exactly "{text:text}"`
+///
+/// This will check exactly that the stdout of the command matches the given
+/// text. This assumes the command outputs valid utf-8 and decodes it as such.
#[step]
pub fn stdout_is(runcmd: &Runcmd, text: &str) {
if !check_matches(runcmd, Stream::Stdout, MatchKind::Exact, text)? {
@@ -263,6 +339,12 @@ pub fn stdout_is(runcmd: &Runcmd, text: &str) {
}
}
+/// Check that the stdout of the command is exactly not a given value
+///
+/// # `then stdout isn't exactly "{text:text}"`
+///
+/// This will check exactly that the stdout of the command does not match the given
+/// text. This assumes the command outputs valid utf-8 and decodes it as such.
#[step]
pub fn stdout_isnt(runcmd: &Runcmd, text: &str) {
if check_matches(runcmd, Stream::Stdout, MatchKind::Exact, text)? {
@@ -270,6 +352,12 @@ pub fn stdout_isnt(runcmd: &Runcmd, text: &str) {
}
}
+/// Check that the stderr of the command matches exactly
+///
+/// # `then stderr is exactly "{text:text}"`
+///
+/// This will check exactly that the stderr of the command matches the given
+/// text. This assumes the command outputs valid utf-8 and decodes it as such.
#[step]
pub fn stderr_is(runcmd: &Runcmd, text: &str) {
if !check_matches(runcmd, Stream::Stderr, MatchKind::Exact, text)? {
@@ -277,6 +365,12 @@ pub fn stderr_is(runcmd: &Runcmd, text: &str) {
}
}
+/// Check that the stderr of the command is exactly not a given value
+///
+/// # `then stderr isn't exactly "{text:text}"`
+///
+/// This will check exactly that the stderr of the command does not match the given
+/// text. This assumes the command outputs valid utf-8 and decodes it as such.
#[step]
pub fn stderr_isnt(runcmd: &Runcmd, text: &str) {
if check_matches(runcmd, Stream::Stderr, MatchKind::Exact, text)? {
@@ -284,6 +378,12 @@ pub fn stderr_isnt(runcmd: &Runcmd, text: &str) {
}
}
+/// Check that the stdout of the command contains a given string
+///
+/// # `then stdout contains "{text:text}"`
+///
+/// This will check that the stdout of the command contains the given substring. This
+/// assumes the command outputs valid utf-8 and decodes it as such.
#[step]
pub fn stdout_contains(runcmd: &Runcmd, text: &str) {
if !check_matches(runcmd, Stream::Stdout, MatchKind::Contains, text)? {
@@ -291,6 +391,12 @@ pub fn stdout_contains(runcmd: &Runcmd, text: &str) {
}
}
+/// Check that the stdout of the command does not contain a given string
+///
+/// # `then stdout doesn't contain "{text:text}"`
+///
+/// This will check that the stdout of the command does not contain the given substring. This
+/// assumes the command outputs valid utf-8 and decodes it as such.
#[step]
pub fn stdout_doesnt_contain(runcmd: &Runcmd, text: &str) {
if check_matches(runcmd, Stream::Stdout, MatchKind::Contains, text)? {
@@ -298,6 +404,12 @@ pub fn stdout_doesnt_contain(runcmd: &Runcmd, text: &str) {
}
}
+/// Check that the stderr of the command contains a given string
+///
+/// # `then stderr contains "{text:text}"`
+///
+/// This will check that the stderr of the command contains the given substring. This
+/// assumes the command outputs valid utf-8 and decodes it as such.
#[step]
pub fn stderr_contains(runcmd: &Runcmd, text: &str) {
if !check_matches(runcmd, Stream::Stderr, MatchKind::Contains, text)? {
@@ -305,6 +417,12 @@ pub fn stderr_contains(runcmd: &Runcmd, text: &str) {
}
}
+/// Check that the stderr of the command does not contain a given string
+///
+/// # `then stderr doesn't contain "{text:text}"`
+///
+/// This will check that the stderr of the command does not contain the given substring. This
+/// assumes the command outputs valid utf-8 and decodes it as such.
#[step]
pub fn stderr_doesnt_contain(runcmd: &Runcmd, text: &str) {
if check_matches(runcmd, Stream::Stderr, MatchKind::Contains, text)? {
@@ -312,6 +430,13 @@ pub fn stderr_doesnt_contain(runcmd: &Runcmd, text: &str) {
}
}
+/// Check that the stdout of the command matches a given regular expression
+///
+/// # `then stdout matches regex {regex:text}`
+///
+/// This will check that the stdout of the command matches the given regular expression.
+/// This will fail if the regular expression is bad, or if the command did not output
+/// valid utf-8 to be decoded.
#[step]
pub fn stdout_matches_regex(runcmd: &Runcmd, regex: &str) {
if !check_matches(runcmd, Stream::Stdout, MatchKind::Regex, regex)? {
@@ -319,6 +444,13 @@ pub fn stdout_matches_regex(runcmd: &Runcmd, regex: &str) {
}
}
+/// Check that the stdout of the command does not match a given regular expression
+///
+/// # `then stdout doesn't match regex {regex:text}`
+///
+/// This will check that the stdout of the command fails to match the given regular expression.
+/// This will fail if the regular expression is bad, or if the command did not output
+/// valid utf-8 to be decoded.
#[step]
pub fn stdout_doesnt_match_regex(runcmd: &Runcmd, regex: &str) {
if check_matches(runcmd, Stream::Stdout, MatchKind::Regex, regex)? {
@@ -326,6 +458,13 @@ pub fn stdout_doesnt_match_regex(runcmd: &Runcmd, regex: &str) {
}
}
+/// Check that the stderr of the command matches a given regular expression
+///
+/// # `then stderr matches regex {regex:text}`
+///
+/// This will check that the stderr of the command matches the given regular expression.
+/// This will fail if the regular expression is bad, or if the command did not output
+/// valid utf-8 to be decoded.
#[step]
pub fn stderr_matches_regex(runcmd: &Runcmd, regex: &str) {
if !check_matches(runcmd, Stream::Stderr, MatchKind::Regex, regex)? {
@@ -333,6 +472,13 @@ pub fn stderr_matches_regex(runcmd: &Runcmd, regex: &str) {
}
}
+/// Check that the stderr of the command does not match a given regular expression
+///
+/// # `then stderr doesn't match regex {regex:text}`
+///
+/// This will check that the stderr of the command fails to match the given regular expression.
+/// This will fail if the regular expression is bad, or if the command did not output
+/// valid utf-8 to be decoded.
#[step]
pub fn stderr_doesnt_match_regex(runcmd: &Runcmd, regex: &str) {
if check_matches(runcmd, Stream::Stderr, MatchKind::Regex, regex)? {