use std::cell::{Cell, RefCell};
use std::collections::VecDeque;
use std::io;
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum Node {
Seed {
value: Value,
},
Action {
tick: Tick,
value: Value,
},
Request {
operation: String,
arguments: Value,
},
Call {
operation: String,
arguments: Value,
result: Value,
},
Check {
value: Value,
},
End,
}
#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub struct Tick {
pub monotonic_ms: u64,
pub unix_seconds: i64,
}
enum Mode {
Live,
Replay(VecDeque<Node>),
}
pub struct Tape {
mode: RefCell<Mode>,
fault: Cell<Option<&'static str>>,
tick: Cell<Tick>,
finished: Cell<bool>,
started: std::time::Instant,
#[cfg(feature = "test-support")]
fixture: Option<Fixture>,
}
#[cfg(feature = "test-support")]
type FixtureResponder = dyn Fn(&str, &Value) -> io::Result<Value>;
#[cfg(feature = "test-support")]
struct Fixture {
nodes: RefCell<Vec<Node>>,
respond: Box<FixtureResponder>,
}
impl Default for Tape {
fn default() -> Self {
Self::live()
}
}
impl Tape {
pub fn live() -> Self {
Self {
mode: RefCell::new(Mode::Live),
fault: Cell::new(None),
tick: Cell::new(Tick::default()),
finished: Cell::new(false),
started: std::time::Instant::now(),
#[cfg(feature = "test-support")]
fixture: None,
}
}
pub fn new() -> Self {
Self::live()
}
pub fn replay(nodes: Vec<Node>) -> Self {
Self {
mode: RefCell::new(Mode::Replay(nodes.into())),
..Self::live()
}
}
#[cfg(feature = "test-support")]
pub fn fixture(respond: impl Fn(&str, &Value) -> io::Result<Value> + 'static) -> Self {
Self {
fixture: Some(Fixture {
nodes: RefCell::new(Vec::new()),
respond: Box::new(respond),
}),
..Self::live()
}
}
#[cfg(feature = "test-support")]
pub fn fixture_nodes(&self) -> Vec<Node> {
self.fixture
.as_ref()
.expect("fixture recorder")
.nodes
.borrow()
.clone()
}
#[cfg(feature = "test-support")]
fn has_fixture(&self) -> bool {
self.fixture.is_some()
}
#[cfg(not(feature = "test-support"))]
fn has_fixture(&self) -> bool {
false
}
pub fn is_replay(&self) -> bool {
matches!(&*self.mode.borrow(), Mode::Replay(_))
}
pub fn observes(&self) -> bool {
self.is_replay() || self.captures()
}
pub fn sample_tick(&self) -> Tick {
if self.is_replay() || self.has_fixture() {
return self.now();
}
Tick {
monotonic_ms: u64::try_from(self.started.elapsed().as_millis())
.unwrap_or(u64::MAX)
.max(self.now().monotonic_ms),
unix_seconds: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |duration| {
i64::try_from(duration.as_secs()).unwrap_or(i64::MAX)
}),
}
}
pub fn now(&self) -> Tick {
self.tick.get()
}
pub fn set_tick(&self, tick: Tick) -> io::Result<()> {
if tick.monotonic_ms < self.tick.get().monotonic_ms {
return self.fail("clock moved backwards");
}
self.tick.set(tick);
Ok(())
}
fn fail<T>(&self, message: &'static str) -> io::Result<T> {
if self.fault.get().is_none() {
self.fault.set(Some(message));
}
Err(io::Error::other(message))
}
pub fn healthy(&self) -> io::Result<()> {
match self.fault.get() {
Some(message) => Err(io::Error::other(message)),
None => Ok(()),
}
}
fn pop(&self) -> io::Result<Node> {
self.healthy()?;
let node = match &mut *self.mode.borrow_mut() {
Mode::Replay(nodes) => nodes.pop_front(),
Mode::Live => None,
};
node.ok_or_else(|| {
if self.fault.get().is_none() {
self.fault.set(Some("unexpected end of replay"));
}
io::Error::other("unexpected end of replay")
})
}
fn captures(&self) -> bool {
if self.has_fixture() {
return true;
}
crate::capture_content()
}
fn emit(&self, node: &Node) {
#[cfg(feature = "test-support")]
if let Some(fixture) = &self.fixture {
fixture.nodes.borrow_mut().push(node.clone());
return;
}
if crate::capture_content() {
crate::record(crate::EventKind::Replay, node);
}
}
fn value<T: Serialize>(&self, value: &T) -> io::Result<Value> {
let mut bytes = crate::bounded::Bytes::new(crate::MAX_RECORD_BYTES);
if serde_json::to_writer(&mut bytes, value).is_err() {
if self.is_replay() || self.has_fixture() {
return self.fail("replay value exceeds capture bound");
}
crate::mark_incomplete("forensic value exceeds cap or cannot serialize");
return Ok(Value::Null);
}
match serde_json::from_slice(&bytes.into_vec()) {
Ok(value) => Ok(value),
Err(_) => self.fail("encoded replay value cannot decode"),
}
}
fn decode<T: DeserializeOwned>(&self, value: Value) -> io::Result<T> {
serde_json::from_value(value).map_err(|_| {
if self.fault.get().is_none() {
self.fault.set(Some("replay payload does not decode"));
}
io::Error::other("replay payload does not decode")
})
}
pub fn seed<S: Serialize>(&self, seed: &S) -> io::Result<()> {
if self.is_replay() {
return self.fail("live seed entered replay");
}
if self.finished.get() {
return self.fail("recording finished");
}
if self.captures() {
self.emit(&Node::Seed {
value: self.value(seed)?,
});
}
Ok(())
}
pub fn take_seed<S: DeserializeOwned>(&self) -> io::Result<S> {
match self.pop()? {
Node::Seed { value } => self.decode(value),
_ => self.fail("replay must start with seed"),
}
}
pub fn action<A: Serialize>(&self, tick: Tick, action: &A) -> io::Result<()> {
if self.is_replay() {
return self.fail("live action entered replay");
}
if self.finished.get() {
return self.fail("recording finished");
}
self.set_tick(tick)?;
if self.captures() {
self.emit(&Node::Action {
tick,
value: self.value(action)?,
});
}
Ok(())
}
pub fn next<A: DeserializeOwned>(&self) -> io::Result<Option<A>> {
match self.pop()? {
Node::Action { tick, value } => {
self.set_tick(tick)?;
self.decode(value).map(Some)
}
Node::End => {
let empty = matches!(&*self.mode.borrow(), Mode::Replay(nodes) if nodes.is_empty());
if !empty {
return self.fail("records after replay end");
}
Ok(None)
}
_ => self.fail("unconsumed replay observation"),
}
}
pub fn request<A: Serialize>(&self, operation: &str, arguments: &A) -> io::Result<bool> {
self.healthy()?;
if self.finished.get() && !self.is_replay() {
return self.fail("recording finished");
}
if !self.is_replay() && !self.captures() {
return Ok(true);
}
let arguments = self.value(arguments)?;
#[cfg(feature = "test-support")]
if self.fixture.is_some() {
self.emit(&Node::Request {
operation: operation.into(),
arguments,
});
return Ok(false);
}
if self.is_replay() {
match self.pop()? {
Node::Request {
operation: expected,
arguments: wanted,
} if expected == operation && wanted == arguments => Ok(false),
_ => self.fail("request identity or arguments diverged"),
}
} else {
self.emit(&Node::Request {
operation: operation.into(),
arguments,
});
Ok(true)
}
}
pub fn call<A: Serialize, R: Serialize + DeserializeOwned>(
&self,
operation: &str,
arguments: &A,
native: impl FnOnce() -> R,
) -> io::Result<R> {
self.healthy()?;
if self.finished.get() && !self.is_replay() {
return self.fail("recording finished");
}
if !self.is_replay() && !self.captures() {
return Ok(native());
}
let arguments = self.value(arguments)?;
#[cfg(feature = "test-support")]
if let Some(fixture) = &self.fixture {
let result = match (fixture.respond)(operation, &arguments) {
Ok(result) => result,
Err(error) => {
if self.fault.get().is_none() {
self.fault.set(Some("fixture observation missing"));
}
return Err(error);
}
};
self.emit(&Node::Call {
operation: operation.into(),
arguments,
result: result.clone(),
});
return self.decode(result);
}
if self.is_replay() {
match self.pop()? {
Node::Call {
operation: expected,
arguments: wanted,
result,
} if expected == operation && wanted == arguments => self.decode(result),
_ => self.fail("synchronous service call diverged"),
}
} else {
let result = native();
self.emit(&Node::Call {
operation: operation.into(),
arguments,
result: self.value(&result)?,
});
Ok(result)
}
}
pub fn check<C: Serialize>(&self, state: &C) -> io::Result<()> {
self.healthy()?;
if self.finished.get() && !self.is_replay() {
return self.fail("recording finished");
}
if !self.is_replay() && !self.captures() {
return Ok(());
}
let value = self.value(state)?;
if self.is_replay() {
match self.pop()? {
Node::Check { value: expected } if value == expected => Ok(()),
_ => self.fail("editor state diverged"),
}
} else {
self.emit(&Node::Check { value });
Ok(())
}
}
pub fn finish(&self) -> io::Result<()> {
self.healthy()?;
if self.is_replay() {
return self.fail("live finish entered replay");
}
if self.finished.get() {
return self.fail("recording finished");
}
self.finished.set(true);
self.emit(&Node::End);
Ok(())
}
}