summaryrefslogtreecommitdiff
path: root/src/bin/subplot.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/bin/subplot.rs')
-rw-r--r--src/bin/subplot.rs349
1 files changed, 349 insertions, 0 deletions
diff --git a/src/bin/subplot.rs b/src/bin/subplot.rs
new file mode 100644
index 0000000..ebbdec8
--- /dev/null
+++ b/src/bin/subplot.rs
@@ -0,0 +1,349 @@
+//! Subplot top level binary
+//!
+
+use anyhow::Result;
+
+use chrono::{Local, TimeZone};
+use structopt::StructOpt;
+use subplot::{generate_test_program, resource, template_spec, DataFile, Document, Style};
+
+use std::convert::TryFrom;
+use std::ffi::OsString;
+use std::fs::{self, write, File};
+use std::io::{Read, Write};
+use std::path::{Path, PathBuf};
+use std::process::{self, Command};
+use std::time::UNIX_EPOCH;
+
+mod cli;
+
+#[derive(Debug, StructOpt)]
+struct Toplevel {
+ #[structopt(flatten)]
+ resources: resource::ResourceOpts,
+
+ #[structopt(flatten)]
+ command: Cmd,
+}
+
+impl Toplevel {
+ fn run(&self) -> Result<()> {
+ self.command.run()
+ }
+}
+
+#[derive(Debug, StructOpt)]
+enum Cmd {
+ Extract(Extract),
+ Filter(Filter),
+ Metadata(Metadata),
+ Docgen(Docgen),
+ Codegen(Codegen),
+}
+
+impl Cmd {
+ fn run(&self) -> Result<()> {
+ match self {
+ Cmd::Extract(e) => e.run(),
+ Cmd::Filter(f) => f.run(),
+ Cmd::Metadata(m) => m.run(),
+ Cmd::Docgen(d) => d.run(),
+ Cmd::Codegen(c) => c.run(),
+ }
+ }
+}
+
+#[derive(Debug, StructOpt)]
+/// Extract embedded files from a subplot document
+///
+/// If no embedded filenames are provided, this will
+/// extract all embedded files. if the output directory
+/// is not specified then this will extract to the current directory.
+struct Extract {
+ /// Directory to write extracted files to
+ #[structopt(
+ name = "DIR",
+ long = "directory",
+ short = "d",
+ parse(from_os_str),
+ default_value = "."
+ )]
+ directory: PathBuf,
+
+ /// Don't actually write the files out
+ #[structopt(long)]
+ dry_run: bool,
+
+ /// Input subplot document filename
+ #[structopt(parse(from_os_str))]
+ filename: PathBuf,
+
+ /// Names of embedded files to be extracted.
+ embedded: Vec<String>,
+}
+
+impl Extract {
+ fn run(&self) -> Result<()> {
+ let doc = cli::load_document(&self.filename, Style::default())?;
+
+ let files: Vec<&DataFile> = if self.embedded.is_empty() {
+ doc.files()
+ .iter()
+ .map(Result::Ok)
+ .collect::<Result<Vec<_>>>()
+ } else {
+ self.embedded
+ .iter()
+ .map(|filename| cli::extract_file(&doc, filename))
+ .collect::<Result<Vec<_>>>()
+ }?;
+
+ for f in files {
+ let outfile = self.directory.join(f.filename());
+ if self.dry_run {
+ println!("Would write out {}", outfile.display());
+ } else {
+ write(outfile, f.contents())?
+ }
+ }
+
+ Ok(())
+ }
+}
+
+#[derive(Debug, StructOpt)]
+/// Filter a pandoc JSON document.
+///
+/// This filters a pandoc JSON document, applying Subplot's formatting rules and
+/// image conversion support.
+///
+/// If input/output filename is provided, this operates on STDIN/STDOUT.
+struct Filter {
+ #[structopt(name = "INPUT", long = "input", short = "i", parse(from_os_str))]
+ /// Input file (uses STDIN if omitted)
+ input: Option<PathBuf>,
+
+ #[structopt(name = "OUTPUT", long = "output", short = "o", parse(from_os_str))]
+ /// Output file (uses STDOUT if omitted)
+ output: Option<PathBuf>,
+
+ #[structopt(name = "BASE", long = "base", short = "b", parse(from_os_str))]
+ /// Base directory (defaults to dir of input if given, or '.' if using STDIN)
+ base: Option<PathBuf>,
+}
+
+impl Filter {
+ fn run(&self) -> Result<()> {
+ let mut buffer = String::new();
+ if let Some(filename) = &self.input {
+ File::open(filename)?.read_to_string(&mut buffer)?;
+ } else {
+ std::io::stdin().read_to_string(&mut buffer)?;
+ }
+ let basedir = if let Some(path) = &self.base {
+ path.as_path()
+ } else if let Some(path) = &self.input {
+ path.parent().unwrap_or_else(|| Path::new("."))
+ } else {
+ Path::new(".")
+ };
+ let style = Style::default();
+ let mut doc = Document::from_json(basedir, vec![], &buffer, style)?;
+ doc.typeset();
+ let bytes = doc.ast()?.into_bytes();
+ if let Some(filename) = &self.output {
+ File::create(filename)?.write_all(&bytes)?;
+ } else {
+ std::io::stdout().write_all(&bytes)?;
+ }
+ Ok(())
+ }
+}
+
+#[derive(Debug, StructOpt)]
+/// Extract metadata about a document
+///
+/// Load and process a subplot document, extracting various metadata about the
+/// document. This can then render that either as a plain text report for humans,
+/// or as a JSON object for further scripted processing.
+struct Metadata {
+ /// Form that you want the output to take
+ #[structopt(short = "o", default_value = "plain", possible_values=&["plain", "json"])]
+ output_format: cli::OutputFormat,
+
+ /// Input subplot document filename
+ #[structopt(parse(from_os_str))]
+ filename: PathBuf,
+}
+
+impl Metadata {
+ fn run(&self) -> Result<()> {
+ let mut doc = cli::load_document(&self.filename, Style::default())?;
+ let meta = cli::Metadata::try_from(&mut doc)?;
+ match self.output_format {
+ cli::OutputFormat::Plain => meta.write_out(),
+ cli::OutputFormat::Json => println!("{}", serde_json::to_string_pretty(&meta)?),
+ }
+ Ok(())
+ }
+}
+
+#[derive(Debug, StructOpt)]
+/// Typeset subplot document
+///
+/// Process a subplot document and typeset it using Pandoc.
+struct Docgen {
+ // Input Subplot document
+ #[structopt(parse(from_os_str))]
+ input: PathBuf,
+
+ // Output document filename
+ #[structopt(name = "FILE", long = "--output", short = "-o", parse(from_os_str))]
+ output: PathBuf,
+
+ // Set date.
+ #[structopt(name = "DATE", long = "--date")]
+ date: Option<String>,
+}
+
+impl Docgen {
+ fn run(&self) -> Result<()> {
+ let mut style = Style::default();
+ if self.output.extension() == Some(&OsString::from("pdf")) {
+ style.typeset_links_as_notes();
+ }
+ let mut doc = cli::load_document(&self.input, style)?;
+ doc.lint()?;
+ if !doc.check_named_files_exist()? {
+ eprintln!("Continuing despite warnings");
+ }
+
+ let mut pandoc = pandoc::new();
+ // Metadata date from command line or file mtime. However, we
+ // can't set it directly, since we don't want to override the date
+ // in the actual document, if given, so we only set
+ // user-provided-date. Our parsing code will use that if date is
+ // not document metadata.
+ let date = if let Some(date) = self.date.clone() {
+ date
+ } else if let Some(date) = doc.meta().date() {
+ date.to_string()
+ } else {
+ Self::mtime_formatted(Self::mtime(&self.input)?)
+ };
+ pandoc.add_option(pandoc::PandocOption::Meta("date".to_string(), Some(date)));
+ pandoc.add_option(pandoc::PandocOption::TableOfContents);
+ pandoc.add_option(pandoc::PandocOption::Standalone);
+ pandoc.add_option(pandoc::PandocOption::NumberSections);
+
+ if Self::need_output(&mut doc, &self.output) {
+ doc.typeset();
+ pandoc.set_input_format(pandoc::InputFormat::Json, vec![]);
+ pandoc.set_input(pandoc::InputKind::Pipe(doc.ast()?));
+ pandoc.set_output(pandoc::OutputKind::File(self.output.clone()));
+ pandoc.execute()?;
+ }
+
+ Ok(())
+ }
+
+ fn mtime(filename: &Path) -> Result<(u64, u32)> {
+ let mtime = fs::metadata(filename)?.modified()?;
+ let mtime = mtime.duration_since(UNIX_EPOCH)?;
+ Ok((mtime.as_secs(), mtime.subsec_nanos()))
+ }
+
+ fn mtime_formatted(mtime: (u64, u32)) -> String {
+ let secs: i64 = format!("{}", mtime.0).parse().unwrap_or(0);
+ let dt = Local.timestamp(secs, mtime.1);
+ dt.format("%Y-%m-%d %H:%M").to_string()
+ }
+
+ fn need_output(doc: &mut subplot::Document, output: &Path) -> bool {
+ let output = match Self::mtime(output) {
+ Err(_) => return true,
+ Ok(ts) => ts,
+ };
+
+ for filename in doc.sources() {
+ let source = match Self::mtime(&filename) {
+ Err(_) => return true,
+ Ok(ts) => ts,
+ };
+ if source >= output {
+ return true;
+ }
+ }
+ false
+ }
+}
+
+#[derive(Debug, StructOpt)]
+/// 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 Codegen {
+ /// Input filename.
+ #[structopt(parse(from_os_str))]
+ filename: PathBuf,
+
+ /// Write generated test program to this file.
+ #[structopt(
+ long,
+ short,
+ parse(from_os_str),
+ help = "Writes generated test program to FILE"
+ )]
+ output: PathBuf,
+
+ /// Run the generated test program after writing it?
+ #[structopt(long, short, help = "Runs generated test program")]
+ run: bool,
+}
+
+impl Codegen {
+ fn run(&self) -> Result<()> {
+ let mut doc = cli::load_document(&self.filename, Style::default())?;
+ doc.lint()?;
+ if !doc.check_named_files_exist()? {
+ eprintln!("Unable to continue");
+ std::process::exit(1);
+ }
+
+ let spec = template_spec(&doc)?;
+ generate_test_program(&mut doc, &spec, &self.output)?;
+
+ if self.run {
+ let run = match spec.run() {
+ None => {
+ eprintln!(
+ "Template {} does not specify how to run suites",
+ spec.template_filename().display()
+ );
+ std::process::exit(1);
+ }
+ Some(x) => x,
+ };
+
+ let status = Command::new(run).arg(&self.output).status()?;
+ if !status.success() {
+ eprintln!("Test suite failed!");
+ std::process::exit(2);
+ }
+ }
+ Ok(())
+ }
+}
+
+fn main() {
+ let args = Toplevel::from_args();
+ args.resources.handle();
+ match args.run() {
+ Ok(_) => {}
+ Err(e) => {
+ eprintln!("Failure: {:?}", e);
+ process::exit(1);
+ }
+ }
+}