use std::path::{Path, PathBuf};
use std::process::ExitCode;
use std::sync::Arc;
use clap::{Args, Subcommand};
use watoots::TraceHook;
use watoots_trace::{Header, Recorder, Trace, binary, text};
use crate::run::{Invocation, build_host, call_and_print};
use crate::{Failure, Outcome, plugin_name, read, read_manifest, verdict, write};
#[derive(Args)]
pub(crate) struct RecordArgs {
#[command(flatten)]
invocation: Invocation,
#[arg(short, long)]
output: PathBuf,
}
#[derive(Args)]
pub(crate) struct ReplayArgs {
trace: PathBuf,
#[arg(short, long)]
component: PathBuf,
#[arg(long)]
assert: bool,
#[arg(long, value_name = "FILE")]
emit_test: Option<PathBuf>,
}
#[derive(Subcommand)]
pub(crate) enum TraceCommand {
Fmt {
input: PathBuf,
#[arg(short, long)]
output: Option<PathBuf>,
#[arg(long)]
binary: bool,
},
}
pub(crate) fn record(args: &RecordArgs) -> Outcome {
let invocation = &args.invocation;
let wasm = read(&invocation.component)?;
let (manifest, manifest_toml) = read_manifest(invocation.manifest.as_deref())?;
let name = plugin_name(&invocation.component);
let recorder = Arc::new(Recorder::new(Header {
component_sha256: Trace::hash_component(&wasm),
plugin: name.clone(),
manifest_toml,
}));
let outcome = (|| -> Outcome {
let host = build_host(
invocation,
manifest,
Some(Arc::clone(&recorder) as Arc<dyn TraceHook>),
None,
)?;
let mut plugin = match read(&signature_beside(&invocation.component)) {
Ok(signature) => host.load_binary_signed(&name, &wasm, &signature)?,
Err(_) => host.load_binary(&name, &wasm)?,
};
call_and_print(&mut plugin, invocation)?;
Ok(ExitCode::SUCCESS)
})();
let trace = recorder.finish()?;
if trace.events.is_empty() {
return outcome;
}
write_trace(&trace, &args.output)?;
eprintln!(
"wrote {} ({} crossings)",
args.output.display(),
trace.events.len()
);
match outcome {
Ok(code) => Ok(code),
Err(failure) => {
eprintln!("watoots: {failure}");
Ok(ExitCode::FAILURE)
}
}
}
fn signature_beside(component: &Path) -> PathBuf {
let mut path = component.as_os_str().to_os_string();
path.push(".sig");
PathBuf::from(path)
}
pub(crate) fn replay(args: &ReplayArgs) -> Outcome {
let trace = load_trace(&args.trace)?;
let wasm = read(&args.component)?;
let report = watoots_trace::replay(&trace, &wasm)?;
println!("{}", report.describe());
if let Some(path) = &args.emit_test {
write_test(path, &args.trace, &args.component)?;
eprintln!("wrote {}", path.display());
}
verdict(!args.assert || report.is_faithful())
}
pub(crate) fn trace(command: TraceCommand) -> Outcome {
let TraceCommand::Fmt {
input,
output,
binary: as_binary,
} = command;
let trace = load_trace(&input)?;
match output {
Some(path) => write(&path, encode(&trace, as_binary))?,
None if as_binary => {
return Err("--binary needs --output; a trace is not printed as bytes".into());
}
None => print!("{}", text::to_text(&trace)),
}
Ok(ExitCode::SUCCESS)
}
fn load_trace(path: &Path) -> Result<Trace, Failure> {
Ok(watoots_trace::decode(&read(path)?)
.map_err(|err| format!("{}: {}", path.display(), err.message()))?)
}
fn encode(trace: &Trace, as_binary: bool) -> Vec<u8> {
if as_binary {
binary::to_bytes(trace)
} else {
text::to_text(trace).into_bytes()
}
}
fn write_trace(trace: &Trace, path: &Path) -> Result<(), Failure> {
let as_binary = path.extension().is_some_and(|ext| ext == "wtr");
write(path, encode(trace, as_binary))
}
fn write_test(path: &Path, trace: &Path, component: &Path) -> Result<(), Failure> {
let name = trace
.file_stem()
.map_or_else(|| "trace".to_string(), |s| s.to_string_lossy().into())
.replace(['-', '.', ' '], "_");
let source = format!(
r#"// Generated by `watoots replay --emit-test`.
//
// Reproduces a recorded session with no application present: the trace answers
// the plugin's imports and drives its exports.
#[test]
fn replays_{name}() {{
let wasm = std::fs::read({component:?}).expect("reading the component");
let bytes = std::fs::read({trace:?}).expect("reading the trace");
let trace = watoots_trace::decode(&bytes).expect("parsing the trace");
let report = watoots_trace::replay(&trace, &wasm).expect("replaying");
assert!(report.is_faithful(), "{{}}", report.describe());
}}
"#
);
write(path, source)
}