use amberfork_align::DiffParams;
use amberfork_model::Run;
use amberfork_record::normalize;
use amberfork_replay::{ReplayError, ReplayServer, Upstream};
use std::fmt;
use std::future::Future;
use crate::oracle::{RunVerdict, classify_recovery};
pub trait AgentDriver: Send + Sync {
fn drive(&self, base_url: &str) -> impl Future<Output = Result<(), AgentError>> + Send;
}
#[derive(Debug)]
#[non_exhaustive]
pub enum AgentError {
Spawn(std::io::Error),
}
impl fmt::Display for AgentError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Spawn(source) => write!(f, "cannot run the agent: {source}"),
}
}
}
impl std::error::Error for AgentError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Spawn(source) => Some(source),
}
}
}
#[derive(Debug)]
#[non_exhaustive]
pub enum ReexecError {
Bind(ReplayError),
Agent(AgentError),
}
impl fmt::Display for ReexecError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Bind(source) => write!(f, "cannot start the replay listener: {source}"),
Self::Agent(source) => write!(f, "{source}"),
}
}
}
impl std::error::Error for ReexecError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Bind(source) => Some(source),
Self::Agent(source) => Some(source),
}
}
}
pub(crate) async fn reexecute_once<U, D>(
patched: amberfork_record::Cassette,
good: &Run,
origin: usize,
driver: &D,
upstream: U,
reexecuted_id: &str,
params: &DiffParams,
) -> Result<RunVerdict, ReexecError>
where
U: Upstream + 'static,
D: AgentDriver,
{
let server = ReplayServer::bind(patched, upstream, reexecuted_id)
.await
.map_err(ReexecError::Bind)?;
driver
.drive(&server.base_url())
.await
.map_err(ReexecError::Agent)?;
let reexecuted = normalize(&server.reexecuted().await);
Ok(classify_recovery(good, &reexecuted, origin, params))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::testkit::{
B2, G0, G1, G2, ScriptedAgent, body_of, cassette, good_cassette, response,
};
use amberfork_record::Cassette;
use amberfork_replay::ScriptedUpstream;
fn good_run() -> Run {
normalize(&good_cassette())
}
fn patched_cassette() -> Cassette {
cassette("bad", &[("q0", G0), ("q1", G1), ("q2_bad", B2)])
}
#[tokio::test]
async fn a_rerun_that_follows_the_good_path_is_recovered() {
let verdict = reexecute_once(
patched_cassette(),
&good_run(),
1,
&ScriptedAgent {
requests: vec![body_of("q0"), body_of("q1"), body_of("q2_good")],
},
ScriptedUpstream::new([response(G2)]),
"reexec",
&DiffParams::default(),
)
.await
.expect("the experiment runs");
assert_eq!(verdict, RunVerdict::Recovered);
}
#[tokio::test]
async fn a_rerun_that_still_diverges_is_not_recovered() {
let verdict = reexecute_once(
patched_cassette(),
&good_run(),
1,
&ScriptedAgent {
requests: vec![body_of("q0"), body_of("q1"), body_of("q2_bad")],
},
ScriptedUpstream::new([]), "reexec",
&DiffParams::default(),
)
.await
.expect("the experiment runs");
assert_eq!(verdict, RunVerdict::NotRecovered);
}
}