1use std::cell::{Cell, RefCell};
22use std::collections::VecDeque;
23use std::io;
24
25use serde::{de::DeserializeOwned, Deserialize, Serialize};
26use serde_json::Value;
27
28#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
29#[serde(tag = "kind", rename_all = "snake_case")]
30pub enum Node {
31 Seed {
32 value: Value,
33 },
34 Action {
35 tick: Tick,
36 value: Value,
37 },
38 Request {
39 operation: String,
40 arguments: Value,
41 },
42 Call {
43 operation: String,
44 arguments: Value,
45 result: Value,
46 },
47 Check {
48 value: Value,
49 },
50 End,
51}
52
53#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
56pub struct Tick {
57 pub monotonic_ms: u64,
58 pub unix_seconds: i64,
59}
60
61enum Mode {
62 Live,
63 Replay(VecDeque<Node>),
64}
65
66pub struct Tape {
67 mode: RefCell<Mode>,
68 fault: Cell<Option<&'static str>>,
69 tick: Cell<Tick>,
70 finished: Cell<bool>,
71 started: std::time::Instant,
72 #[cfg(feature = "test-support")]
73 fixture: Option<Fixture>,
74}
75
76#[cfg(feature = "test-support")]
77type FixtureResponder = dyn Fn(&str, &Value) -> io::Result<Value>;
78
79#[cfg(feature = "test-support")]
80struct Fixture {
81 nodes: RefCell<Vec<Node>>,
82 respond: Box<FixtureResponder>,
83}
84
85impl Default for Tape {
86 fn default() -> Self {
87 Self::live()
88 }
89}
90
91impl Tape {
92 pub fn live() -> Self {
95 Self {
96 mode: RefCell::new(Mode::Live),
97 fault: Cell::new(None),
98 tick: Cell::new(Tick::default()),
99 finished: Cell::new(false),
100 started: std::time::Instant::now(),
101 #[cfg(feature = "test-support")]
102 fixture: None,
103 }
104 }
105
106 pub fn new() -> Self {
109 Self::live()
110 }
111
112 pub fn replay(nodes: Vec<Node>) -> Self {
114 Self {
115 mode: RefCell::new(Mode::Replay(nodes.into())),
116 ..Self::live()
117 }
118 }
119
120 #[cfg(feature = "test-support")]
124 pub fn fixture(respond: impl Fn(&str, &Value) -> io::Result<Value> + 'static) -> Self {
125 Self {
126 fixture: Some(Fixture {
127 nodes: RefCell::new(Vec::new()),
128 respond: Box::new(respond),
129 }),
130 ..Self::live()
131 }
132 }
133
134 #[cfg(feature = "test-support")]
136 pub fn fixture_nodes(&self) -> Vec<Node> {
137 self.fixture
138 .as_ref()
139 .expect("fixture recorder")
140 .nodes
141 .borrow()
142 .clone()
143 }
144
145 #[cfg(feature = "test-support")]
146 fn has_fixture(&self) -> bool {
147 self.fixture.is_some()
148 }
149
150 #[cfg(not(feature = "test-support"))]
151 fn has_fixture(&self) -> bool {
152 false
153 }
154
155 pub fn is_replay(&self) -> bool {
156 matches!(&*self.mode.borrow(), Mode::Replay(_))
157 }
158
159 pub fn observes(&self) -> bool {
161 self.is_replay() || self.captures()
162 }
163
164 pub fn sample_tick(&self) -> Tick {
167 if self.is_replay() || self.has_fixture() {
168 return self.now();
169 }
170 Tick {
171 monotonic_ms: u64::try_from(self.started.elapsed().as_millis())
172 .unwrap_or(u64::MAX)
173 .max(self.now().monotonic_ms),
174 unix_seconds: std::time::SystemTime::now()
175 .duration_since(std::time::UNIX_EPOCH)
176 .map_or(0, |duration| {
177 i64::try_from(duration.as_secs()).unwrap_or(i64::MAX)
178 }),
179 }
180 }
181
182 pub fn now(&self) -> Tick {
183 self.tick.get()
184 }
185
186 pub fn set_tick(&self, tick: Tick) -> io::Result<()> {
187 if tick.monotonic_ms < self.tick.get().monotonic_ms {
188 return self.fail("clock moved backwards");
189 }
190 self.tick.set(tick);
191 Ok(())
192 }
193
194 fn fail<T>(&self, message: &'static str) -> io::Result<T> {
195 if self.fault.get().is_none() {
196 self.fault.set(Some(message));
197 }
198 Err(io::Error::other(message))
199 }
200
201 pub fn healthy(&self) -> io::Result<()> {
203 match self.fault.get() {
204 Some(message) => Err(io::Error::other(message)),
205 None => Ok(()),
206 }
207 }
208
209 fn pop(&self) -> io::Result<Node> {
210 self.healthy()?;
211 let node = match &mut *self.mode.borrow_mut() {
212 Mode::Replay(nodes) => nodes.pop_front(),
213 Mode::Live => None,
214 };
215 node.ok_or_else(|| {
216 if self.fault.get().is_none() {
217 self.fault.set(Some("unexpected end of replay"));
218 }
219 io::Error::other("unexpected end of replay")
220 })
221 }
222
223 fn captures(&self) -> bool {
224 if self.has_fixture() {
225 return true;
226 }
227 crate::capture_content()
228 }
229
230 fn emit(&self, node: &Node) {
231 #[cfg(feature = "test-support")]
232 if let Some(fixture) = &self.fixture {
233 fixture.nodes.borrow_mut().push(node.clone());
234 return;
235 }
236 if crate::capture_content() {
237 crate::record(crate::EventKind::Replay, node);
238 }
239 }
240
241 fn value<T: Serialize>(&self, value: &T) -> io::Result<Value> {
245 let mut bytes = crate::bounded::Bytes::new(crate::chunk::MAX_VALUE_BYTES);
246 if serde_json::to_writer(&mut bytes, value).is_err() {
247 if self.is_replay() || self.has_fixture() {
248 return self.fail("replay value exceeds capture bound");
249 }
250 crate::mark_incomplete("forensic value exceeds cap or cannot serialize");
253 return Ok(Value::Null);
254 }
255 match serde_json::from_slice(&bytes.into_vec()) {
256 Ok(value) => Ok(value),
257 Err(_) => self.fail("encoded replay value cannot decode"),
258 }
259 }
260
261 fn decode<T: DeserializeOwned>(&self, value: Value) -> io::Result<T> {
262 serde_json::from_value(value).map_err(|_| {
263 if self.fault.get().is_none() {
264 self.fault.set(Some("replay payload does not decode"));
265 }
266 io::Error::other("replay payload does not decode")
267 })
268 }
269
270 pub fn seed<S: Serialize>(&self, seed: &S) -> io::Result<()> {
273 if self.is_replay() {
274 return self.fail("live seed entered replay");
275 }
276 if self.finished.get() {
277 return self.fail("recording finished");
278 }
279 if self.captures() {
280 self.emit(&Node::Seed {
281 value: self.value(seed)?,
282 });
283 }
284 Ok(())
285 }
286
287 pub fn take_seed<S: DeserializeOwned>(&self) -> io::Result<S> {
289 match self.pop()? {
290 Node::Seed { value } => self.decode(value),
291 _ => self.fail("replay must start with seed"),
292 }
293 }
294
295 pub fn action<A: Serialize>(&self, tick: Tick, action: &A) -> io::Result<()> {
297 if self.is_replay() {
298 return self.fail("live action entered replay");
299 }
300 if self.finished.get() {
301 return self.fail("recording finished");
302 }
303 self.set_tick(tick)?;
304 if self.captures() {
305 self.emit(&Node::Action {
306 tick,
307 value: self.value(action)?,
308 });
309 }
310 Ok(())
311 }
312
313 pub fn next<A: DeserializeOwned>(&self) -> io::Result<Option<A>> {
316 match self.pop()? {
317 Node::Action { tick, value } => {
318 self.set_tick(tick)?;
319 self.decode(value).map(Some)
320 }
321 Node::End => {
322 let empty = matches!(&*self.mode.borrow(), Mode::Replay(nodes) if nodes.is_empty());
323 if !empty {
324 return self.fail("records after replay end");
325 }
326 Ok(None)
327 }
328 _ => self.fail("unconsumed replay observation"),
329 }
330 }
331
332 pub fn request<A: Serialize>(&self, operation: &str, arguments: &A) -> io::Result<bool> {
337 self.healthy()?;
338 if self.finished.get() && !self.is_replay() {
339 return self.fail("recording finished");
340 }
341 if !self.is_replay() && !self.captures() {
342 return Ok(true);
343 }
344 let arguments = self.value(arguments)?;
345 #[cfg(feature = "test-support")]
346 if self.fixture.is_some() {
347 self.emit(&Node::Request {
348 operation: operation.into(),
349 arguments,
350 });
351 return Ok(false);
352 }
353 if self.is_replay() {
354 match self.pop()? {
355 Node::Request {
356 operation: expected,
357 arguments: wanted,
358 } if expected == operation && wanted == arguments => Ok(false),
359 _ => self.fail("request identity or arguments diverged"),
360 }
361 } else {
362 self.emit(&Node::Request {
363 operation: operation.into(),
364 arguments,
365 });
366 Ok(true)
367 }
368 }
369
370 pub fn call<A: Serialize, R: Serialize + DeserializeOwned>(
375 &self,
376 operation: &str,
377 arguments: &A,
378 native: impl FnOnce() -> R,
379 ) -> io::Result<R> {
380 self.healthy()?;
381 if self.finished.get() && !self.is_replay() {
382 return self.fail("recording finished");
383 }
384 if !self.is_replay() && !self.captures() {
385 return Ok(native());
386 }
387 let arguments = self.value(arguments)?;
388 #[cfg(feature = "test-support")]
389 if let Some(fixture) = &self.fixture {
390 let result = match (fixture.respond)(operation, &arguments) {
391 Ok(result) => result,
392 Err(error) => {
393 if self.fault.get().is_none() {
394 self.fault.set(Some("fixture observation missing"));
395 }
396 return Err(error);
397 }
398 };
399 self.emit(&Node::Call {
400 operation: operation.into(),
401 arguments,
402 result: result.clone(),
403 });
404 return self.decode(result);
405 }
406 if self.is_replay() {
407 match self.pop()? {
408 Node::Call {
409 operation: expected,
410 arguments: wanted,
411 result,
412 } if expected == operation && wanted == arguments => self.decode(result),
413 _ => self.fail("synchronous service call diverged"),
414 }
415 } else {
416 let result = native();
417 self.emit(&Node::Call {
418 operation: operation.into(),
419 arguments,
420 result: self.value(&result)?,
421 });
422 Ok(result)
423 }
424 }
425
426 pub fn check<C: Serialize>(&self, state: &C) -> io::Result<()> {
428 self.healthy()?;
429 if self.finished.get() && !self.is_replay() {
430 return self.fail("recording finished");
431 }
432 if !self.is_replay() && !self.captures() {
433 return Ok(());
434 }
435 let value = self.value(state)?;
436 if self.is_replay() {
437 match self.pop()? {
438 Node::Check { value: expected } if value == expected => Ok(()),
439 _ => self.fail("editor state diverged"),
440 }
441 } else {
442 self.emit(&Node::Check { value });
443 Ok(())
444 }
445 }
446
447 pub fn finish(&self) -> io::Result<()> {
449 self.healthy()?;
450 if self.is_replay() {
451 return self.fail("live finish entered replay");
452 }
453 if self.finished.get() {
454 return self.fail("recording finished");
455 }
456 self.finished.set(true);
457 self.emit(&Node::End);
458 Ok(())
459 }
460}