summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDaniel Silverstone <dsilvers+gitlab@digital-scurf.org>2023-11-11 10:49:14 +0000
committerDaniel Silverstone <dsilvers+gitlab@digital-scurf.org>2023-11-11 10:49:14 +0000
commit16521f5e92c06c978c148aa69bd5223eb126aea5 (patch)
tree9b060a977b768ebc7ce42338fd82076f815c1d6b
parentdccf8d75321fca24caa8efd3a6eb41bc7af15f2f (diff)
parent6a8a210de12bb9e79d9d610a7ca091567dc9550a (diff)
downloadsubplot-16521f5e92c06c978c148aa69bd5223eb126aea5.tar.gz
Merge branch 'liw/libdoc' into 'main'
feat: generate library documentation from bindings files Closes #341 See merge request subplot/subplot!362
-rw-r--r--share/common/lib/runcmd.yaml65
-rw-r--r--src/bin/subplot.rs89
-rw-r--r--src/bindings.rs51
-rw-r--r--tests/bindings-ubm.rs2
4 files changed, 191 insertions, 16 deletions
diff --git a/share/common/lib/runcmd.yaml b/share/common/lib/runcmd.yaml
index 7be2c05..0a387ed 100644
--- a/share/common/lib/runcmd.yaml
+++ b/share/common/lib/runcmd.yaml
@@ -8,6 +8,8 @@
function: subplotlib::steplibrary::runcmd::helper_script
types:
script: file
+ doc: |
+ FIXME.
- given: srcdir is in the PATH
impl:
@@ -15,6 +17,10 @@
function: runcmd_helper_srcdir_path
rust:
function: subplotlib::steplibrary::runcmd::helper_srcdir_path
+ doc: |
+ Make sure the source directory of the project being testes is on
+ the shell PATH. This makes it easy for tests to invoke programs
+ from the source tree.
- when: I run {argv0}{args:text}
impl:
@@ -22,6 +28,8 @@
function: runcmd_step
rust:
function: subplotlib::steplibrary::runcmd::run
+ doc: |
+ Run a program, and make sure it succeeds.
- when: I run, in {dirname}, {argv0}{args}
impl:
@@ -33,6 +41,9 @@
dirname: path
argv0: word
args: text
+ doc: |
+ Change to a different directory and run a program, and make sure
+ it succeeds;
- when: I try to run {argv0}{args:text}
impl:
@@ -40,6 +51,9 @@
function: runcmd_try_to_run
rust:
function: subplotlib::steplibrary::runcmd::try_to_run
+ doc: |
+ Run a program, but allow it to fail. Other steps can check if it
+ succeeded.
- when: I try to run, in {dirname}, {argv0}{args}
impl:
@@ -51,6 +65,9 @@
dirname: path
argv0: word
args: text
+ doc: |
+ Change to a different directory and run a program, but allow it to
+ fail. Other steps can check if it succeeded.
# Steps to examine exit code of latest command.
@@ -62,6 +79,9 @@
function: subplotlib::steplibrary::runcmd::exit_code_is
types:
exit: int
+ doc: |
+ Make sure the latest command run by `lib/runcmd` had a specific
+ exit code.
- then: exit code is not {exit}
impl:
@@ -71,6 +91,9 @@
function: subplotlib::steplibrary::runcmd::exit_code_is_not
types:
exit: int
+ doc: |
+ Make sure the latest command run by `lib/runcmd` did not have a
+ specific exit code.
- then: command is successful
impl:
@@ -78,6 +101,9 @@
function: runcmd_exit_code_is_zero
rust:
function: subplotlib::steplibrary::runcmd::exit_code_is_zero
+ doc: |
+ Make sure the latest command run by `lib/runcmd` indicated the
+ command succeeded.
- then: command fails
impl:
@@ -85,6 +111,9 @@
function: runcmd_exit_code_is_nonzero
rust:
function: subplotlib::steplibrary::runcmd::exit_code_is_nonzero
+ doc: |
+ Make sure the latest command run by `lib/runcmd` indicated the
+ command failed.
# Steps to examine stdout/stderr for exact content.
@@ -94,6 +123,9 @@
function: runcmd_stdout_is
rust:
function: subplotlib::steplibrary::runcmd::stdout_is
+ doc: |
+ Make sure the standard output of the latest command run by
+ `lib/runcmd` is exactly as desired.
- then: 'stdout isn''t exactly "{text:text}"'
impl:
@@ -101,6 +133,9 @@
function: runcmd_stdout_isnt
rust:
function: subplotlib::steplibrary::runcmd::stdout_isnt
+ doc: |
+ Make sure the standard output of the latest command run by
+ `lib/runcmd` is different from what is not wanted.
- then: stderr is exactly "{text:text}"
impl:
@@ -108,6 +143,9 @@
function: runcmd_stderr_is
rust:
function: subplotlib::steplibrary::runcmd::stderr_is
+ doc: |
+ Make sure the standard error output of the latest command run by
+ `lib/runcmd` is exactly as desired.
- then: 'stderr isn''t exactly "{text:text}"'
impl:
@@ -115,6 +153,9 @@
function: runcmd_stderr_isnt
rust:
function: subplotlib::steplibrary::runcmd::stderr_isnt
+ doc: |
+ Make sure the standard error output of the latest command run by
+ `lib/runcmd` is different from what is not wanted.
# Steps to examine stdout/stderr for sub-strings.
@@ -124,6 +165,9 @@
function: runcmd_stdout_contains
rust:
function: subplotlib::steplibrary::runcmd::stdout_contains
+ doc: |
+ Make sure the standard output of the latest command run by
+ `lib/runcmd` contains the desired sub-string.
- then: 'stdout doesn''t contain "{text:text}"'
impl:
@@ -131,6 +175,9 @@
function: runcmd_stdout_doesnt_contain
rust:
function: subplotlib::steplibrary::runcmd::stdout_doesnt_contain
+ doc: |
+ Make sure the standard output of the latest command run by
+ `lib/runcmd` does not contain the sub-string.
- then: stderr contains "{text:text}"
impl:
@@ -138,6 +185,9 @@
function: runcmd_stderr_contains
rust:
function: subplotlib::steplibrary::runcmd::stderr_contains
+ doc: |
+ Make sure the standard output of the latest command run by
+ `lib/runcmd` contains the desired sub-string.
- then: 'stderr doesn''t contain "{text:text}"'
impl:
@@ -145,6 +195,9 @@
function: runcmd_stderr_doesnt_contain
rust:
function: subplotlib::steplibrary::runcmd::stderr_doesnt_contain
+ doc: |
+ Make sure the standard error output of the latest command run by
+ `lib/runcmd` does not contain the sub-string.
# Steps to match stdout/stderr against regular expressions.
@@ -154,6 +207,9 @@
function: runcmd_stdout_matches_regex
rust:
function: subplotlib::steplibrary::runcmd::stdout_matches_regex
+ doc: |
+ Make sure the standard output of the latest command run by
+ `lib/runcmd` matches the desired regular expression.
- then: stdout doesn't match regex {regex:text}
impl:
@@ -161,6 +217,9 @@
function: runcmd_stdout_doesnt_match_regex
rust:
function: subplotlib::steplibrary::runcmd::stdout_doesnt_match_regex
+ doc: |
+ Make sure the standard output of the latest command run by
+ `lib/runcmd` does not match a regular expression.
- then: stderr matches regex {regex:text}
impl:
@@ -168,6 +227,9 @@
function: runcmd_stderr_matches_regex
rust:
function: subplotlib::steplibrary::runcmd::stderr_matches_regex
+ doc: |
+ Make sure the standard error output of the latest command run by
+ `lib/runcmd` matches the desired regular expression.
- then: stderr doesn't match regex {regex:text}
impl:
@@ -175,3 +237,6 @@
function: runcmd_stderr_doesnt_match_regex
rust:
function: subplotlib::steplibrary::runcmd::stderr_doesnt_match_regex
+ doc: |
+ Make sure the standard error output of the latest command run by
+ `lib/runcmd` does not match a regular expression.
diff --git a/src/bin/subplot.rs b/src/bin/subplot.rs
index 6c118d4..40f0f55 100644
--- a/src/bin/subplot.rs
+++ b/src/bin/subplot.rs
@@ -6,8 +6,8 @@ use anyhow::Result;
use env_logger::fmt::Color;
use log::{debug, error, info, trace, warn};
use subplot::{
- codegen, load_document, resource, Document, EmbeddedFile, MarkupOpts, Style, SubplotError,
- Warnings,
+ codegen, load_document, resource, Binding, Bindings, Document, EmbeddedFile, MarkupOpts, Style,
+ SubplotError, Warnings,
};
use time::{format_description::FormatItem, macros::format_description, OffsetDateTime};
@@ -58,6 +58,8 @@ enum Cmd {
Codegen(Codegen),
#[clap(hide = true)]
Resources(Resources),
+ #[clap(hide = true)]
+ Libdocgen(Libdocgen),
}
impl Cmd {
@@ -68,6 +70,7 @@ impl Cmd {
Cmd::Docgen(d) => d.run(),
Cmd::Codegen(c) => c.run(),
Cmd::Resources(r) => r.run(),
+ Cmd::Libdocgen(r) => r.run(),
}
}
@@ -78,6 +81,7 @@ impl Cmd {
Cmd::Docgen(d) => d.doc_path(),
Cmd::Codegen(c) => c.doc_path(),
Cmd::Resources(r) => r.doc_path(),
+ Cmd::Libdocgen(r) => r.doc_path(),
}
}
}
@@ -371,6 +375,87 @@ impl Codegen {
}
}
+#[derive(Debug, Parser)]
+/// Generate test suites from Subplot documents
+///
+/// This reads a subplot document, extracts the scenarios, and writes out a test
+/// program capable of running the scenarios in the subplot document.
+struct Libdocgen {
+ // Bindings file to read.
+ input: PathBuf,
+
+ // Output document filename
+ #[clap(name = "FILE", long = "output", short = 'o')]
+ output: PathBuf,
+
+ /// The template to use from the document.
+ ///
+ /// If not specified, subplot will try and find a unique template name from the document
+ #[clap(name = "TEMPLATE", long = "template", short = 't')]
+ template: Option<String>,
+}
+
+impl Libdocgen {
+ fn doc_path(&self) -> Option<&Path> {
+ None
+ }
+
+ fn run(&self) -> Result<()> {
+ debug!("libdocgen starts");
+
+ let mut bindings = Bindings::new();
+ bindings.add_from_file(&self.input, None)?;
+ // println!("{:#?}", bindings);
+
+ let mut doc = LibDoc::new(&self.input);
+ for b in bindings.bindings() {
+ // println!("{} {}", b.kind(), b.pattern());
+ doc.push_binding(b);
+ }
+
+ std::fs::write(&self.output, doc.to_markdown())?;
+
+ debug!("libdogen ends successfully");
+ Ok(())
+ }
+}
+
+struct LibDoc {
+ filename: PathBuf,
+ bindings: Vec<Binding>,
+}
+
+impl LibDoc {
+ fn new(filename: &Path) -> Self {
+ Self {
+ filename: filename.into(),
+ bindings: vec![],
+ }
+ }
+
+ fn push_binding(&mut self, binding: &Binding) {
+ self.bindings.push(binding.clone());
+ }
+
+ fn to_markdown(&self) -> String {
+ let mut md = String::new();
+ md.push_str(&format!("# Library `{}`\n\n", self.filename.display()));
+ for b in self.bindings.iter() {
+ md.push_str(&format!("\n## {} `{}`\n", b.kind(), b.pattern()));
+ if let Some(doc) = b.doc() {
+ md.push_str(&format!("\n{}\n", doc));
+ }
+ if b.types().count() > 0 {
+ md.push_str("\nCaptures:\n\n");
+ for (name, cap_type) in b.types() {
+ md.push_str(&format!("- `{}`: {}\n", name, cap_type.as_str()));
+ }
+ }
+ }
+ md
+ }
+}
+
fn load_linted_doc(
filename: &Path,
style: Style,
diff --git a/src/bindings.rs b/src/bindings.rs
index b629992..5fe5887 100644
--- a/src/bindings.rs
+++ b/src/bindings.rs
@@ -166,6 +166,7 @@ pub struct Binding {
regex: Regex,
impls: HashMap<String, Arc<BindingImpl>>,
types: HashMap<String, CaptureType>,
+ doc: Option<String>,
}
impl Binding {
@@ -175,6 +176,7 @@ impl Binding {
pattern: &str,
case_sensitive: bool,
mut types: HashMap<String, CaptureType>,
+ doc: Option<String>,
) -> Result<Binding, SubplotError> {
let regex = RegexBuilder::new(&format!("^{pattern}$"))
.case_insensitive(!case_sensitive)
@@ -193,6 +195,7 @@ impl Binding {
regex,
impls: HashMap::new(),
types,
+ doc,
})
}
@@ -214,6 +217,11 @@ impl Binding {
&self.pattern
}
+ /// Return documentation string for binding, if any.
+ pub fn doc(&self) -> Option<&str> {
+ self.doc.as_deref()
+ }
+
/// Retrieve a particular implementation by name
pub fn step_impl(&self, template: &str) -> Option<Arc<BindingImpl>> {
self.impls.get(template).cloned()
@@ -319,7 +327,7 @@ mod test_binding {
#[test]
fn creates_new() {
- let b = Binding::new(StepKind::Given, "I am Tomjon", false, HashMap::new()).unwrap();
+ let b = Binding::new(StepKind::Given, "I am Tomjon", false, HashMap::new(), None).unwrap();
assert_eq!(b.kind(), StepKind::Given);
assert!(b.regex().is_match("I am Tomjon"));
assert!(!b.regex().is_match("I am Tomjon of Lancre"));
@@ -328,19 +336,20 @@ mod test_binding {
#[test]
fn equal() {
- let a = Binding::new(StepKind::Given, "I am Tomjon", false, HashMap::new()).unwrap();
- let b = Binding::new(StepKind::Given, "I am Tomjon", false, HashMap::new()).unwrap();
+ let a = Binding::new(StepKind::Given, "I am Tomjon", false, HashMap::new(), None).unwrap();
+ let b = Binding::new(StepKind::Given, "I am Tomjon", false, HashMap::new(), None).unwrap();
assert_eq!(a, b);
}
#[test]
fn not_equal() {
- let a = Binding::new(StepKind::Given, "I am Tomjon", false, HashMap::new()).unwrap();
+ let a = Binding::new(StepKind::Given, "I am Tomjon", false, HashMap::new(), None).unwrap();
let b = Binding::new(
StepKind::Given,
"I am Tomjon of Lancre",
false,
HashMap::new(),
+ None,
)
.unwrap();
assert_ne!(a, b);
@@ -349,21 +358,21 @@ mod test_binding {
#[test]
fn does_not_match_with_wrong_kind() {
let step = ScenarioStep::new(StepKind::Given, "given", "yo", Location::Unknown);
- let b = Binding::new(StepKind::When, "yo", false, HashMap::new()).unwrap();
+ let b = Binding::new(StepKind::When, "yo", false, HashMap::new(), None).unwrap();
assert!(b.match_with_step("", &step).is_none());
}
#[test]
fn does_not_match_with_wrong_text() {
let step = ScenarioStep::new(StepKind::Given, "given", "foo", Location::Unknown);
- let b = Binding::new(StepKind::Given, "bar", false, HashMap::new()).unwrap();
+ let b = Binding::new(StepKind::Given, "bar", false, HashMap::new(), None).unwrap();
assert!(b.match_with_step("", &step).is_none());
}
#[test]
fn match_with_fixed_pattern() {
let step = ScenarioStep::new(StepKind::Given, "given", "foo", Location::Unknown);
- let b = Binding::new(StepKind::Given, "foo", false, HashMap::new()).unwrap();
+ let b = Binding::new(StepKind::Given, "foo", false, HashMap::new(), None).unwrap();
let m = b.match_with_step("", &step).unwrap();
assert_eq!(m.kind(), StepKind::Given);
let mut parts = m.parts();
@@ -385,6 +394,7 @@ mod test_binding {
r"I am (?P<who>\S+), I am",
false,
HashMap::new(),
+ None,
)
.unwrap();
let m = b.match_with_step("", &step).unwrap();
@@ -399,9 +409,9 @@ mod test_binding {
#[test]
fn case_sensitive_mismatch() {
let step = ScenarioStep::new(StepKind::Given, "given", "I am Tomjon", Location::Unknown);
- let b = Binding::new(StepKind::Given, r"i am tomjon", false, HashMap::new()).unwrap();
+ let b = Binding::new(StepKind::Given, r"i am tomjon", false, HashMap::new(), None).unwrap();
assert!(b.match_with_step("", &step).is_some());
- let b = Binding::new(StepKind::Given, r"i am tomjon", true, HashMap::new()).unwrap();
+ let b = Binding::new(StepKind::Given, r"i am tomjon", true, HashMap::new(), None).unwrap();
assert!(b.match_with_step("", &step).is_none());
}
}
@@ -445,6 +455,7 @@ struct ParsedBinding {
case_sensitive: bool,
#[serde(default)]
types: HashMap<String, CaptureType>,
+ doc: Option<String>,
}
#[derive(Debug, Deserialize)]
@@ -578,7 +589,13 @@ fn from_hashmap(parsed: &ParsedBinding) -> Result<Binding, SubplotError> {
trace!("Successfully acquired binding");
- let mut ret = Binding::new(kind, &pattern, parsed.case_sensitive, types)?;
+ let mut ret = Binding::new(
+ kind,
+ &pattern,
+ parsed.case_sensitive,
+ types,
+ parsed.doc.clone(),
+ )?;
trace!("Binding parsed OK");
for (template, pimpl) in &parsed.impls {
ret.add_impl(template, &pimpl.function, pimpl.cleanup.as_deref());
@@ -612,6 +629,7 @@ mod test_bindings {
r"I am (?P<name>\S+)",
false,
HashMap::new(),
+ None,
)
.unwrap();
let mut bindings = Bindings::new();
@@ -694,7 +712,8 @@ mod test_bindings {
#[test]
fn does_not_find_match_for_unmatching_kind() {
let step = ScenarioStep::new(StepKind::Given, "given", "I am Tomjon", Location::Unknown);
- let binding = Binding::new(StepKind::When, r"I am Tomjon", false, HashMap::new()).unwrap();
+ let binding =
+ Binding::new(StepKind::When, r"I am Tomjon", false, HashMap::new(), None).unwrap();
let mut bindings = Bindings::new();
bindings.add(binding);
assert!(matches!(
@@ -711,6 +730,7 @@ mod test_bindings {
r"I am Tomjon of Lancre",
false,
HashMap::new(),
+ None,
)
.unwrap();
let mut bindings = Bindings::new();
@@ -725,7 +745,9 @@ mod test_bindings {
fn two_matching_bindings() {
let step = ScenarioStep::new(StepKind::Given, "given", "I am Tomjon", Location::Unknown);
let mut bindings = Bindings::default();
- bindings.add(Binding::new(StepKind::Given, r"I am Tomjon", false, HashMap::new()).unwrap());
+ bindings.add(
+ Binding::new(StepKind::Given, r"I am Tomjon", false, HashMap::new(), None).unwrap(),
+ );
bindings.add(
Binding::new(
StepKind::Given,
@@ -733,6 +755,7 @@ mod test_bindings {
.unwrap(),
false,
HashMap::new(),
+ None,
)
.unwrap(),
);
@@ -745,7 +768,8 @@ mod test_bindings {
#[test]
fn finds_match_for_fixed_string_pattern() {
let step = ScenarioStep::new(StepKind::Given, "given", "I am Tomjon", Location::Unknown);
- let binding = Binding::new(StepKind::Given, r"I am Tomjon", false, HashMap::new()).unwrap();
+ let binding =
+ Binding::new(StepKind::Given, r"I am Tomjon", false, HashMap::new(), None).unwrap();
let mut bindings = Bindings::new();
bindings.add(binding);
let m = bindings.find("", &step).unwrap();
@@ -767,6 +791,7 @@ mod test_bindings {
r"I am (?P<name>\S+)",
false,
HashMap::new(),
+ None,
)
.unwrap();
let mut bindings = Bindings::new();
diff --git a/tests/bindings-ubm.rs b/tests/bindings-ubm.rs
index 9a1dbc5..4c4aaa0 100644
--- a/tests/bindings-ubm.rs
+++ b/tests/bindings-ubm.rs
@@ -34,7 +34,7 @@ fn bindings_microbenchmark() {
let mut toadd = vec![];
for t in texts.iter() {
- toadd.push(Binding::new(StepKind::Given, t, false, HashMap::new()).unwrap());
+ toadd.push(Binding::new(StepKind::Given, t, false, HashMap::new(), None).unwrap());
}
let created = time.elapsed().unwrap();