summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLars Wirzenius <liw@liw.fi>2022-01-03 06:16:07 +0000
committerLars Wirzenius <liw@liw.fi>2022-01-03 06:16:07 +0000
commit0734368d45b349a6ab433c1d41f35a95ea58c9ec (patch)
treeb69c6b84b9ce025d6f7afd92d734452757f4f365
parente8082fef03bd04042dad3fb2d0513587c54874ab (diff)
parentb9de4d5e810d07b679ad9ab9e46d58e4c76213cc (diff)
downloadsubplot-0734368d45b349a6ab433c1d41f35a95ea58c9ec.tar.gz
Merge branch 'more-rust-subplots' into 'main'
codegen: Refuse to generate code if the specified template is not present Closes #259 See merge request subplot/subplot!242
-rw-r--r--Cargo.lock1
-rwxr-xr-xcheck71
-rw-r--r--flake.nix8
-rw-r--r--share/rust/template/template.rs.tera1
-rw-r--r--src/doc.rs3
-rw-r--r--src/error.rs9
-rw-r--r--subplot.md2
-rw-r--r--subplot.yaml31
-rw-r--r--subplotlib/Cargo.toml7
-rw-r--r--subplotlib/build.rs5
-rw-r--r--subplotlib/files.md102
-rw-r--r--subplotlib/runcmd.md187
-rw-r--r--subplotlib/src/scenario.rs28
-rw-r--r--subplotlib/src/steplibrary/datadir.rs11
-rw-r--r--subplotlib/src/steplibrary/runcmd.rs15
-rw-r--r--subplotlib/subplot-rust-support.rs219
-rw-r--r--subplotlib/tests/subplot.rs1
-rw-r--r--tests/subplots/common/files.md (renamed from tests/python/files.md)4
-rw-r--r--tests/subplots/common/runcmd.md (renamed from tests/python/runcmd.md)6
-rw-r--r--tests/subplots/common/runcmd_test.py (renamed from tests/python/runcmd_test.py)0
-rw-r--r--tests/subplots/common/runcmd_test.rs25
-rw-r--r--tests/subplots/common/runcmd_test.yaml (renamed from tests/python/runcmd_test.yaml)6
-rw-r--r--tests/subplots/python/daemon.md (renamed from tests/python/daemon.md)0
23 files changed, 406 insertions, 336 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 494d6a9..79aeabe 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1159,6 +1159,7 @@ dependencies = [
"lazy_static",
"regex",
"remove_dir_all 0.7.0",
+ "serde_json",
"shell-words",
"state",
"subplot-build",
diff --git a/check b/check
index cb5ffc1..2582709 100755
--- a/check
+++ b/check
@@ -114,7 +114,7 @@ class Runcmd:
]
return subcommand in lines
- def codegen(self, md, output, **kwargs):
+ def codegen(self, md, template, output, **kwargs):
"""Run the Subplot code generator and the test program it produces"""
self.cargo(
[
@@ -124,6 +124,7 @@ class Runcmd:
"--",
f"--resources={os.path.abspath('share')}",
"codegen",
+ f"--template={template}",
md,
f"--output={output}",
],
@@ -160,7 +161,7 @@ class Runcmd:
**kwargs,
)
- def get_template(self, filename):
+ def get_templates(self, filename):
metadata = self.cargo(
[
"run",
@@ -181,7 +182,7 @@ class Runcmd:
if not impls:
sys.exit(f"{filename} does not specify a template")
impl_names = [name for name in impls.keys()]
- return impl_names[0]
+ return impl_names
def find_files(pattern, pred):
@@ -270,38 +271,40 @@ def check_subplots(r):
md = os.path.basename(md0)
base, _ = os.path.splitext(md)
- template = r.get_template(md0)
- if template == "python":
- test_py = os.path.join(output, f"test-{base}.py")
- test_log = os.path.join(output, f"test-{base}.log")
-
- # Remove test log from previous run, if any.
- if os.path.exists(test_log):
- os.remove(test_log)
-
- bindir = get_bin_dir(r)
-
- r.codegen(md, test_py, cwd=dirname)
- p = r.runcmd_unchecked(
- [
- "python3",
- test_py,
- "--log",
- test_log,
- f"--env=SUBPLOT_DIR={bindir}",
- ],
- cwd=dirname,
- )
- if p.returncode != 0:
+ for template in r.get_templates(md0):
+ if template == "python":
+ test_py = os.path.join(output, f"test-{base}.py")
+ test_log = os.path.join(output, f"test-{base}.log")
+
+ # Remove test log from previous run, if any.
if os.path.exists(test_log):
- tail(test_log)
- sys.exit(1)
- elif template == "bash":
- test_sh = os.path.join(output, f"test-{base}.sh")
- r.codegen(md, test_sh, cwd=dirname)
- r.runcmd(["bash", "-x", test_sh], cwd=dirname)
- else:
- sys.exit(f"unknown template {template} in {md0}")
+ os.remove(test_log)
+
+ bindir = get_bin_dir(r)
+
+ r.codegen(md, "python", test_py, cwd=dirname)
+ p = r.runcmd_unchecked(
+ [
+ "python3",
+ test_py,
+ "--log",
+ test_log,
+ f"--env=SUBPLOT_DIR={bindir}",
+ ],
+ cwd=dirname,
+ )
+ if p.returncode != 0:
+ if os.path.exists(test_log):
+ tail(test_log)
+ sys.exit(1)
+ elif template == "bash":
+ test_sh = os.path.join(output, f"test-{base}.sh")
+ r.codegen(md, "bash", test_sh, cwd=dirname)
+ r.runcmd(["bash", "-x", test_sh], cwd=dirname)
+ elif template == "rust":
+ r.msg(f"Ignoring Rust template in {md0}")
+ else:
+ sys.exit(f"unknown template {template} in {md0}")
base = os.path.basename(md)
base, _ = os.path.splitext(md)
diff --git a/flake.nix b/flake.nix
index cdd50e6..9f1aafe 100644
--- a/flake.nix
+++ b/flake.nix
@@ -30,11 +30,9 @@
(python3.withPackages test-python-packages)
black
];
- shellHook = ''
- export SUBPLOT_DOT_PATH=${pkgs.graphviz}/bin/dot
- export SUBPLOT_JAVA_PATH=${pkgs.jre}/bin/java
- export SUBPLOT_PLANTUML_JAR_PATH=${pkgs.plantuml}/lib/plantuml.jar
- '';
+ SUBPLOT_DOT_PATH = "${pkgs.graphviz}/bin/dot";
+ SUBPLOT_JAVA_PATH = "${pkgs.jre}/bin/java";
+ SUBPLOT_PLANTUML_JAR_PATH = "${pkgs.plantuml}/lib/plantuml.jar";
};
});
}
diff --git a/share/rust/template/template.rs.tera b/share/rust/template/template.rs.tera
index c94d63c..65fb755 100644
--- a/share/rust/template/template.rs.tera
+++ b/share/rust/template/template.rs.tera
@@ -28,6 +28,7 @@ lazy_static! {
// {{ scenario.title | commentsafe }}
#[test]
+#[allow(non_snake_case)]
fn {{ scenario.title | nameslug }}() {
let mut scenario = Scenario::new(&base64_decode("{{scenario.title | base64}}"));
{% for step in scenario.steps %}
diff --git a/src/doc.rs b/src/doc.rs
index 9989bc4..5c81e27 100644
--- a/src/doc.rs
+++ b/src/doc.rs
@@ -559,6 +559,9 @@ pub fn codegen(filename: &Path, output: &Path, template: Option<&str>) -> Result
.unwrap_or_else(|| doc.template())?
.to_string();
event!(Level::TRACE, ?template);
+ if !doc.meta().templates().any(|t| t == template) {
+ return Err(SubplotError::TemplateSupportNotPresent);
+ }
if !doc.check_named_files_exist(&template)?
|| !doc.check_matched_steps_have_impl(&template)
|| !doc.check_embedded_files_are_used(&template)?
diff --git a/src/error.rs b/src/error.rs
index ec88733..bcee4ff 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -125,6 +125,15 @@ pub enum SubplotError {
#[error("document has more than one template possibility")]
AmbiguousTemplate,
+ /// Document does not support the requested template
+ ///
+ /// The document YAML metadata does not specify support for the
+ /// stated template.
+ ///
+ /// To fix, specify a template which is provided for in the document.
+ #[error("document lacks specified template support")]
+ TemplateSupportNotPresent,
+
/// Pandoc AST is not JSON
///
/// Subplot acts as a Pandoc filter, and as part of that Pandoc
diff --git a/subplot.md b/subplot.md
index b1c500b..d2ec877 100644
--- a/subplot.md
+++ b/subplot.md
@@ -10,6 +10,8 @@ impls:
- subplot.py
- lib/files.py
- lib/runcmd.py
+ rust:
+ - subplotlib/subplot-rust-support.rs
classes:
- json
...
diff --git a/subplot.yaml b/subplot.yaml
index b865d83..12a9fae 100644
--- a/subplot.yaml
+++ b/subplot.yaml
@@ -3,60 +3,83 @@
python:
function: install_subplot
cleanup: uninstall_subplot
+ rust:
+ function: install_subplot
+ cleanup: uninstall_subplot
- then: scenario "{name:text}" was run
impl:
python:
function: scenario_was_run
+ rust:
+ function: scenario_was_run
- then: scenario "{name:text}" was not run
impl:
python:
function: scenario_was_not_run
+ rust:
+ function: scenario_was_not_run
- then: step "(?P<keyword>given|when|then) (?P<name>.+)" was run
impl:
python:
function: step_was_run
+ rust:
+ function: step_was_run
regex: true
- then: step "(?P<keyword1>given|when|then) (?P<name1>.+)" was run, and then step "(?P<keyword2>given|when|then) (?P<name2>.+)"
impl:
python:
function: step_was_run_and_then
+ rust:
+ function: step_was_run_and_then
regex: true
- then: cleanup for "(?P<keyword1>given|when|then) (?P<name1>.+)" was run, and then for "(?P<keyword2>given|when|then) (?P<name2>.+)"
impl:
python:
function: cleanup_was_run
+ rust:
+ function: cleanup_was_run
regex: true
- then: cleanup for "(?P<keyword>given|when|then) (?P<name>.+)" was not run
impl:
python:
function: cleanup_was_not_run
+ rust:
+ function: cleanup_was_not_run
regex: true
- then: JSON output matches {filename}
impl:
python:
function: json_output_matches_file
+ rust:
+ function: json_output_matches_file
- then: "{filename} does not end in a newline"
impl:
python:
function: file_ends_in_zero_newlines
+ rust:
+ function: file_ends_in_zero_newlines
- then: "{filename} ends in one newline"
impl:
python:
function: file_ends_in_one_newline
+ rust:
+ function: file_ends_in_one_newline
- then: "{filename} ends in two newlines"
impl:
python:
function: file_ends_in_two_newlines
+ rust:
+ function: file_ends_in_two_newlines
# In order to cope with low granularity filesystems, sometimes we need to wait
# for things to happen
@@ -64,6 +87,8 @@
impl:
python:
function: sleep_seconds
+ rust:
+ function: sleep_seconds
regex: true
types:
delay: uint
@@ -74,13 +99,19 @@
impl:
python:
function: do_nothing
+ rust:
+ function: do_nothing
- when: I do the required actions
impl:
python:
function: do_nothing
+ rust:
+ function: do_nothing
- then: the desired outcome is achieved
impl:
python:
function: do_nothing
+ rust:
+ function: do_nothing
diff --git a/subplotlib/Cargo.toml b/subplotlib/Cargo.toml
index 781a017..ed3fb8a 100644
--- a/subplotlib/Cargo.toml
+++ b/subplotlib/Cargo.toml
@@ -16,7 +16,7 @@ repository = "https://gitlab.com/subplot/subplot"
[dependencies]
fehler = "1"
-subplotlib-derive = { version="0.1", path = "../subplotlib-derive" }
+subplotlib-derive = { version = "0.1", path = "../subplotlib-derive" }
lazy_static = "1"
base64 = "0.13"
state = "0.5"
@@ -32,4 +32,7 @@ remove_dir_all = "0.7"
[build-dependencies]
glob = "0.3"
-subplot-build = { version="0.1", path = "../subplot-build" }
+subplot-build = { version = "0.1", path = "../subplot-build" }
+
+[dev-dependencies]
+serde_json = "1.0"
diff --git a/subplotlib/build.rs b/subplotlib/build.rs
index 5f94883..d1eeefd 100644
--- a/subplotlib/build.rs
+++ b/subplotlib/build.rs
@@ -17,9 +17,12 @@ use std::path::Path;
fn main() {
let subplots = glob("*.md").expect("failed to find subplots in subplotlib");
let tests = Path::new("tests");
+ let subplots = subplots.chain(Some(Ok("../subplot.md".into())));
+ let subplots = subplots
+ .chain(glob("../tests/subplots/common/*.md").expect("failed to find common subplots"));
for entry in subplots {
let entry = entry.expect("failed to get subplot dir entry in subplotlib");
- let mut inc = tests.join(&entry);
+ let mut inc = tests.join(&entry.file_name().unwrap());
inc.set_extension("rs");
if !inc.exists() {
panic!("missing include file: {}", inc.display());
diff --git a/subplotlib/files.md b/subplotlib/files.md
deleted file mode 100644
index 22e8f3c..0000000
--- a/subplotlib/files.md
+++ /dev/null
@@ -1,102 +0,0 @@
----
-title: Acceptance criteria for the files subplotlib step library
-author: The Subplot project
-bindings:
- - lib/files.yaml
-impls:
- rust: []
-...
-
-# 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 `subplotlib/steplibrary/files.yaml` and
-implementations in Rust as part of `subplotlib`.
-
-[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`
-step 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, .*/
-```
-
-# Directories
-
-There are also a large number of directory based steps and some directory
-based behaviour available in creating files which are available in the files
-library.
-
-```scenario
-given a directory first
-then directory first exists
-and directory first is empty
-and directory second does not exist
-when I remove directory first
-then directory first does not exist
-when I create directory second
-then directory second exists
-and directory second is empty
-given file second/third/hello.txt from hello.txt
-then directory second is not empty
-and directory second/third exists
-and directory second/third is not empty
-when I remove directory second
-then directory second does not exist
-```
diff --git a/subplotlib/runcmd.md b/subplotlib/runcmd.md
deleted file mode 100644
index f6b9d85..0000000
--- a/subplotlib/runcmd.md
+++ /dev/null
@@ -1,187 +0,0 @@
----
-title: Acceptance criteria for the runcmd step library for subplotlib.
-author: The Subplot project
-bindings:
- - lib/runcmd.yaml
- - lib/files.yaml
-impls:
- rust: []
-...
-
-# Introduction
-
-The [Subplot][] step library `runcmd` for Rust provides scenario steps
-and their implementations for running Unix commands and examining the
-results. The library consists of a bindings file `steplibrary/runcmd.yaml` and
-implementations inside the `subplotlib` crate itself.
-
-[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
-`steplibrary/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 `true` and `false` to
-generate exit codes, and `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 true
-then exit code is 0
-and command is successful
-```
-
-## Successful execution in a sub-directory
-
-```scenario
-given a directory xyzzy
-when I run, in xyzzy, pwd
-then exit code is 0
-then command is successful
-then stdout contains "/xyzzy"
-```
-
-## Failed execution
-
-```scenario
-when I try to run false
-then exit code is not 0
-and command fails
-```
-
-## Failed execution in a sub-directory
-
-```scenario
-given a directory xyzzy
-when I try to run, in xyzzy, 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 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 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 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 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 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 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$
-```
diff --git a/subplotlib/src/scenario.rs b/subplotlib/src/scenario.rs
index b239883..c0f3e87 100644
--- a/subplotlib/src/scenario.rs
+++ b/subplotlib/src/scenario.rs
@@ -284,6 +284,7 @@ impl Scenario {
// Firstly, we start all the contexts
let mut ret = Ok(());
let mut highest_start = None;
+ println!("Scenario Start: {}", self.contexts.title());
for (i, hook) in self.contexts.hooks.borrow().iter().enumerate() {
let res = hook.scenario_starts(&self.contexts);
if res.is_err() {
@@ -292,10 +293,14 @@ impl Scenario {
}
highest_start = Some(i);
}
-
+ println!(
+ "*** Context hooks returned {}",
+ if ret.is_ok() { "OK" } else { "Failure" }
+ );
if ret.is_ok() {
let mut highest = None;
for (i, step) in self.steps.iter().map(|(step, _)| step).enumerate() {
+ println!(" !!! Step {}", step.name());
let mut highest_prep = None;
for (i, prep) in self.contexts.hooks.borrow().iter().enumerate() {
let res = prep.step_starts(&self.contexts, step.name());
@@ -305,8 +310,17 @@ impl Scenario {
}
highest_prep = Some(i);
}
+ println!(
+ " *** Context hooks returned {}",
+ if ret.is_ok() { "OK" } else { "Failure" }
+ );
if ret.is_ok() {
+ println!(" >>> Run step function");
let res = step.call(&self.contexts, false);
+ println!(
+ " Step returned {}",
+ if res.is_ok() { "OK" } else { "Failure" }
+ );
if res.is_err() {
ret = res;
break;
@@ -314,6 +328,7 @@ impl Scenario {
highest = Some(i);
}
if let Some(n) = highest_prep {
+ println!(" *** Unwinding step contexts");
for hookn in (0..=n).rev() {
let res = self.contexts.hooks.borrow()[hookn].step_stops(&self.contexts);
ret = ret.and(res)
@@ -321,9 +336,15 @@ impl Scenario {
}
}
if let Some(n) = highest {
+ println!(" *** Running cleanup functions");
for stepn in (0..=n).rev() {
if let (_, Some(cleanup)) = &self.steps[stepn] {
+ println!(" >>> Cleanup {}", cleanup.name());
let res = cleanup.call(&self.contexts, true);
+ println!(
+ " Cleanup returned {}",
+ if res.is_ok() { "OK" } else { "Failure" }
+ );
ret = ret.and(res);
}
}
@@ -331,11 +352,16 @@ impl Scenario {
}
if let Some(n) = highest_start {
+ println!("*** Running scenario closedown");
for hookn in (0..=n).rev() {
let res = self.contexts.hooks.borrow()[hookn].scenario_stops(&self.contexts);
ret = ret.and(res);
}
}
+ println!(
+ "<<< Scenario returns {}",
+ if ret.is_ok() { "OK" } else { "Failure" }
+ );
ret
}
}
diff --git a/subplotlib/src/steplibrary/datadir.rs b/subplotlib/src/steplibrary/datadir.rs
index 8aa6f00..88e6375 100644
--- a/subplotlib/src/steplibrary/datadir.rs
+++ b/subplotlib/src/steplibrary/datadir.rs
@@ -91,6 +91,17 @@ impl Datadir {
.open(full_path)?
}
+ /// Open a file for reading
+ #[throws(StepError)]
+ pub fn open_read<S: AsRef<Path>>(&self, subpath: S) -> File {
+ let full_path = self.canonicalise_filename(subpath)?;
+ OpenOptions::new()
+ .create(false)
+ .write(false)
+ .read(true)
+ .open(full_path)?
+ }
+
#[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/runcmd.rs b/subplotlib/src/steplibrary/runcmd.rs
index f42df6e..5fc12eb 100644
--- a/subplotlib/src/steplibrary/runcmd.rs
+++ b/subplotlib/src/steplibrary/runcmd.rs
@@ -67,9 +67,24 @@ impl ContextElement for Runcmd {
}
impl Runcmd {
+ /// Prepend the given location to the run path
pub fn prepend_to_path<S: Into<OsString>>(&mut self, element: S) {
self.paths.push(element.into());
}
+
+ /// Retrieve the last run command's stdout as a string.
+ ///
+ /// This does a lossy conversion from utf8 so should always succeed.
+ pub fn stdout_as_string(&self) -> String {
+ String::from_utf8_lossy(&self.stdout).into_owned()
+ }
+
+ /// Retrieve the last run command's stderr as a string.
+ ///
+ /// This does a lossy conversion from utf8 so should always succeed.
+ pub fn stderr_as_string(&self) -> String {
+ String::from_utf8_lossy(&self.stderr).into_owned()
+ }
}
#[step]
diff --git a/subplotlib/subplot-rust-support.rs b/subplotlib/subplot-rust-support.rs
new file mode 100644
index 0000000..19a4ff7
--- /dev/null
+++ b/subplotlib/subplot-rust-support.rs
@@ -0,0 +1,219 @@
+// Rust support for running subplot-rust.md
+
+use subplotlib::steplibrary::datadir::Datadir;
+use subplotlib::steplibrary::runcmd::{self, Runcmd};
+
+use tempfile::TempDir;
+
+use std::io::{Read, Seek, SeekFrom};
+
+#[derive(Default)]
+struct SubplotContext {
+ bin_dir: Option<TempDir>,
+}
+
+impl ContextElement for SubplotContext {}
+
+#[step]
+fn do_nothing(context: &ScenarioContext) {
+ // Nothing to do here
+}
+
+#[step]
+#[context(SubplotContext)]
+#[context(Runcmd)]
+fn install_subplot(context: &ScenarioContext) {
+ if let Some(bindir) = std::env::var_os("SUBPLOT_DIR") {
+ println!("Found SUBPLOT_DIR environment variable, using that");
+ context.with_mut(
+ |rc: &mut Runcmd| {
+ rc.prepend_to_path(bindir);
+ Ok(())
+ },
+ false,
+ )?;
+ } else {
+ let bin_dir = TempDir::new()?;
+ println!("Creating temporary rundir at {}", bin_dir.path().display());
+
+ // Since we don't get CARGO_BIN_EXE_subplot when building a subcrate
+ // we retrieve the path to `subplot` via the assumption that integration
+ // tests are always located one dir down from the outer crate binaries.
+ let target_path = std::fs::canonicalize(
+ std::env::current_exe()
+ .expect("Cannot determine test exe path")
+ .parent()
+ .unwrap()
+ .join(".."),
+ )
+ .expect("Cannot canonicalise path to binaries");
+
+ let src_dir = env!("CARGO_MANIFEST_DIR");
+ for bin_name in &["subplot"] {
+ let file_path = bin_dir.path().join(bin_name);
+ std::fs::write(
+ &file_path,
+ format!(
+ r#"
+#!/bin/sh
+set -eu
+exec '{target_path}/{bin_name}' --resources '{src_dir}/share' "$@"
+"#,
+ target_path = target_path.display(),
+ bin_name = bin_name,
+ src_dir = src_dir,
+ ),
+ )?;
+ {
+ let mut perms = std::fs::metadata(&file_path)?.permissions();
+ use std::os::unix::fs::PermissionsExt;
+ perms.set_mode(perms.mode() | 0o111); // Set executable bit
+ std::fs::set_permissions(&file_path, perms)?;
+ }
+ }
+
+ context.with_mut(
+ |context: &mut Runcmd| {
+ context.prepend_to_path(bin_dir.path());
+ context.prepend_to_path(target_path);
+ Ok(())
+ },
+ false,
+ )?;
+ }
+}
+
+#[step]
+fn uninstall_subplot(context: &mut SubplotContext) {
+ context.bin_dir.take();
+}
+
+#[step]
+#[context(Runcmd)]
+fn scenario_was_run(context: &ScenarioContext, name: &str) {
+ let text = format!("\nscenario: {}\n", name);
+ runcmd::stdout_contains::call(context, &text)?;
+}
+
+#[step]
+#[context(Runcmd)]
+fn scenario_was_not_run(context: &ScenarioContext, name: &str) {
+ let text = format!("\nscenario: {}\n", name);
+ runcmd::stdout_doesnt_contain::call(context, &text)?;
+}
+
+#[step]
+#[context(Runcmd)]
+fn step_was_run(context: &ScenarioContext, keyword: &str, name: &str) {
+ let text = format!("\n step: {} {}\n", keyword, name);
+ runcmd::stdout_contains::call(context, &text)?;
+}
+
+#[step]
+#[context(Runcmd)]
+fn step_was_run_and_then(
+ context: &ScenarioContext,
+ keyword1: &str,
+ name1: &str,
+ keyword2: &str,
+ name2: &str,
+) {
+ let text = format!(
+ "\n step: {} {}\n step: {} {}",
+ keyword1, name1, keyword2, name2
+ );
+ runcmd::stdout_contains::call(context, &text)?;
+}
+
+#[step]
+#[context(Runcmd)]
+fn cleanup_was_run(
+ context: &ScenarioContext,
+ keyword1: &str,
+ name1: &str,
+ keyword2: &str,
+ name2: &str,
+) {
+ let text = format!(
+ "\n cleanup: {} {}\n cleanup: {} {}\n",
+ keyword1, name1, keyword2, name2
+ );
+ runcmd::stdout_contains::call(context, &text)?;
+}
+
+#[step]
+#[context(Runcmd)]
+fn cleanup_was_not_run(context: &ScenarioContext, keyword: &str, name: &str) {
+ let text = format!("\n cleanup: {} {}\n", keyword, name);
+ runcmd::stdout_doesnt_contain::call(context, &text)?;
+}
+
+#[throws(StepError)]
+fn end_of_file(context: &Datadir, filename: &str, nbytes: usize) -> Vec<u8> {
+ let mut fh = context.open_read(filename)?;
+ fh.seek(SeekFrom::End(-(nbytes as i64)))?;
+ let mut b = vec![0; nbytes];
+ fh.read_exact(&mut b[0..nbytes])?;
+ b
+}
+
+#[step]
+fn file_ends_in_zero_newlines(context: &Datadir, filename: &str) {
+ let b = end_of_file(context, filename, 1)?;
+ if b[0] == b'\n' {
+ throw!(format!("File {} ends in unexpected newline", filename));
+ }
+}
+
+#[step]
+fn file_ends_in_one_newline(context: &Datadir, filename: &str) {
+ let b = end_of_file(context, filename, 2)?;
+ if !(b[0] != b'\n' && b[1] == b'\n') {
+ throw!(format!(
+ "File {} does not end in exactly one newline",
+ filename
+ ));
+ }
+}
+
+#[step]
+fn file_ends_in_two_newlines(context: &Datadir, filename: &str) {
+ let b = end_of_file(context, filename, 2)?;
+ if b[0] != b'\n' || b[1] != b'\n' {
+ throw!(format!(
+ "File {} does not end in exactly two newlines",
+ filename
+ ));
+ }
+}
+
+#[step]
+fn sleep_seconds(context: &Datadir, delay: u64) {
+ std::thread::sleep(std::time::Duration::from_secs(delay));
+}
+
+#[step]
+#[context(Datadir)]
+#[context(Runcmd)]
+fn json_output_matches_file(context: &ScenarioContext, filename: &str) {
+ let output = context.with(|rc: &Runcmd| Ok(rc.stdout_as_string()), false)?;
+ let fcontent = context.with(
+ |dd: &Datadir| {
+ Ok(std::fs::read_to_string(
+ dd.canonicalise_filename(filename)?,
+ )?)
+ },
+ false,
+ )?;
+ let output: serde_json::Value = serde_json::from_str(&output)?;
+ let fcontent: serde_json::Value = serde_json::from_str(&fcontent)?;
+ println!("########");
+ println!("Output:\n{:#}", output);
+ println!("File:\n{:#}", fcontent);
+ println!("########");
+ assert_eq!(
+ output, fcontent,
+ "Command output does not match the content of {}",
+ filename
+ );
+}
diff --git a/subplotlib/tests/subplot.rs b/subplotlib/tests/subplot.rs
new file mode 100644
index 0000000..00accd7
--- /dev/null
+++ b/subplotlib/tests/subplot.rs
@@ -0,0 +1 @@
+include!(concat!(env!("OUT_DIR"), "/subplot.rs"));
diff --git a/tests/python/files.md b/tests/subplots/common/files.md
index 7837da6..13d9874 100644
--- a/tests/python/files.md
+++ b/tests/subplots/common/files.md
@@ -3,7 +3,8 @@
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`.
+implementations in Python in `lib/files.py` or in Rust within the
+`subplotlib` crate.
[Subplot]: https://subplot.liw.fi/
@@ -102,4 +103,5 @@ bindings:
impls:
python:
- lib/files.py
+ rust: []
...
diff --git a/tests/python/runcmd.md b/tests/subplots/common/runcmd.md
index 01e6904..4f66685 100644
--- a/tests/python/runcmd.md
+++ b/tests/subplots/common/runcmd.md
@@ -3,8 +3,8 @@
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.
+implementations in Python in `lib/runcmd.py` or in the Rust subplotlib
+step library. There is no Bash version.
[Subplot]: https://subplot.liw.fi/
@@ -211,4 +211,6 @@ impls:
- lib/runcmd.py
- runcmd_test.py
- lib/files.py
+ rust:
+ - runcmd_test.rs
...
diff --git a/tests/python/runcmd_test.py b/tests/subplots/common/runcmd_test.py
index 4aa5f49..4aa5f49 100644
--- a/tests/python/runcmd_test.py
+++ b/tests/subplots/common/runcmd_test.py
diff --git a/tests/subplots/common/runcmd_test.rs b/tests/subplots/common/runcmd_test.rs
new file mode 100644
index 0000000..7759e5f
--- /dev/null
+++ b/tests/subplots/common/runcmd_test.rs
@@ -0,0 +1,25 @@
+use subplotlib::steplibrary::files::{self, Datadir};
+use subplotlib::steplibrary::runcmd::Runcmd;
+
+#[cfg(unix)]
+use std::os::unix::fs::PermissionsExt;
+
+#[step]
+#[context(Datadir)]
+fn create_script_from_embedded(
+ context: &ScenarioContext,
+ filename: &str,
+ embedded: SubplotDataFile,
+) {
+ files::create_from_embedded_with_other_name::call(context, filename, embedded)?;
+ let filename = context.with(|dd: &Datadir| dd.canonicalise_filename(filename), false)?;
+ let mut perms = std::fs::symlink_metadata(&filename)?.permissions();
+ #[cfg(unix)]
+ perms.set_mode(perms.mode() | 0o111);
+ std::fs::set_permissions(&filename, perms)?;
+}
+
+#[step]
+fn prepend_to_path(context: &mut Runcmd, dirname: &str) {
+ context.prepend_to_path(dirname);
+}
diff --git a/tests/python/runcmd_test.yaml b/tests/subplots/common/runcmd_test.yaml
index 2ad981e..daab202 100644
--- a/tests/python/runcmd_test.yaml
+++ b/tests/subplots/common/runcmd_test.yaml
@@ -1,9 +1,13 @@
-- given: "executable script {filename} from {embedded}"
+- given: "executable script {filename} from {embedded:file}"
impl:
python:
function: create_script_from_embedded
+ rust:
+ function: create_script_from_embedded
- when: "I prepend {dirname} to PATH"
impl:
python:
function: runcmd_prepend_to_path
+ rust:
+ function: prepend_to_path
diff --git a/tests/python/daemon.md b/tests/subplots/python/daemon.md
index 51c77b4..51c77b4 100644
--- a/tests/python/daemon.md
+++ b/tests/subplots/python/daemon.md