diff options
author | Lars Wirzenius <liw@liw.fi> | 2023-11-04 08:30:45 +0200 |
---|---|---|
committer | Lars Wirzenius <liw@liw.fi> | 2023-11-04 10:10:45 +0200 |
commit | 6a8a210de12bb9e79d9d610a7ca091567dc9550a (patch) | |
tree | 470d7faaf9a02afb39439c27193edf0c0a45b6d4 | |
parent | 7252cf5c1e0288b1bbb54908999fc7524d12bf1e (diff) | |
download | subplot-6a8a210de12bb9e79d9d610a7ca091567dc9550a.tar.gz |
feat: generate library documentation from bindings files
Add a `doc` field to a binding. It's a string meant to contain
markdown.
Add subcommand `subplot libdocgen YAML -o MD` to read a bindings file
and output a markdown file documenting the steps in the bindings file,
and the type of captures, and the documentation for the step.
Signed-off-by: Lars Wirzenius <liw@liw.fi>
Sponsored-by: author
-rw-r--r-- | share/common/lib/runcmd.yaml | 65 | ||||
-rw-r--r-- | src/bin/subplot.rs | 89 | ||||
-rw-r--r-- | src/bindings.rs | 51 | ||||
-rw-r--r-- | tests/bindings-ubm.rs | 2 |
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(); |