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;
use git_testament::*;
git_testament!(VERSION);
#[derive(Debug, StructOpt)]
struct Toplevel {
#[structopt(flatten)]
resources: resource::ResourceOpts,
#[structopt(flatten)]
command: Cmd,
}
impl Toplevel {
fn run(&self) -> Result<()> {
self.command.run()
}
fn handle_resources(&self) {
let doc_path = self.command.doc_path();
self.resources.handle(doc_path);
}
}
#[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(),
}
}
fn doc_path(&self) -> Option<&Path> {
match self {
Cmd::Extract(e) => e.doc_path(),
Cmd::Filter(f) => f.doc_path(),
Cmd::Metadata(m) => m.doc_path(),
Cmd::Docgen(d) => d.doc_path(),
Cmd::Codegen(c) => c.doc_path(),
}
}
}
fn long_version() -> Result<String> {
use std::fmt::Write as _;
let mut ret = String::new();
writeln!(ret, "{}", render_testament!(VERSION))?;
writeln!(ret, "Crate version: {}", env!("CARGO_PKG_VERSION"))?;
if let Some(branch) = VERSION.branch_name {
writeln!(ret, "Built from branch: {}", branch)?;
} else {
writeln!(ret, "Branch information is missing.")?;
}
writeln!(ret, "Commit info: {}", VERSION.commit)?;
if VERSION.modifications.is_empty() {
writeln!(ret, "Working tree is clean")?;
} else {
use GitModification::*;
for fmod in VERSION.modifications {
match fmod {
Added(f) => writeln!(ret, "Added: {}", String::from_utf8_lossy(f))?,
Removed(f) => writeln!(ret, "Removed: {}", String::from_utf8_lossy(f))?,
Modified(f) => writeln!(ret, "Modified: {}", String::from_utf8_lossy(f))?,
Untracked(f) => writeln!(ret, "Untracked: {}", String::from_utf8_lossy(f))?,
}
}
}
Ok(ret)
}
#[derive(Debug, StructOpt)]
struct Extract {
#[structopt(
name = "DIR",
long = "directory",
short = "d",
parse(from_os_str),
default_value = "."
)]
directory: PathBuf,
#[structopt(long)]
dry_run: bool,
#[structopt(parse(from_os_str))]
filename: PathBuf,
embedded: Vec<String>,
}
impl Extract {
fn doc_path(&self) -> Option<&Path> {
self.filename.parent()
}
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)]
struct Filter {
#[structopt(name = "INPUT", long = "input", short = "i", parse(from_os_str))]
input: Option<PathBuf>,
#[structopt(name = "OUTPUT", long = "output", short = "o", parse(from_os_str))]
output: Option<PathBuf>,
#[structopt(name = "BASE", long = "base", short = "b", parse(from_os_str))]
base: Option<PathBuf>,
}
impl Filter {
fn doc_path(&self) -> Option<&Path> {
self.input.as_deref().and_then(Path::parent)
}
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)]
struct Metadata {
#[structopt(short = "o", default_value = "plain", possible_values=&["plain", "json"])]
output_format: cli::OutputFormat,
#[structopt(parse(from_os_str))]
filename: PathBuf,
}
impl Metadata {
fn doc_path(&self) -> Option<&Path> {
self.filename.parent()
}
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)]
struct Docgen {
#[structopt(parse(from_os_str))]
input: PathBuf,
#[structopt(name = "FILE", long = "--output", short = "-o", parse(from_os_str))]
output: PathBuf,
#[structopt(name = "DATE", long = "--date")]
date: Option<String>,
}
impl Docgen {
fn doc_path(&self) -> Option<&Path> {
self.input.parent()
}
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();
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)]
struct Codegen {
#[structopt(parse(from_os_str))]
filename: PathBuf,
#[structopt(
long,
short,
parse(from_os_str),
help = "Writes generated test program to FILE"
)]
output: PathBuf,
#[structopt(long, short, help = "Runs generated test program")]
run: bool,
}
impl Codegen {
fn doc_path(&self) -> Option<&Path> {
self.filename.parent()
}
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 argparser = Toplevel::clap();
let version = long_version().unwrap();
let argparser = argparser.long_version(version.as_str());
let args = argparser.get_matches();
let args = Toplevel::from_clap(&args);
args.handle_resources();
match args.run() {
Ok(_) => {}
Err(e) => {
eprintln!("Failure: {:?}", e);
process::exit(1);
}
}
}