use super::dispatch;
use crate::{
args::{Batch, ReportStyle},
outcome::{CliError, CommandResult, EXIT_OK, EXIT_PROBLEM, Outcome},
};
use std::fmt::Write as _;
fn with_line_feed(text: &str) -> String {
if text.is_empty() || text.ends_with('\n') {
text.to_string()
} else {
format!("{text}\n")
}
}
fn add_stdout(all: &mut String, label: &str, text: &str, style: ReportStyle, shown: usize) {
if text.is_empty() {
return;
}
let text = with_line_feed(text);
match style {
ReportStyle::Text => {
if shown > 0 {
all.push('\n');
}
let _ = writeln!(all, "==> {label} <==");
all.push_str(&text);
}
ReportStyle::Json => all.push_str(&text),
ReportStyle::Csv => {
if shown == 0 {
all.push_str(&text);
} else if let Some((_, rows)) = text.split_once('\n') {
all.push_str(rows);
}
}
}
}
fn add_stderr(all: &mut String, label: &str, text: &str) {
if text.is_empty() {
return;
}
let _ = writeln!(all, "==> {label} <==");
all.push_str(&with_line_feed(text));
}
pub fn run(batch: &Batch) -> CommandResult {
if let Some(dir) = &batch.out_dir
&& !dir.is_dir()
{
return Err(CliError::io(format!(
"--out-dir {} is not a folder that exists",
dir.display()
)));
}
let total = batch.items.len();
let mut all = Outcome::default();
let mut failed = 0_usize;
let mut first_failure = None;
let mut problem = false;
let mut shown = 0_usize;
for item in &batch.items {
let label = item
.design()
.map_or_else(String::new, |path| path.display().to_string());
let result = dispatch(item).unwrap_or_else(|error| Outcome::failure(&error));
match result.exit {
EXIT_OK => {}
EXIT_PROBLEM => problem = true,
code => {
failed += 1;
first_failure.get_or_insert(code);
}
}
if !result.stdout.is_empty() {
add_stdout(&mut all.stdout, &label, &result.stdout, batch.style, shown);
shown += 1;
}
add_stderr(&mut all.stderr, &label, &result.stderr);
all.files.extend(result.files);
}
if failed > 0 {
let _ = writeln!(all.stderr, "error: {failed} of {total} design files failed");
}
all.exit = first_failure.unwrap_or(if problem { EXIT_PROBLEM } else { EXIT_OK });
Ok(all)
}
#[cfg(test)]
mod tests {
use crate::{
outcome::{EXIT_IO, EXIT_OK, EXIT_PROBLEM},
testing,
};
fn two_designs(tag: &str) -> (String, String) {
(
testing::write_design(&format!("batch-{tag}-a.isdd"), &testing::template()),
testing::write_design(&format!("batch-{tag}-b.isdd"), &testing::brilliant_at_172()),
)
}
fn missing_file(name: &str) -> String {
testing::temp_path(name).to_string_lossy().into_owned()
}
#[test]
fn the_text_reports_of_several_designs_come_under_their_file_names() {
let (a, b) = two_designs("text");
let outcome = testing::run(&["info", &a, &b]);
assert_eq!(outcome.exit, EXIT_OK, "{}", outcome.stderr);
let first = testing::run(&["info", &a]);
let second = testing::run(&["info", &b]);
assert_eq!(
outcome.stdout,
format!(
"==> {a} <==\n{}\n==> {b} <==\n{}",
first.stdout, second.stdout
)
);
}
#[test]
fn the_json_reports_of_several_designs_are_a_stream_of_documents() {
let (a, b) = two_designs("json");
let outcome = testing::run(&["info", &a, &b, "--json"]);
assert_eq!(outcome.exit, EXIT_OK, "{}", outcome.stderr);
let first = testing::run(&["info", &a, "--json"]);
let second = testing::run(&["info", &b, "--json"]);
assert_eq!(outcome.stdout, format!("{}{}", first.stdout, second.stdout));
let documents = serde_json::Deserializer::from_str(&outcome.stdout)
.into_iter::<serde_json::Value>()
.count();
assert_eq!(documents, 2);
}
#[test]
fn metrics_csv_keeps_one_header_line_and_a_row_for_each_design() {
let (a, b) = two_designs("csv");
let line = |text: &str, n: usize| text.lines().nth(n).map(str::to_string);
let outcome = testing::run(&["metrics", &a, &b, "--ri", "1.76", "--csv"]);
assert_eq!(outcome.exit, EXIT_OK, "{}", outcome.stderr);
let first = testing::run(&["metrics", &a, "--ri", "1.76", "--csv"]);
let second = testing::run(&["metrics", &b, "--ri", "1.76", "--csv"]);
assert_eq!(outcome.stdout.lines().count(), 3, "{}", outcome.stdout);
assert_eq!(line(&outcome.stdout, 0), line(&first.stdout, 0));
assert_eq!(line(&outcome.stdout, 1), line(&first.stdout, 1));
assert_eq!(line(&outcome.stdout, 2), line(&second.stdout, 1));
}
#[test]
fn a_failing_file_is_reported_and_the_others_still_run_and_write() {
let (a, b) = two_designs("fail");
let missing = missing_file("batch-fail-no-such-file.isdd");
let folder = testing::temp_path("batch-fail-out");
let _ = std::fs::create_dir_all(&folder);
let dir = folder.to_string_lossy().into_owned();
let outcome = testing::run(&["solve", &a, &missing, &b, "--out-dir", &dir]);
assert_eq!(outcome.exit, EXIT_IO, "{}", outcome.stderr);
assert_eq!(outcome.files.len(), 2, "the two good designs are saved");
assert!(outcome.files[0].path.ends_with("batch-fail-a.isdd"));
assert!(outcome.files[1].path.ends_with("batch-fail-b.isdd"));
assert!(outcome.files[0].path.starts_with(&folder));
assert!(
outcome
.stderr
.contains(&format!("==> {missing} <==\nerror: ")),
"{}",
outcome.stderr
);
assert!(
outcome
.stderr
.ends_with("error: 1 of 3 design files failed\n"),
"{}",
outcome.stderr
);
}
#[test]
fn the_exit_code_is_the_first_failure_else_zero() {
let (a, _) = two_designs("exit");
let missing = missing_file("batch-exit-gone.isdd");
let broken = testing::temp_path("batch-exit-broken.isdd");
std::fs::write(&broken, "this is not a design").expect("the folder is writable");
let broken = broken.to_string_lossy().into_owned();
let outcome = testing::run(&["info", &missing, &broken, &a]);
assert_eq!(outcome.exit, EXIT_IO, "{}", outcome.stderr);
let outcome = testing::run(&["info", &broken, &missing, &a]);
assert_ne!(outcome.exit, EXIT_OK, "{}", outcome.stderr);
assert_ne!(outcome.exit, EXIT_PROBLEM, "{}", outcome.stderr);
let outcome = testing::run(&["info", &a, &a]);
assert_eq!(outcome.exit, EXIT_OK, "{}", outcome.stderr);
}
#[test]
fn a_folder_that_does_not_exist_is_an_io_error_before_anything_runs() {
let (a, b) = two_designs("folder");
let no_folder = missing_file("batch-folder-no-such-folder");
let outcome = testing::run(&["solve", &a, &b, "--out-dir", &no_folder]);
assert_eq!(outcome.exit, EXIT_IO, "{}", outcome.stderr);
assert!(
outcome.stderr.starts_with("error: --out-dir "),
"{}",
outcome.stderr
);
assert_eq!(outcome.stdout, "");
assert_eq!(outcome.files, [] as [crate::outcome::OutFile; 0]);
}
}