use crate::utils::get_or_create_and_get_scarfbench_home_dir;
use anyhow::{Context, Result};
use chrono::{DateTime, Utc};
use clap::{ArgAction, Args};
use rayon::prelude::*;
use serde::{Deserialize, Serialize};
use std::process::{Command, Stdio};
use std::time::Duration;
use std::{fs::OpenOptions, io::Write, path::PathBuf, sync::mpsc};
use wait_timeout::ChildExt;
use walkdir::WalkDir;
#[derive(Args, Debug)]
pub struct BenchTestArgs {
#[arg(
long,
help = "Path to the root of the scarf benchmark.",
value_name = "DIRECTORY"
)]
pub benchmark_dir: PathBuf,
#[arg(long, help = "Application layer to test.", action=ArgAction::Append, value_name="LAYER")]
pub layer: Vec<String>,
#[arg(long, help = "Application to run the test on.", action = ArgAction::Append, value_name="APPLICATION")]
pub app: Vec<String>,
#[arg(
long = "dry-run",
action = ArgAction::SetTrue,
help = "Use dry run instead of full run."
)]
pub dry_run: bool,
#[arg(
long = "logs-dest",
action = ArgAction::SetTrue,
help = "Where to save the logs."
)]
pub maybe_logs: Option<PathBuf>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct RunResult {
pub timestamp: DateTime<Utc>,
pub path: PathBuf,
pub ok: bool,
pub stdout: String,
pub stderr: String,
}
fn run_makefile(path: &PathBuf, dry_run: bool) -> Result<RunResult> {
if !path.join("Makefile").exists() {
return Err(anyhow::anyhow!(
"No Makefile found in the provided directory: {}",
path.display()
));
}
let mut cmd = Command::new("make");
if dry_run {
cmd.arg("-n");
}
let mut child = cmd
.arg("test")
.current_dir(path)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
let dur = Duration::from_secs(600);
match child.wait_timeout(dur)? {
Some(_) => {
let output = child.wait_with_output()?;
Ok(RunResult {
timestamp: Utc::now(),
path: path.clone(),
ok: output.status.success(),
stdout: String::from_utf8_lossy(&output.stdout).into_owned(),
stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
})
},
None => {
child.kill()?;
let output = child.wait_with_output()?;
Ok(RunResult {
timestamp: Utc::now(),
path: path.clone(),
ok: false,
stdout: String::from_utf8_lossy(&output.stdout).into_owned(),
stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
})
},
}
}
pub fn run(args: BenchTestArgs) -> Result<i32> {
log::info!(
"Running tests to ensure functionality of benchmark applications..."
);
if !args.benchmark_dir.exists() {
anyhow::bail!(
"The benchmark dir {} doesnt seem to exist.",
&args.benchmark_dir.display(),
)
}
let bench_root = std::fs::canonicalize(&args.benchmark_dir)
.context(format!(
"Failed to canonicalize the benchmark root path: {}",
args.benchmark_dir.display()
))
.unwrap();
if bench_root.exists() {
log::debug!("Benchmark directory: {}", bench_root.display());
} else {
anyhow::bail!(
"The benchmark directory {} does not exist?",
bench_root.display()
);
}
let app_dirs: Vec<_> = (if !args.layer.is_empty() {
args.layer.clone()
} else {
WalkDir::new(&args.benchmark_dir)
.min_depth(1)
.max_depth(1)
.into_iter()
.filter_map(|e| e.ok())
.filter(|e| e.file_type().is_dir())
.map(|e| e.file_name().to_string_lossy().into_owned())
.collect()
})
.iter()
.flat_map(|layer| {
WalkDir::new(args.benchmark_dir.join(layer))
.min_depth(2)
.max_depth(2)
.into_iter()
.filter_map(|e| e.ok())
.filter(|e| e.file_type().is_dir())
.filter(|e| {
if args.app.is_empty() {
true
} else {
e.path()
.parent()
.and_then(|f| f.file_name())
.map(|os| os.to_string_lossy().into_owned())
.map(|n| args.app.iter().any(|a| a.to_string() == n))
.unwrap_or(false)
}
})
.map(|e| e.path().to_path_buf())
})
.collect();
if app_dirs.is_empty() {
anyhow::bail!(
"The app(s) provided with the --app flag were not found for the specified --layer(s)."
);
}
let (tx, rx) = mpsc::channel::<(PathBuf, anyhow::Result<RunResult>)>();
app_dirs.par_iter().for_each_with(tx, |tx, dir| {
let result = run_makefile(dir, args.dry_run);
log::info!("Completed makefile test in directory: {}.", dir.display(),);
let _ = tx.send((dir.to_path_buf(), result));
});
let log_dir = get_or_create_and_get_scarfbench_home_dir()?.join("logs");
let log_file = log_dir
.join(format!("benchtest.logs.{}.jsonl", Utc::now().to_rfc3339()));
let mut results: Vec<[String; 2]> = Vec::new();
for (dir, res) in rx.iter() {
let mut file =
OpenOptions::new().create(true).append(true).open(&log_file)?;
match res {
Ok(res) => {
let status = if res.ok { "Success" } else { "Failure" };
results.push([
dir.to_string_lossy().into_owned(),
status.to_string(),
]);
if res.ok {
log::info!(
"Makefile test in {} succeeded",
res.path.display()
);
} else {
log::warn!(
"Makefile test in {} failed",
res.path.display()
);
}
let jsonl = serde_json::to_string(&res)?;
writeln!(file, "{jsonl}")?;
},
Err(e) => {
results.push([
dir.to_string_lossy().into_owned(),
"Error".to_string(),
]);
log::error!(
"Makefile test in {} encountered an error: {}",
dir.display(),
e
);
},
}
file.flush()?;
}
let header: [String; 2] =
["Application Path".to_string(), "Result".to_string()];
let mut table: comfy_table::Table = comfy_table::Table::new();
table.load_preset(comfy_table::presets::UTF8_FULL_CONDENSED);
table.set_header(header);
for rows in results {
table.add_row(rows.to_vec());
}
println!("{}", table);
Ok(0)
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
fn _touch_makefile(dir: &PathBuf) -> Result<()> {
fs::create_dir_all(dir)?;
let makefile_path = dir.join("Makefile");
fs::write(makefile_path, "test:\n\techo Hello World\n")?;
Ok(())
}
#[test]
pub fn test_make_dry_run() {
let tempfile = tempfile::tempdir().unwrap();
let app_dir = tempfile.path().join("layer/app/framwork");
_touch_makefile(&app_dir)
.expect("Failed to create Makefile in app directory");
let result =
run_makefile(&app_dir, false).expect("Failed to run the makefile");
assert_eq!(result.path, app_dir);
assert!(result.ok);
assert!(result.stdout.contains("echo Hello World"));
}
}