Skip to main content

dev_chaos/
crash.rs

1//! Crash-point markers that truncate writes at a known byte offset.
2//!
3//! `CrashPoint::after_byte(N)` wraps a `Write` so the underlying type
4//! receives the first N bytes of the cumulative write stream and then
5//! every subsequent write returns `WriteZero`. The wrapper does not
6//! kill the process; it simulates "the process crashed after writing
7//! N bytes" so recovery code can be exercised in-process.
8//!
9//! Pair with [`dev-fixtures`'s `TempProject`](https://crates.io/crates/dev-fixtures)
10//! to model "crash mid-write, then reopen and recover."
11
12use std::io::{self, Write};
13
14/// A write-truncating crash marker.
15///
16/// `CrashPoint::after_byte(N).wrap(writer)` returns a writer that
17/// passes through up to `N` bytes (cumulative across all `write`
18/// calls), then refuses every subsequent byte with
19/// `ErrorKind::WriteZero`.
20///
21/// # Example
22///
23/// ```
24/// use dev_chaos::crash::CrashPoint;
25/// use std::io::Write;
26///
27/// let mut sink: Vec<u8> = Vec::new();
28/// let mut crashed = CrashPoint::after_byte(3).wrap(&mut sink);
29///
30/// crashed.write_all(b"abcd").ok();
31/// drop(crashed);
32/// assert_eq!(sink, b"abc");
33/// ```
34#[derive(Debug, Clone, Copy)]
35pub struct CrashPoint {
36    after: usize,
37}
38
39impl CrashPoint {
40    /// Crash after `n` bytes have been written cumulatively.
41    pub fn after_byte(n: usize) -> Self {
42        Self { after: n }
43    }
44
45    /// Wrap `writer` with this crash point.
46    pub fn wrap<W: Write>(self, writer: W) -> CrashWriter<W> {
47        CrashWriter {
48            inner: writer,
49            after: self.after,
50            written: 0,
51        }
52    }
53}
54
55/// Writer wrapper produced by [`CrashPoint::wrap`].
56pub struct CrashWriter<W: Write> {
57    inner: W,
58    after: usize,
59    written: usize,
60}
61
62impl<W: Write> CrashWriter<W> {
63    /// Total bytes successfully passed through to the inner writer.
64    pub fn bytes_written(&self) -> usize {
65        self.written
66    }
67
68    /// Consume the wrapper and return the underlying writer.
69    pub fn into_inner(self) -> W {
70        self.inner
71    }
72}
73
74impl<W: Write> Write for CrashWriter<W> {
75    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
76        if self.written >= self.after {
77            return Err(io::Error::new(
78                io::ErrorKind::WriteZero,
79                "crash point reached",
80            ));
81        }
82        let remaining = self.after - self.written;
83        let to_write = remaining.min(buf.len());
84        if to_write == 0 {
85            return Err(io::Error::new(
86                io::ErrorKind::WriteZero,
87                "crash point reached",
88            ));
89        }
90        let written = self.inner.write(&buf[..to_write])?;
91        self.written += written;
92        // If the caller asked for more than we let through, we report
93        // the partial write so they can detect the truncation.
94        Ok(written)
95    }
96
97    fn flush(&mut self) -> io::Result<()> {
98        self.inner.flush()
99    }
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105
106    #[test]
107    fn crash_after_byte_passes_through_then_truncates() {
108        let sink: Vec<u8> = Vec::new();
109        let mut w = CrashPoint::after_byte(3).wrap(sink);
110        w.write_all(b"abcd").ok();
111        let sink = w.into_inner();
112        assert_eq!(sink, b"abc");
113    }
114
115    #[test]
116    fn crash_after_zero_writes_nothing() {
117        let sink: Vec<u8> = Vec::new();
118        let mut w = CrashPoint::after_byte(0).wrap(sink);
119        let r = w.write(b"a");
120        assert!(r.is_err());
121        let sink = w.into_inner();
122        assert!(sink.is_empty());
123    }
124
125    #[test]
126    fn crash_with_large_budget_passes_through() {
127        let sink: Vec<u8> = Vec::new();
128        let mut w = CrashPoint::after_byte(1_000).wrap(sink);
129        w.write_all(b"hello").unwrap();
130        let sink = w.into_inner();
131        assert_eq!(sink, b"hello");
132    }
133
134    #[test]
135    fn bytes_written_tracks_progress() {
136        let sink: Vec<u8> = Vec::new();
137        let mut w = CrashPoint::after_byte(5).wrap(sink);
138        w.write_all(b"ab").unwrap();
139        assert_eq!(w.bytes_written(), 2);
140    }
141
142    #[test]
143    fn split_across_writes_still_truncates_at_offset() {
144        let sink: Vec<u8> = Vec::new();
145        let mut w = CrashPoint::after_byte(4).wrap(sink);
146        w.write_all(b"ab").unwrap();
147        // Next write_all asks for 4 more, only 2 fit, so write_all
148        // succeeds for the first chunk then errors.
149        let _ = w.write_all(b"cdef");
150        let sink = w.into_inner();
151        assert_eq!(sink, b"abcd");
152    }
153}