Oregami
Repositories/oxedyne/ore

oxedyne/ore/cli/tests/basics.rs

13.9 KiB, 20 runs

created by r2848102244:23, 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//! The verbs, driven as a person drives them: through the compiled binary, in a
2//! directory of its own.
3//!
4//! These tests treat the binary as the unit, because it is. Everything below it
5//! -- the log, the sequence, the two formats -- has its own tests in the engine;
6//! what is unproven until it is run this way is whether the tool assembles them
7//! into something that keeps a working copy.
8
9mod support;
10
11use support::{
12 marks,
13 operations,
14 ore,
15 tree_of,
16 write,
17 Scratch,
18};
19
20use oxedyne_fe2o3_core::prelude::*;
21
22use std::fs;
23
24
25/// The bytes the first mark holds.
26const FIRST_A: &[u8] = b"hello\nworld\n";
27/// The bytes the second mark holds.
28const SECOND_A: &[u8] = b"hello\nbrave\nworld\n";
29/// A second file, unchanged throughout, so that a verb that touches everything
30/// is caught doing it.
31const B: &[u8] = b"one\ntwo\nthree\n";
32
33
34/// A repository, two marks, a journey back to the first and forward to the
35/// second, and an edit after the journey.
36#[test]
37fn marks_and_time_travel() -> Outcome<()> {
38 let scratch = res!(Scratch::new("marks"));
39 let root = res!(scratch.sub("work"));
40
41 res!(res!(ore(&root, &["init"])).good("init"));
42 res!(write(&root, "a.txt", FIRST_A));
43 res!(write(&root, "b.txt", B));
44 res!(write(&root, "sub/c.txt", b"deep\n"));
45 let out = res!(ore(&root, &["mark", "first"]));
46 let text = fmt!("{}", res!(out.good("mark first")));
47 assert!(text.contains("3 created"), "the first mark captures three files: {}", text);
48
49 res!(write(&root, "a.txt", SECOND_A));
50 let out = res!(ore(&root, &["mark", "second"]));
51 let text = fmt!("{}", res!(out.good("mark second")));
52 assert!(text.contains("1 edited"), "the second mark captures one edit: {}", text);
53
54 // The log names both marks, newest first.
55 let out = res!(ore(&root, &["log"]));
56 let text = fmt!("{}", res!(out.good("log")));
57 let at_first = match text.find("\"first\"") {
58 Some(i) => i,
59 None => return Err(err!("The log does not name the first mark: {}", text; Test)),
60 };
61 let at_second = match text.find("\"second\"") {
62 Some(i) => i,
63 None => return Err(err!("The log does not name the second mark: {}", text; Test)),
64 };
65 assert!(at_second < at_first, "the log is newest first: {}", text);
66 // What the history holds before the journey, so that the claim below is that
67 // the log grew and not that it reached a particular number.
68 let was = res!(operations(&text));
69
70 // Back to the first mark, and the bytes are exactly what they were.
71 res!(res!(ore(&root, &["back", "first"])).good("back first"));
72 let now = res!(tree_of(&root));
73 assert_eq!(now.get("a.txt").map(|v| v.as_slice()), Some(FIRST_A));
74 assert_eq!(now.get("b.txt").map(|v| v.as_slice()), Some(B));
75 assert_eq!(now.get("sub/c.txt").map(|v| v.as_slice()), Some(&b"deep\n"[..]));
76
77 // And forward to the second, which is the state it was left in.
78 res!(res!(ore(&root, &["back", "second"])).good("back second"));
79 let now = res!(tree_of(&root));
80 assert_eq!(now.get("a.txt").map(|v| v.as_slice()), Some(SECOND_A));
81
82 // An edit made after a journey is captured like any other.
83 res!(write(&root, "a.txt", b"hello\nbrave\nnew\nworld\n"));
84 let out = res!(ore(&root, &["mark", "third"]));
85 let text = fmt!("{}", res!(out.good("mark third")));
86 assert!(text.contains("1 edited"), "the edit after the journey is captured: {}", text);
87 let out = res!(ore(&root, &["log"]));
88 let text = fmt!("{}", res!(out.good("log")));
89 assert!(text.contains("captured nothing"),
90 "nothing is left to capture straight after a mark: {}", text);
91 for name in ["\"first\"", "\"second\"", "\"third\""] {
92 assert!(text.contains(name), "the log names {}: {}", name, text);
93 }
94 // Nothing was rewritten: the journey and the mark after it added operations
95 // and took none away. A total is not written down here because every command
96 // that appends anything now ends by naming the point it reached, so the total
97 // is a function of how many commands the test happens to run; what is being
98 // claimed is that the log only grows, and that is a comparison.
99 let now = res!(operations(&text));
100 assert!(now > was,
101 "the log only grows: it held {} operations before the journey and {} after: {}",
102 was, now, text);
103 Ok(())
104}
105
106/// The log shows the marks a person named, and `--auto` shows the ones the tool
107/// wrote as well.
108///
109/// Every command that appends anything ends by naming the point it reached, so
110/// the plain listing would otherwise bury the handful somebody chose under
111/// several hundred nobody did. What is asserted here is the distinction itself:
112/// the named mark is in both listings, the automatic one is in exactly one of
113/// them, and the plain listing says how many it is not showing.
114#[test]
115fn the_log_shows_named_marks_and_auto_shows_the_rest() -> Outcome<()> {
116 let scratch = res!(Scratch::new("log_auto"));
117 let root = res!(scratch.sub("work"));
118 res!(res!(ore(&root, &["init"])).good("init"));
119 res!(write(&root, "a.txt", b"alpha\n"));
120 res!(res!(ore(&root, &["mark", "release"])).good("mark"));
121 // Three commands that append and name nothing, so there are marks to hide.
122 for i in 0..3 {
123 res!(write(&root, "a.txt", &fmt!("alpha\nline {}\n", i).into_bytes()));
124 res!(res!(ore(&root, &["note", "a.txt:1", "a remark"])).good("note"));
125 }
126
127 let out = res!(ore(&root, &["log"]));
128 let plain = fmt!("{}", res!(out.good("log")));
129 let (named, automatic) = marks(&plain);
130 assert_eq!(named, 1, "the mark somebody named is listed: {}", plain);
131 assert_eq!(automatic, 0, "and the ones the tool wrote are not: {}", plain);
132 assert!(plain.contains("`ore log --auto` lists those too"),
133 "the listing says what it is not showing, rather than leaving it to be \
134 noticed: {}", plain);
135
136 let out = res!(ore(&root, &["log", "--auto"]));
137 let all = fmt!("{}", res!(out.good("log --auto")));
138 let (named, automatic) = marks(&all);
139 assert_eq!(named, 1, "the named mark is in both listings: {}", all);
140 assert!(automatic >= 3,
141 "and --auto shows one for each command that appended anything: {}", all);
142 assert!(!all.contains("`ore log --auto` lists those too"),
143 "a listing showing everything has nothing to offer: {}", all);
144 Ok(())
145}
146
147
148/// A file deleted from the working copy is recorded as gone, and comes back on
149/// the journey to a mark that had it.
150#[test]
151fn a_deletion_is_an_operation() -> Outcome<()> {
152 let scratch = res!(Scratch::new("delete"));
153 let root = res!(scratch.sub("work"));
154 res!(res!(ore(&root, &["init"])).good("init"));
155 res!(write(&root, "keep.txt", b"kept\n"));
156 res!(write(&root, "gone.txt", b"going\n"));
157 res!(res!(ore(&root, &["mark", "both"])).good("mark both"));
158
159 res!(fs::remove_file(root.join("gone.txt")));
160 let out = res!(ore(&root, &["mark", "one"]));
161 let text = fmt!("{}", res!(out.good("mark one")));
162 assert!(text.contains("1 deleted"), "the deletion is captured: {}", text);
163 assert!(!root.join("gone.txt").exists());
164
165 res!(res!(ore(&root, &["back", "both"])).good("back both"));
166 let now = res!(tree_of(&root));
167 assert_eq!(now.get("gone.txt").map(|v| v.as_slice()), Some(&b"going\n"[..]));
168 Ok(())
169}
170
171/// The ignore file keeps files out of the history, and a dotfile it says
172/// nothing about goes in like any other.
173#[test]
174fn ignored_files_are_not_captured() -> Outcome<()> {
175 let scratch = res!(Scratch::new("ignore"));
176 let root = res!(scratch.sub("work"));
177 res!(res!(ore(&root, &["init"])).good("init"));
178 res!(write(&root, ".oreignore", b"build/\n*.tmp\nnotes.txt\n"));
179 res!(write(&root, "kept.txt", b"kept\n"));
180 res!(write(&root, "notes.txt", b"private\n"));
181 res!(write(&root, "scratch.tmp", b"rubbish\n"));
182 res!(write(&root, "build/out.bin", b"artefact\n"));
183 res!(write(&root, ".hidden", b"dotfile\n"));
184 let out = res!(ore(&root, &["mark", "one"]));
185 let text = fmt!("{}", res!(out.good("mark one")));
186 // The ignore file itself is captured, so that its rules travel with the
187 // repository; everything it names stays out; and a dotfile no rule mentions
188 // is content like anything else.
189 assert!(text.contains("3 created"),
190 "the kept file, the dotfile and the rules: {}", text);
191 assert!(text.contains("kept.txt"), "the kept file is the right one: {}", text);
192 assert!(text.contains(".oreignore"), "and the rules are recorded: {}", text);
193 assert!(text.contains(".hidden"), "and the dotfile with them: {}", text);
194 // The files left alone are still where they were.
195 assert!(root.join("notes.txt").exists());
196 assert!(root.join("build/out.bin").exists());
197 Ok(())
198}
199
200/// `who` accounts for every byte of a file, run by run.
201#[test]
202fn who_accounts_for_the_bytes() -> Outcome<()> {
203 let scratch = res!(Scratch::new("who"));
204 let root = res!(scratch.sub("work"));
205 res!(res!(ore(&root, &["init"])).good("init"));
206 res!(write(&root, "a.txt", FIRST_A));
207 res!(res!(ore(&root, &["mark", "first"])).good("mark first"));
208 res!(write(&root, "a.txt", SECOND_A));
209 res!(res!(ore(&root, &["mark", "second"])).good("mark second"));
210
211 let out = res!(ore(&root, &["who", "a.txt"]));
212 let text = fmt!("{}", res!(out.good("who a.txt")));
213 assert!(text.contains("18 bytes in 3 runs"),
214 "the middle of the file came from the second edit: {}", text);
215 assert!(text.contains("hello\\n"), "the runs show their content: {}", text);
216 assert!(text.contains("brave\\n"), "including the inserted stretch: {}", text);
217 // One replica wrote all of it, so every run names the same one. A second
218 // replica's runs are proven by the import tests, where git's authors supply
219 // the second writer.
220 let mut replicas = std::collections::BTreeSet::new();
221 for line in text.lines() {
222 if let Some(at) = line.find(" r") {
223 let rest = &line[at + 2..];
224 let name = rest.split(':').next().unwrap_or("");
225 replicas.insert(fmt!("{}", name));
226 }
227 }
228 assert_eq!(replicas.len(), 1, "one writer, one replica: {:?}", replicas);
229 Ok(())
230}
231
232/// `flags` says the history is clean when nothing concurrent has happened.
233#[test]
234fn flags_are_clean_on_a_linear_history() -> Outcome<()> {
235 let scratch = res!(Scratch::new("clean"));
236 let root = res!(scratch.sub("work"));
237 res!(res!(ore(&root, &["init"])).good("init"));
238 res!(write(&root, "a.txt", FIRST_A));
239 res!(res!(ore(&root, &["mark", "first"])).good("mark first"));
240 res!(write(&root, "a.txt", SECOND_A));
241 res!(res!(ore(&root, &["mark", "second"])).good("mark second"));
242 let out = res!(ore(&root, &["flags"]));
243 let text = fmt!("{}", res!(out.good("flags")));
244 assert!(text.contains("clean"), "a linear history flags nothing: {}", text);
245 Ok(())
246}
247
248/// Initialising twice is refused, since a second replica identifier for one
249/// working copy would let one writer spend another's counters.
250#[test]
251fn a_second_init_is_refused() -> Outcome<()> {
252 let scratch = res!(Scratch::new("twice"));
253 let root = res!(scratch.sub("work"));
254 res!(res!(ore(&root, &["init"])).good("init"));
255 let out = res!(ore(&root, &["init"]));
256 assert!(!out.ok, "the second init fails");
257 assert!(out.err.contains("already an Ore repository"),
258 "and says why: {}", out.err);
259 Ok(())
260}
261
262/// A verb outside a repository says so, and says what to do about it.
263#[test]
264fn a_verb_outside_a_repository_says_so() -> Outcome<()> {
265 let scratch = res!(Scratch::new("nowhere"));
266 let root = res!(scratch.sub("work"));
267 for verb in [
268 vec!["log"],
269 vec!["flags"],
270 vec!["mark", "x"],
271 vec!["who", "a.txt"],
272 vec!["back", "x"],
273 vec!["undo"],
274 ] {
275 let out = res!(ore(&root, &verb));
276 assert!(!out.ok, "`ore {}` outside a repository fails", verb.join(" "));
277 assert!(out.err.contains("ore init"),
278 "and says to run init: {}", out.err);
279 }
280 Ok(())
281}
282
283/// A segment cut short is an error that names the file it read.
284#[test]
285fn a_truncated_segment_names_itself() -> Outcome<()> {
286 let scratch = res!(Scratch::new("truncated"));
287 let root = res!(scratch.sub("work"));
288 res!(res!(ore(&root, &["init"])).good("init"));
289 res!(write(&root, "a.txt", FIRST_A));
290 res!(res!(ore(&root, &["mark", "first"])).good("mark first"));
291
292 let path = root.join(".ore").join("log").join("000000.seg");
293 let bytes = res!(fs::read(&path));
294 assert!(bytes.len() > 8, "the segment holds records");
295 res!(fs::write(&path, &bytes[..bytes.len() - 3]));
296
297 let out = res!(ore(&root, &["log"]));
298 assert!(!out.ok, "a truncated segment is an error");
299 assert!(out.err.contains("000000.seg"),
300 "and the error names the file: {}", out.err);
301 Ok(())
302}
303
304/// A log longer than one segment is written across several and replayed from
305/// all of them.
306#[test]
307fn segments_rotate_and_replay() -> Outcome<()> {
308 let scratch = res!(Scratch::new("rotate"));
309 let root = res!(scratch.sub("work"));
310 res!(res!(ore(&root, &["init"])).good("init"));
311 // One splice carrying more than the segment threshold, so the next append
312 // has to start a second file.
313 let big: Vec<u8> = (0..(1usize << 20) + 4096).map(|i| (i % 251) as u8).collect();
314 res!(write(&root, "big.dat", &big));
315 res!(res!(ore(&root, &["mark", "big"])).good("mark big"));
316 res!(write(&root, "small.txt", b"after the rotation\n"));
317 res!(res!(ore(&root, &["mark", "after"])).good("mark after"));
318
319 let dir = root.join(".ore").join("log");
320 let mut names: Vec<String> = Vec::new();
321 for entry in res!(fs::read_dir(&dir)) {
322 names.push(res!(entry).file_name().to_string_lossy().into_owned());
323 }
324 names.sort();
325 assert!(names.len() >= 2, "the log rotated onto a second segment: {:?}", names);
326
327 // Everything replays: both marks are there and the big file is intact.
328 let out = res!(ore(&root, &["log"]));
329 let text = fmt!("{}", res!(out.good("log")));
330 assert!(text.contains("\"big\"") && text.contains("\"after\""),
331 "both marks survive the rotation: {}", text);
332 let now = res!(tree_of(&root));
333 assert_eq!(now.get("big.dat").map(|v| v.len()), Some(big.len()));
334 assert_eq!(now.get("big.dat").map(|v| v.as_slice()), Some(big.as_slice()));
335 Ok(())
336}
337
338/// A mark nobody made is an error that lists the marks that were made.
339#[test]
340fn an_unknown_mark_lists_the_known_ones() -> Outcome<()> {
341 let scratch = res!(Scratch::new("nomark"));
342 let root = res!(scratch.sub("work"));
343 res!(res!(ore(&root, &["init"])).good("init"));
344 res!(write(&root, "a.txt", FIRST_A));
345 res!(res!(ore(&root, &["mark", "first"])).good("mark first"));
346 let out = res!(ore(&root, &["back", "second"]));
347 assert!(!out.ok, "going back to a mark nobody made fails");
348 assert!(out.err.contains("\"first\""), "and lists what there is: {}", out.err);
349 Ok(())
350}
351