diff options
author | Daniel Silverstone <dsilvers@digital-scurf.org> | 2022-02-26 11:04:04 +0000 |
---|---|---|
committer | Daniel Silverstone <dsilvers@digital-scurf.org> | 2022-02-26 11:04:04 +0000 |
commit | 791ed213a6e913d0750740a7be1dc96645584724 (patch) | |
tree | e174b23503ff7598c555368a0c0b38e33b5d98ce /subplotlib | |
parent | 7edacdf1a9362983492c19a6aff4622b84557822 (diff) | |
download | subplot-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.rs | 21 | ||||
-rw-r--r-- | subplotlib/src/steplibrary/files.rs | 129 | ||||
-rw-r--r-- | subplotlib/src/steplibrary/runcmd.rs | 146 |
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)? { |