Oregami
Repositories/oxedyne/ore

oxedyne/ore/cli/tests/sweep.rs

14.2 KiB, 1 run

created by r2848102244:1175, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1//! What a store no longer notices, what the sweep notices instead, and what
2//! happens next.
3//!
4//! A vouched read of a sealed segment does not hash the record bodies. That was
5//! the only check standing between a body byte that flipped on the disk and a
6//! history that can never forget it: the signature is skipped on the same
7//! warrant, the file keeps its length and its modification time, and the fold a
8//! verdict is filed under is over the digests written beside the bodies rather
9//! than over the bodies. So the damage this file plants is damage nothing on the
10//! hot path can see, and that is asserted here rather than assumed.
11//!
12//! Every test plants the same thing: one byte of one record body, in a sealed
13//! segment a verdict vouches for, flipped in place. The file is the length it
14//! was and dated as it was, checked both times, because a tamper that moved
15//! either would be caught by something other than what is under test.
16//!
17//! The three answers, one test each:
18//!
19//! - the hot path does not notice, which is the trade;
20//! - `ore repack --verify` does, and says something a person can act on;
21//! - a verdict that has gone off does too, with no sweep involved at all, which
22//! is why a timer that stopped cannot take the checking with it.
23
24mod support;
25
26use support::{
27 ore,
28 write,
29 Ran,
30 Scratch,
31};
32
33use oxedyne_fe2o3_core::prelude::*;
34
35use std::fs;
36use std::path::{
37 Path,
38 PathBuf,
39};
40
41
42/// The marker planted in a file's content, and damaged in the segment.
43///
44/// Content reaches a segment as itself, so a string written into a working file
45/// is findable in the bytes on the disk. Flipping a letter of it is bit rot as
46/// it actually reads: the record is the length it was and the same shape, and
47/// nothing but a hash over it could ever have told.
48const MARK: &[u8] = b"ROTHERE";
49
50
51/// A repository holding more than one segment, so that at least one of them is
52/// sealed and can earn a verdict.
53///
54/// A segment is sealed once the next one has been started, which happens past
55/// `SEGMENT_LIMIT` -- a megabyte of records -- so this writes rather more than
56/// that. The marker goes in the first file, which is therefore in the first
57/// segment and sealed long before the writing stops.
58fn stocked(what: &str)
59 -> Outcome<Scratch>
60{
61 let scratch = res!(Scratch::new(what));
62 let root = &scratch.path;
63 res!(ore(root, &["init"])).good("init")?;
64 let mut first = Vec::new();
65 first.extend_from_slice(MARK);
66 first.extend_from_slice(b"\n");
67 for i in 0..8_000 {
68 first.extend_from_slice(fmt!("the {}th line of the first file\n", i).as_bytes());
69 }
70 res!(write(root, "first.txt", &first));
71 res!(ore(root, &["mark", "one"])).good("mark one")?;
72 for f in 0..7 {
73 let mut body = Vec::new();
74 for i in 0..8_000 {
75 body.extend_from_slice(fmt!("file {} line {} of filler\n", f, i).as_bytes());
76 }
77 res!(write(root, &fmt!("filler{}.txt", f), &body));
78 res!(ore(root, &["mark", &fmt!("fill-{}", f)])).good("mark filler")?;
79 }
80 Ok(scratch)
81}
82
83/// Every segment file, in log order.
84fn segments(root: &Path)
85 -> Outcome<Vec<PathBuf>>
86{
87 let dir = root.join(".ore").join("log");
88 let mut out: Vec<PathBuf> = Vec::new();
89 for entry in res!(fs::read_dir(&dir)) {
90 let path = res!(entry).path();
91 if path.extension().map(|e| e == "seg").unwrap_or(false) {
92 out.push(path);
93 }
94 }
95 out.sort();
96 Ok(out)
97}
98
99/// The segment holding the marker, which is sealed because it is not the last.
100fn marked_segment(root: &Path)
101 -> Outcome<PathBuf>
102{
103 let all = res!(segments(root));
104 if all.len() < 2 {
105 return Err(err!(
106 "The fixture left {} segment{}, so nothing in it is sealed and there is \
107 nothing to vouch for.", all.len(), if all.len() == 1 { "" } else { "s" };
108 Test, Invalid));
109 }
110 for path in &all[..all.len() - 1] {
111 let bytes = res!(fs::read(path));
112 if bytes.windows(MARK.len()).any(|w| w == MARK) {
113 return Ok(path.clone());
114 }
115 }
116 Err(err!(
117 "No sealed segment carries the marker, so the fixture is not damaging what \
118 it thinks it is."; Test, Missing))
119}
120
121/// Flips one letter of the marker where it sits in a segment file, and puts the
122/// modification time back.
123///
124/// Both halves are asserted rather than trusted. A tamper that changed the
125/// length or the date would be caught by [`ore_store::verdict`] before anything
126/// under test here got a chance, and the test would pass for the wrong reason.
127fn rot(path: &Path)
128 -> Outcome<()>
129{
130 let was = res!(fs::metadata(path));
131 let when = res!(was.modified());
132 let mut bytes = res!(fs::read(path));
133 let at = res!(bytes.windows(MARK.len()).position(|w| w == MARK).ok_or_else(|| err!(
134 "The marker is not in {:?}.", path; Test, Missing)));
135 bytes[at + 1] ^= 0x20;
136 res!(fs::write(path, &bytes));
137 let now = res!(fs::metadata(path));
138 assert_eq!(now.len(), was.len(), "the rot must not change the length");
139 let file = res!(fs::OpenOptions::new().write(true).open(path));
140 res!(file.set_modified(when));
141 let after = res!(fs::metadata(path));
142 assert_eq!(res!(after.modified()), when, "nor the modification time");
143 Ok(())
144}
145
146/// The verdict file, as text, or empty where there is none.
147fn verdicts(root: &Path) -> String {
148 fs::read_to_string(root.join(".ore").join("verified")).unwrap_or_default()
149}
150
151/// Puts every verdict's date back by some days, which is the only thing
152/// separating a store that is swept from one that is not.
153fn age_verdicts(root: &Path, days: u128)
154 -> Outcome<()>
155{
156 let path = root.join(".ore").join("verified");
157 let text = res!(fs::read_to_string(&path));
158 let mut out = String::new();
159 for (i, line) in text.lines().enumerate() {
160 if i == 0 || line.trim().is_empty() {
161 out.push_str(line);
162 out.push('\n');
163 continue;
164 }
165 let field: Vec<&str> = line.split_whitespace().collect();
166 if field.len() != 5 {
167 return Err(err!(
168 "A verdict line has {} fields, not five: {:?}", field.len(), line;
169 Test, Invalid));
170 }
171 let was: u128 = res!(field[4].parse::<u128>().map_err(|e| err!(
172 "The verdict date {:?} is not a number: {}", field[4], e; Test, Invalid)));
173 let back = days * 24 * 60 * 60 * 1_000_000_000;
174 out.push_str(&fmt!("{} {} {} {} {}\n", field[0], field[1], field[2], field[3],
175 was.saturating_sub(back)));
176 }
177 res!(fs::write(&path, out));
178 Ok(())
179}
180
181/// Everything a test says about a command it expected to fail.
182fn refused(ran: &Ran, what: &str)
183 -> Outcome<String>
184{
185 if ran.ok {
186 return Err(err!(
187 "`ore {}` succeeded over a damaged segment: {}{}", what, ran.out, ran.err;
188 Test, Invalid));
189 }
190 Ok(fmt!("{}{}", ran.out, ran.err))
191}
192
193
194/// **The trade.** A flipped body byte in a vouched sealed segment goes
195/// unnoticed by the commands, and the same byte with the verdict withdrawn does
196/// not.
197///
198/// The second half is what stops the first half being vacuous. A test that only
199/// showed a command succeeding over a damaged store would pass just as well if
200/// the damage had never landed, or had landed somewhere nothing reads; putting
201/// the verdict aside and watching the same command refuse the same bytes proves
202/// the damage is real, is on the path, and is exactly what the skip is skipping.
203#[test]
204fn a_flipped_byte_in_a_vouched_segment_is_invisible_on_the_hot_path() -> Outcome<()> {
205 let scratch = res!(stocked("invisible"));
206 let root = &scratch.path;
207 // A verdict for the sealed segment, earned by a read that checked it.
208 res!(ore(root, &["flags"])).good("flags")?;
209 let seg = res!(marked_segment(root));
210 let name = fmt!("{}", res!(seg.file_name().ok_or_else(|| err!(
211 "The segment has no file name."; Test, Missing))).to_string_lossy());
212 assert!(verdicts(root).contains(&name),
213 "the segment must be vouched for, or nothing is being skipped:\n{}",
214 verdicts(root));
215
216 res!(rot(&seg));
217
218 let after = res!(ore(root, &["flags"]));
219 assert!(after.ok, "the hot path does not notice: {}{}", after.out, after.err);
220 let again = res!(ore(root, &["who", "first.txt"]));
221 assert!(again.ok, "and neither does the next one: {}{}", again.out, again.err);
222
223 // The same bytes, with nothing vouching for them. The framing digest is
224 // intact and firing; it was being skipped, and that is the whole of what
225 // changed.
226 res!(fs::remove_file(root.join(".ore").join("verified")));
227 let said = res!(refused(&res!(ore(root, &["flags"])), "flags"));
228 assert!(said.contains("fails its integrity check"),
229 "and says what is wrong when it does look: {}", said);
230 assert!(said.contains(&name), "naming the file: {}", said);
231 Ok(())
232}
233
234/// **The sweep notices, and says something a person could act on.**
235///
236/// Not only that it fails. The message has to name the segment, say what is
237/// wrong with it in the words the reader used, say what the tool has done about
238/// it, and say what the person is to do -- because the whole reason this exists
239/// is that nothing else is looking, and a sweep whose output does not survive
240/// being read once is the same as no sweep.
241#[test]
242fn the_sweep_finds_the_rot_and_says_what_to_do_about_it() -> Outcome<()> {
243 let scratch = res!(stocked("finds"));
244 let root = &scratch.path;
245 res!(ore(root, &["flags"])).good("flags")?;
246 let seg = res!(marked_segment(root));
247 let name = fmt!("{}", res!(seg.file_name().ok_or_else(|| err!(
248 "The segment has no file name."; Test, Missing))).to_string_lossy());
249 res!(rot(&seg));
250
251 let said = res!(refused(&res!(ore(root, &["repack", "--verify"])), "repack --verify"));
252 assert!(said.contains(&name), "the sweep names the segment: {}", said);
253 assert!(said.contains("is damaged"), "and says so plainly: {}", said);
254 assert!(said.contains("fails its integrity check"),
255 "and carries the reader's own sentence: {}", said);
256 assert!(said.contains("no verdict was left"),
257 "and says what it has done about it: {}", said);
258 assert!(said.contains("backup") || said.contains("replica"),
259 "and what the person is to do: {}", said);
260
261 // What it did about it, on the disk: the damaged segment has no verdict, so
262 // the next command reads it the slow way and refuses.
263 assert!(!verdicts(root).contains(&name),
264 "the damaged segment keeps no verdict:\n{}", verdicts(root));
265 let next = res!(refused(&res!(ore(root, &["flags"])), "flags"));
266 assert!(next.contains("fails its integrity check"),
267 "and the next verb refuses, naming the fault: {}", next);
268
269 // And the record of the sweep says the same thing, for anybody who was not
270 // watching the terminal it ran in.
271 let record = res!(fs::read_to_string(root.join(".ore").join("checked")));
272 assert!(record.contains("ORESWEEP"), "the record says what it is: {}", record);
273 assert!(record.contains(&name), "and names the damaged segment: {}", record);
274 Ok(())
275}
276
277/// **A verdict that has gone off buys nothing, and no sweep was involved.**
278///
279/// This is the answer to a timer that stopped six months ago. The store here has
280/// never been swept and never will be; the notes are simply a week and a day
281/// old, and that is enough to put every body byte back under the hash. What a
282/// dead sweep costs is speed, not the check.
283#[test]
284fn a_verdict_that_has_gone_off_puts_the_checking_back() -> Outcome<()> {
285 let scratch = res!(stocked("expiry"));
286 let root = &scratch.path;
287 res!(ore(root, &["flags"])).good("flags")?;
288 let seg = res!(marked_segment(root));
289 res!(rot(&seg));
290 // Fresh notes, so the damage is invisible -- the state the previous test
291 // leaves off at, restated here so this one does not depend on it.
292 let hidden = res!(ore(root, &["flags"]));
293 assert!(hidden.ok, "while the notes are fresh: {}{}", hidden.out, hidden.err);
294
295 res!(age_verdicts(root, 8));
296 let said = res!(refused(&res!(ore(root, &["flags"])), "flags"));
297 assert!(said.contains("fails its integrity check"),
298 "a week later the same command reads the same bytes and refuses: {}", said);
299 Ok(())
300}
301
302/// A sound store sweeps clean, says how much it read, and leaves fresh notes.
303///
304/// The dates moving is the assertion that matters: a sweep that reported success
305/// without refreshing anything would leave every verdict to go off on its old
306/// date, and the store would be re-read by whichever command came next -- which
307/// is safe, and is also the sweep having done nothing at all.
308#[test]
309fn a_sound_store_sweeps_clean_and_the_notes_move_forward() -> Outcome<()> {
310 let scratch = res!(stocked("clean"));
311 let root = &scratch.path;
312 res!(ore(root, &["flags"])).good("flags")?;
313 res!(age_verdicts(root, 3));
314 let before = verdicts(root);
315
316 let ran = res!(ore(root, &["repack", "--verify"]));
317 let said = ran.good("repack --verify")?;
318 assert!(said.contains("every record holds its digest"),
319 "the sweep says what it checked: {}", said);
320 assert!(said.contains("every signature verified"),
321 "signatures included: {}", said);
322 assert!(said.contains("segments read"), "and how much: {}", said);
323
324 let after = verdicts(root);
325 assert_ne!(after, before, "the notes must be rewritten, not left where they were");
326 let dates = |text: &str| -> Vec<u128> {
327 text.lines().skip(1)
328 .filter_map(|l| l.split_whitespace().nth(4))
329 .filter_map(|f| f.parse::<u128>().ok())
330 .collect()
331 };
332 let (was, now) = (dates(&before), dates(&after));
333 assert!(!now.is_empty(), "there are notes to have moved:\n{}", after);
334 assert_eq!(now.len(), was.len(), "and the same segments are vouched for");
335 for (i, (old, new)) in was.iter().zip(now.iter()).enumerate() {
336 assert!(new > old, "note {} moved forward: {} to {}", i, old, new);
337 }
338
339 // A repack --verify moves no byte of the log, which is the difference between
340 // it and the repack it shares a word with.
341 let paths = res!(segments(root));
342 assert!(paths.len() > 1, "the fixture has segments to leave alone");
343 Ok(())
344}
345
346/// `ore flags` says how long it has been since anything read the whole log, and
347/// stops saying it once something has.
348///
349/// This is the only place the tool volunteers it. Every other verb is silent,
350/// because `ore mark` runs on every commit in every tree on this machine and a
351/// line printed that often is a line nobody reads.
352#[test]
353fn flags_says_when_the_whole_log_was_last_checked() -> Outcome<()> {
354 let scratch = res!(stocked("unswept"));
355 let root = &scratch.path;
356 let first = res!(ore(root, &["flags"]));
357 let said = first.good("flags")?;
358 assert!(said.contains("nothing has ever read this whole log"),
359 "an unswept store says so: {}", said);
360 assert!(said.contains("ore repack --verify"), "and names the command: {}", said);
361
362 res!(ore(root, &["repack", "--verify"])).good("repack --verify")?;
363 let after = res!(ore(root, &["flags"]));
364 let quiet = after.good("flags")?;
365 assert!(!quiet.contains("read this whole log"),
366 "and a swept one says nothing: {}", quiet);
367 Ok(())
368}