use std::path::Path;
use std::process::Command;
use aver::nan_value::{NanValue, NanValueConvert};
use aver::replay::{
JsonValue, RecordedOutcome, SessionRecording, first_diff_path, format_json, value_to_json,
};
use aver::value::Value;
use aver::vm;
use super::super::commands::find_self_host_binary;
use super::super::shared::apply_runtime_policy_to_vm;
#[cfg(feature = "wasm")]
use super::super::shared::{parse_file, read_file};
#[cfg(feature = "wasm")]
use super::find_fn_line;
use super::{
ReplayError, ReplayResult, decode_entry_args, find_json_line, resolve_replay_program_file,
};
pub(super) struct BackendReplayOutcome {
pub actual: RecordedOutcome,
pub effects_consumed: usize,
pub effects_total: usize,
pub args_diff_count: usize,
}
pub(super) fn build_replay_result(
path: &Path,
raw: &str,
recording: &SessionRecording,
replay_program_file: String,
entry_line: usize,
outcome: Result<BackendReplayOutcome, String>,
) -> ReplayResult {
let recording_output_line = find_json_line(raw, "output");
match outcome {
Ok(out) => {
let matched = out.actual == recording.output;
let output_diff = if !matched {
build_output_diff(&recording.output, &out.actual)
} else {
None
};
ReplayResult {
recording_path: path.display().to_string(),
program_file: replay_program_file,
entry_fn: recording.entry_fn.clone(),
entry_line,
matched,
effects_consumed: out.effects_consumed,
effects_total: out.effects_total,
error: None,
output_diff,
args_diffs: out.args_diff_count,
recording_output_line,
}
}
Err(e) => ReplayResult {
recording_path: path.display().to_string(),
program_file: replay_program_file,
entry_fn: recording.entry_fn.clone(),
entry_line,
matched: false,
effects_consumed: 0,
effects_total: recording.effects.len(),
error: Some(ReplayError::Generic(e)),
output_diff: None,
args_diffs: 0,
recording_output_line,
},
}
}
fn build_output_diff(
expected: &RecordedOutcome,
actual: &RecordedOutcome,
) -> Option<(String, String, Option<String>)> {
match (expected, actual) {
(RecordedOutcome::Value(exp), RecordedOutcome::Value(got)) => {
let diff_path = first_diff_path(exp, got).map(|p| p.to_string());
Some((format_json(exp), format_json(got), diff_path))
}
(RecordedOutcome::RuntimeError(exp), RecordedOutcome::RuntimeError(got)) => Some((
format!("runtime_error: {}", exp),
format!("runtime_error: {}", got),
None,
)),
(exp, got) => Some((format!("{:?}", exp), format!("{:?}", got), None)),
}
}
pub(super) fn run_vm_replay(
recording: &SessionRecording,
replay_module_root: &str,
items: &mut Vec<aver::ast::TopLevel>,
check_args: bool,
) -> Result<BackendReplayOutcome, String> {
let pipeline_result = aver::ir::pipeline::run(
items,
aver::ir::PipelineConfig {
typecheck: Some(aver::ir::TypecheckMode::Full {
base_dir: Some(replay_module_root),
}),
..Default::default()
},
);
let tc_result = pipeline_result.typecheck.expect("typecheck was requested");
if !tc_result.errors.is_empty() {
return Err(crate::shared::format_type_errors(&tc_result.errors));
}
let mut arena = aver::nan_value::Arena::new();
vm::register_service_types(&mut arena);
let (code, globals) = vm::compile_program_with_modules(
items,
&mut arena,
Some(replay_module_root),
&recording.program_file,
pipeline_result.analysis.as_ref(),
)
.map_err(|e| format!("VM compile error: {}", e))?;
let mut machine = vm::VM::new(code, globals, arena);
apply_runtime_policy_to_vm(&mut machine, replay_module_root)?;
machine.start_replay(recording.effects.clone(), check_args);
let progress_msg = |machine: &vm::VM, e: &dyn std::fmt::Display| -> String {
let (consumed, total) = machine.replay_progress();
format!(
"Replay failed: {}\nProgress: consumed {} of {} recorded effects",
e, consumed, total
)
};
machine
.run_top_level()
.map_err(|e| progress_msg(&machine, &e))?;
let entry_args = decode_entry_args(&recording.input)?;
let nv_args: Vec<NanValue> = entry_args
.iter()
.map(|v| NanValue::from_value(v, &mut machine.arena))
.collect();
let run_out = machine
.run_named_function(&recording.entry_fn, &nv_args)
.map_err(|e| progress_msg(&machine, &e))?;
let actual = if run_out.is_err() {
let inner = run_out.wrapper_inner(&machine.arena);
RecordedOutcome::RuntimeError(format!(
"{} returned error: {}",
recording.entry_fn,
inner.repr(&machine.arena)
))
} else {
let val = run_out.to_value(&machine.arena);
RecordedOutcome::Value(value_to_json(&val)?)
};
machine
.ensure_replay_consumed()
.map_err(|e| progress_msg(&machine, &format!("{:?}", e) as &dyn std::fmt::Display))?;
let (effects_consumed, effects_total) = machine.replay_progress();
Ok(BackendReplayOutcome {
actual,
effects_consumed,
effects_total,
args_diff_count: machine.args_diff_count(),
})
}
#[cfg(feature = "wasm")]
pub(super) fn run_wasm_gc_replay(
recording: &SessionRecording,
replay_module_root: &str,
replay_program_file: &str,
check_args: bool,
) -> Result<BackendReplayOutcome, String> {
let entry_info: Option<(String, Vec<Value>)> =
if recording.entry_fn == "main" && matches!(&recording.input, JsonValue::Null) {
None
} else {
let args = decode_entry_args(&recording.input)?;
Some((recording.entry_fn.clone(), args))
};
let mode =
super::super::run_wasm_gc::EffectMode::Replaying(Box::new(recording.clone()), check_args);
let run = super::super::run_wasm_gc::try_run_wasm_gc(
replay_program_file,
Some(replay_module_root),
Vec::new(),
mode,
entry_info,
)?;
Ok(BackendReplayOutcome {
actual: RecordedOutcome::Value(run.output),
effects_consumed: run.effects_consumed,
effects_total: run.effects_total,
args_diff_count: run.args_diff_count,
})
}
#[cfg(feature = "wasm")]
pub(super) fn find_fn_line_in_file(program_file: &str, name: &str) -> usize {
match read_file(program_file).and_then(|src| parse_file(&src)) {
Ok(items) => find_fn_line(&items, name),
Err(_) => 1,
}
}
pub(super) fn run_self_host_replay(
recording: &SessionRecording,
replay_module_root: &str,
recording_path: &Path,
check_args: bool,
) -> Result<BackendReplayOutcome, String> {
let replay_program_file = resolve_replay_program_file(recording, replay_module_root);
let binary_path = find_self_host_binary()?;
let guest_args = decode_self_host_guest_args(&recording.input)?;
let mut command = Command::new(&binary_path);
command
.arg(&replay_program_file)
.arg(replay_module_root)
.args(&guest_args)
.env("AVER_REPLAY_ENTRY_FN", "main")
.env("AVER_REPLAY_REPLAY", recording_path)
.env("AVER_REPLAY_MODULE_ROOT", replay_module_root)
.env_remove("AVER_REPLAY_RECORD")
.env_remove("AVER_REPLAY_REQUEST_ID")
.env_remove("AVER_REPLAY_TIMESTAMP")
.env_remove("AVER_REPLAY_PROGRAM_FILE");
if check_args {
command.env("AVER_REPLAY_CHECK_ARGS", "1");
} else {
command.env_remove("AVER_REPLAY_CHECK_ARGS");
}
let output = command.output().map_err(|e| {
format!(
"Failed to run cached self-host replay binary '{}': {}",
binary_path.display(),
e
)
})?;
if !output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
let mut msg = "Self-host replay failed".to_string();
if !stdout.is_empty() {
msg.push_str(&format!("\nstdout:\n{}", stdout));
}
if !stderr.is_empty() {
msg.push_str(&format!("\nstderr:\n{}", stderr));
}
return Err(msg);
}
let n = recording.effects.len();
Ok(BackendReplayOutcome {
actual: recording.output.clone(),
effects_consumed: n,
effects_total: n,
args_diff_count: 0,
})
}
pub(super) fn decode_self_host_guest_args(input: &JsonValue) -> Result<Vec<String>, String> {
decode_entry_args(input)?
.into_iter()
.enumerate()
.map(|(idx, value)| match value {
Value::Str(s) => Ok(s),
other => Err(format!(
"Self-host replay expects guest input as List<String>; item {} was {}",
idx,
aver::value::aver_repr(&other)
)),
})
.collect()
}