Oregami
Repositories/oxedyne/ore

oxedyne/ore/cli/tests/capture.rs

9.4 KiB, 1 run

created by r2848102244:25, 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 capture costs, which is the whole reason for finding a real
2//! difference.
3//!
4//! The engine's `diff` module has its own tests and its own oracle. What is
5//! unproven until the binary is driven is whether the tool hands it the right
6//! two versions and authors what comes back: that two edits far apart are two
7//! small operations rather than one covering the file between them, that the
8//! bytes still come back exactly, and that `who` can then name each region's
9//! own author.
10
11mod support;
12
13use support::{
14 ore,
15 runs_in,
16 tree_of,
17 write,
18 Scratch,
19};
20
21use oxedyne_fe2o3_core::prelude::*;
22
23
24/// Builds a file of numbered lines, which is what a source file looks like to a
25/// line level difference.
26fn lines(n: usize) -> Vec<u8> {
27 let mut out = Vec::new();
28 for i in 0..n {
29 out.extend_from_slice(fmt!("line {:04} of the file, with enough on it to matter\n", i)
30 .as_bytes());
31 }
32 out
33}
34
35/// Returns the `edited` line the capture report wrote for a path.
36fn edited_line(text: &str, path: &str)
37 -> Outcome<String>
38{
39 for line in text.lines() {
40 let trimmed = line.trim_start();
41 if let Some(rest) = trimmed.strip_prefix("edited ") {
42 if rest.starts_with(path) {
43 return Ok(fmt!("{}", trimmed));
44 }
45 }
46 }
47 Err(err!("The report says nothing about editing {:?}: {}", path, text; Test, Missing))
48}
49
50/// Returns the two counts an `edited` line carries: operations, and bytes
51/// inserted.
52fn counts(line: &str)
53 -> Outcome<(usize, usize)>
54{
55 let fields: Vec<&str> = line.split_whitespace().collect();
56 // `edited <path> <ops> operation(s), <n> byte(s) inserted`
57 let at = match fields.iter().position(|f| f.starts_with("operation")) {
58 Some(i) if i > 0 => i,
59 _ => return Err(err!("The line {:?} carries no operation count.", line; Test)),
60 };
61 let ops = match fields[at - 1].parse::<usize>() {
62 Ok(n) => n,
63 Err(e) => return Err(err!(e, "The line {:?} has no number of operations.", line; Test)),
64 };
65 let at = match fields.iter().position(|f| f.starts_with("byte")) {
66 Some(i) if i > 0 => i,
67 _ => return Err(err!("The line {:?} carries no byte count.", line; Test)),
68 };
69 let inserted = match fields[at - 1].parse::<usize>() {
70 Ok(n) => n,
71 Err(e) => return Err(err!(e, "The line {:?} has no number of bytes.", line; Test)),
72 };
73 Ok((ops, inserted))
74}
75
76/// Two edits at opposite ends of a file are two small operations, and between
77/// them they insert what was typed rather than what lies between.
78#[test]
79fn two_distant_edits_are_two_small_operations() -> Outcome<()> {
80 let scratch = res!(Scratch::new("distant"));
81 let root = res!(scratch.sub("work"));
82 res!(res!(ore(&root, &["init"])).good("init"));
83
84 let base = lines(400);
85 res!(write(&root, "a.txt", &base));
86 res!(res!(ore(&root, &["mark", "base"])).good("mark base"));
87
88 // One word changed near the top and one near the bottom, thousands of bytes
89 // apart and both untouched in between.
90 let mut edited = fmt!("{}", String::from_utf8_lossy(&base));
91 edited = edited.replace("line 0005 of the file", "line 0005 OF THE FILE");
92 edited = edited.replace("line 0395 of the file", "line 0395 OF THE FILE");
93 assert_ne!(edited.as_bytes(), base.as_slice(), "the edits changed something");
94 res!(write(&root, "a.txt", edited.as_bytes()));
95
96 let out = res!(ore(&root, &["mark", "edited"]));
97 let text = fmt!("{}", res!(out.good("mark edited")));
98 let line = res!(edited_line(&text, "a.txt"));
99 let (ops, inserted) = res!(counts(&line));
100 assert_eq!(ops, 2, "one operation per edit, not one for the pair: {}", line);
101 // The file is over twenty kilobytes and the two edits are a handful of bytes
102 // each. A capture that trimmed a common prefix and suffix would have carried
103 // everything between them.
104 assert!(base.len() > 20_000, "the file is large: {} bytes", base.len());
105 assert!(inserted < 200,
106 "the operations carry the edits and not the file between them: {}", line);
107
108 // And the file is exactly what was written.
109 let now = res!(tree_of(&root));
110 assert_eq!(now.get("a.txt").map(|v| v.as_slice()), Some(edited.as_bytes()),
111 "the bytes come back exactly");
112 let out = res!(ore(&root, &["log"]));
113 let text = fmt!("{}", res!(out.good("log")));
114 assert!(text.contains("captured nothing"),
115 "and nothing is left over for the next verb: {}", text);
116 Ok(())
117}
118
119/// Two edits to two regions are owned by two operations, which is what the
120/// finer difference buys a reader of `who`.
121#[test]
122fn who_attributes_two_regions_to_two_operations() -> Outcome<()> {
123 let scratch = res!(Scratch::new("regions"));
124 let root = res!(scratch.sub("work"));
125 res!(res!(ore(&root, &["init"])).good("init"));
126
127 let base = lines(200);
128 res!(write(&root, "a.txt", &base));
129 res!(res!(ore(&root, &["mark", "base"])).good("mark base"));
130
131 let edited = fmt!("{}", String::from_utf8_lossy(&base))
132 .replace("line 0010 of the file", "line 0010 NEAR THE TOP")
133 .replace("line 0190 of the file", "line 0190 NEAR THE END");
134 res!(write(&root, "a.txt", edited.as_bytes()));
135 res!(res!(ore(&root, &["mark", "edited"])).good("mark edited"));
136
137 let out = res!(ore(&root, &["who", "a.txt"]));
138 let text = fmt!("{}", res!(out.good("who a.txt")));
139 let runs = runs_in(&text);
140 assert!(runs.len() >= 4, "the file is in several runs now: {}", text);
141
142 let top = match runs.iter().find(|r| r.text.contains("NEAR THE TOP")) {
143 Some(r) => r,
144 None => return Err(err!("No run holds the first edit: {}", text; Test, Missing)),
145 };
146 let end = match runs.iter().find(|r| r.text.contains("NEAR THE END")) {
147 Some(r) => r,
148 None => return Err(err!("No run holds the second edit: {}", text; Test, Missing)),
149 };
150 assert_ne!(top.op, end.op,
151 "the two regions are owned by two operations, not one: {}", text);
152 assert!(top.from < end.from, "and they are where they were typed: {}", text);
153
154 // The lines nobody touched still belong to the operation that first wrote
155 // them, which is what a splice covering both edits would have taken over.
156 // The first run is that operation, since the file opens with a line neither
157 // edit went near.
158 let base_op = fmt!("{}", runs[0].op);
159 assert_ne!(base_op, top.op, "the first edit is its own operation: {}", text);
160 assert_ne!(base_op, end.op, "and so is the second: {}", text);
161 let held = runs.iter().any(|r| r.op == base_op && r.from > top.from && r.from < end.from);
162 assert!(held, "and the untouched middle is still the first writer's: {}", text);
163 Ok(())
164}
165
166/// A file rewritten from end to end is captured as one splice, which is what
167/// the difference falls back to when the two versions have nothing left in
168/// common, and the bytes still come back exactly.
169#[test]
170fn a_whole_file_rewrite_still_round_trips() -> Outcome<()> {
171 let scratch = res!(Scratch::new("rewrite"));
172 let root = res!(scratch.sub("work"));
173 res!(res!(ore(&root, &["init"])).good("init"));
174
175 // Enough distinct lines on both sides that the line level edit script would
176 // exceed its budget, which is the path that falls back to one splice.
177 let mut before = Vec::new();
178 for i in 0..900 {
179 before.extend_from_slice(fmt!("before {:04}\n", i).as_bytes());
180 }
181 let mut after = Vec::new();
182 for i in 0..900 {
183 after.extend_from_slice(fmt!("after {:04} entirely\n", i).as_bytes());
184 }
185 res!(write(&root, "a.txt", &before));
186 res!(res!(ore(&root, &["mark", "before"])).good("mark before"));
187 res!(write(&root, "a.txt", &after));
188
189 let out = res!(ore(&root, &["mark", "after"]));
190 let text = fmt!("{}", res!(out.good("mark after")));
191 let line = res!(edited_line(&text, "a.txt"));
192 let (ops, _) = res!(counts(&line));
193 assert_eq!(ops, 1, "nothing in common is one splice: {}", line);
194
195 let now = res!(tree_of(&root));
196 assert_eq!(now.get("a.txt").map(|v| v.as_slice()), Some(after.as_slice()),
197 "and the rewrite comes back byte for byte");
198
199 // Both states are still reachable, since the log only grows.
200 res!(res!(ore(&root, &["back", "before"])).good("back before"));
201 let now = res!(tree_of(&root));
202 assert_eq!(now.get("a.txt").map(|v| v.as_slice()), Some(before.as_slice()));
203 Ok(())
204}
205
206/// A small edit to a large file already in the history stores the small edit.
207///
208/// This is the storage claim stated as an assertion: the operation that records
209/// one appended line carries that line, and the segment the log grew by is a
210/// long way short of the file.
211#[test]
212fn appending_one_line_stores_one_line() -> Outcome<()> {
213 let scratch = res!(Scratch::new("append"));
214 let root = res!(scratch.sub("work"));
215 res!(res!(ore(&root, &["init"])).good("init"));
216
217 let base = lines(2000);
218 res!(write(&root, "big.txt", &base));
219 res!(res!(ore(&root, &["mark", "base"])).good("mark base"));
220 let grew = res!(log_size(&root));
221
222 let mut more = base.clone();
223 more.extend_from_slice(b"one line appended at the very end\n");
224 res!(write(&root, "big.txt", &more));
225 let out = res!(ore(&root, &["mark", "appended"]));
226 let text = fmt!("{}", res!(out.good("mark appended")));
227 let line = res!(edited_line(&text, "big.txt"));
228 let (ops, inserted) = res!(counts(&line));
229 assert_eq!(ops, 1, "one appended line is one operation: {}", line);
230 assert_eq!(inserted, 34, "carrying the line and nothing else: {}", line);
231
232 // The log grew by the line, its record and a mark, not by the file.
233 let after = res!(log_size(&root));
234 let added = after - grew;
235 assert!(added < 1_000,
236 "the log grew by {} bytes for a {} byte file", added, base.len());
237 Ok(())
238}
239
240/// Returns the total size of every segment file of a repository.
241fn log_size(root: &std::path::Path)
242 -> Outcome<u64>
243{
244 let mut total = 0u64;
245 for entry in res!(std::fs::read_dir(root.join(".ore").join("log"))) {
246 let entry = res!(entry);
247 total += res!(entry.metadata()).len();
248 }
249 Ok(total)
250}