Oregami
Repositories/oxedyne/ore

oxedyne/ore/cli/tests/undo.rs

11.0 KiB, 12 runs

created by r2848102244:43, 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//! `ore undo`, driven as a person drives it.
2//!
3//! Undo here is a new edit that restores an earlier state, so what these tests
4//! insist on is that the bytes come back exactly, that the history grew rather
5//! than shrank while they did, and that running the verb a second time puts
6//! back what the first one took away -- which is the whole of the redo story.
7
8mod support;
9
10use support::{
11 git,
12 git_init,
13 marks,
14 operations,
15 ore,
16 tree_of,
17 write,
18 Scratch,
19};
20
21use oxedyne_fe2o3_core::prelude::*;
22
23
24/// The bytes the first mark holds.
25const FIRST: &[u8] = b"alpha\nbeta\ngamma\n";
26/// The bytes the second mark holds.
27const SECOND: &[u8] = b"alpha\nBETA-changed\ngamma\n";
28
29
30/// Returns the bytes of one file of the working copy.
31fn bytes_of(root: &std::path::Path, path: &str)
32 -> Outcome<Vec<u8>>
33{
34 let now = res!(tree_of(root));
35 match now.get(path) {
36 Some(b) => Ok(b.clone()),
37 None => Err(err!("The working copy has no {:?}.", path; Test, Missing)),
38 }
39}
40
41
42/// Undo puts back the state from before the last command, and undoing again
43/// puts back what that took away.
44#[test]
45fn undo_restores_and_undoing_again_redoes() -> Outcome<()> {
46 let scratch = res!(Scratch::new("undo"));
47 let root = res!(scratch.sub("work"));
48 res!(res!(ore(&root, &["init"])).good("init"));
49 res!(write(&root, "a.txt", FIRST));
50 res!(write(&root, "b.txt", b"untouched\n"));
51 res!(res!(ore(&root, &["mark", "A"])).good("mark A"));
52 res!(write(&root, "a.txt", SECOND));
53 res!(res!(ore(&root, &["mark", "B"])).good("mark B"));
54
55 let out = res!(ore(&root, &["log"]));
56 let before = res!(operations(&fmt!("{}", res!(out.good("log")))));
57
58 // The first undo goes back to the state before `ore mark B`, which is A's.
59 let out = res!(ore(&root, &["undo"]));
60 let text = fmt!("{}", res!(out.good("undo")));
61 assert!(text.contains("the state from before `ore mark B`"),
62 "undo says what it put back: {}", text);
63 assert_eq!(res!(bytes_of(&root, "a.txt")), FIRST.to_vec(),
64 "and the bytes are exactly what they were");
65 assert_eq!(res!(bytes_of(&root, "b.txt")), b"untouched\n".to_vec(),
66 "and the file nobody touched is untouched");
67
68 // The second undo goes back to the state before the first undo, which is B's.
69 let out = res!(ore(&root, &["undo"]));
70 let text = fmt!("{}", res!(out.good("undo")));
71 assert!(text.contains("the state from before `ore undo`"),
72 "the second undo undoes the first: {}", text);
73 assert_eq!(res!(bytes_of(&root, "a.txt")), SECOND.to_vec(),
74 "undoing twice is redoing");
75
76 // And the history only grew: the operations that made both states are still
77 // there, and so are the ones that put them back.
78 // Every mark, because what is being counted is what each undo left behind and
79 // the plain listing shows only what a person named.
80 let out = res!(ore(&root, &["log", "--auto"]));
81 let text = fmt!("{}", res!(out.good("log --auto")));
82 let after = res!(operations(&text));
83 assert_eq!(after, before + 4,
84 "each undo is one splice, and each command that appends anything ends by \
85 naming the point it reached, so two undos are four operations: {}", text);
86 let (named, automatic) = marks(&text);
87 assert_eq!(named, 2, "the marks a person made are still the marks: {}", text);
88 assert_eq!(automatic, 2, "and each undo named the point it left: {}", text);
89 assert!(!text.contains("operations since the last mark"),
90 "the history ends at a mark, so no work is left standing unnamed: {}", text);
91 assert!(text.contains("\"A\"") && text.contains("\"B\""),
92 "with both marks still named: {}", text);
93
94 // Nothing about any of it is concurrent, so nothing is flagged.
95 let out = res!(ore(&root, &["flags"]));
96 let text = fmt!("{}", res!(out.good("flags")));
97 assert!(text.contains("clean"), "an undo is an ordinary edit: {}", text);
98 Ok(())
99}
100
101/// Undo with a mark restores that mark's state, and records the arrival, which
102/// is what distinguishes it from `back`.
103#[test]
104fn undo_to_a_mark_records_the_arrival() -> Outcome<()> {
105 let scratch = res!(Scratch::new("undo_mark"));
106 let root = res!(scratch.sub("work"));
107 res!(res!(ore(&root, &["init"])).good("init"));
108 res!(write(&root, "a.txt", FIRST));
109 res!(res!(ore(&root, &["mark", "A"])).good("mark A"));
110 res!(write(&root, "a.txt", SECOND));
111 res!(res!(ore(&root, &["mark", "B"])).good("mark B"));
112 res!(write(&root, "a.txt", b"alpha\nBETA-again\ngamma\n"));
113 res!(res!(ore(&root, &["mark", "C"])).good("mark C"));
114
115 let out = res!(ore(&root, &["undo", "A"]));
116 let text = fmt!("{}", res!(out.good("undo A")));
117 assert!(text.contains("the state at mark \"A\""), "undo names the mark: {}", text);
118 assert_eq!(res!(bytes_of(&root, "a.txt")), FIRST.to_vec(),
119 "and puts back what it held");
120
121 // `back` would have left the arrival for the next verb to notice. `undo`
122 // recorded it, so the next verb has nothing to capture.
123 let out = res!(ore(&root, &["log"]));
124 let text = fmt!("{}", res!(out.good("log")));
125 assert!(text.contains("captured nothing"),
126 "undo committed the visit rather than leaving it: {}", text);
127 Ok(())
128}
129
130/// Undo with changes nobody has recorded yet undoes those changes.
131#[test]
132fn undo_puts_back_what_was_not_yet_recorded() -> Outcome<()> {
133 let scratch = res!(Scratch::new("undo_dirty"));
134 let root = res!(scratch.sub("work"));
135 res!(res!(ore(&root, &["init"])).good("init"));
136 res!(write(&root, "a.txt", FIRST));
137 res!(res!(ore(&root, &["mark", "A"])).good("mark A"));
138
139 // Edited, and no command has run since.
140 res!(write(&root, "a.txt", SECOND));
141 let out = res!(ore(&root, &["undo"]));
142 let text = fmt!("{}", res!(out.good("undo")));
143 assert!(text.contains("the state from before the 1 operation just captured"),
144 "the changes just captured are what there is to undo: {}", text);
145 assert_eq!(res!(bytes_of(&root, "a.txt")), FIRST.to_vec(),
146 "and the edit is undone");
147
148 // The edit is still in the history, undone rather than removed.
149 let out = res!(ore(&root, &["who", "a.txt"]));
150 let text = fmt!("{}", res!(out.good("who")));
151 assert!(!text.contains("BETA-changed"),
152 "the file does not hold the undone edit: {}", text);
153 let out = res!(ore(&root, &["log"]));
154 let text = fmt!("{}", res!(out.good("log")));
155 assert!(res!(operations(&text)) >= 5,
156 "but the history holds it, and the undo of it: {}", text);
157 Ok(())
158}
159
160/// A file created and then undone is gone from the working copy, and a file
161/// deleted and then undone comes back.
162#[test]
163fn undo_reaches_the_lifecycle_as_well_as_the_bytes() -> Outcome<()> {
164 let scratch = res!(Scratch::new("undo_files"));
165 let root = res!(scratch.sub("work"));
166 res!(res!(ore(&root, &["init"])).good("init"));
167 res!(write(&root, "keep.txt", b"kept\n"));
168 res!(write(&root, "gone.txt", b"going\n"));
169 res!(res!(ore(&root, &["mark", "both"])).good("mark both"));
170
171 // A file arrives and another leaves, in one command.
172 res!(write(&root, "new.txt", b"arrived\n"));
173 res!(std::fs::remove_file(root.join("gone.txt")));
174 res!(res!(ore(&root, &["mark", "changed"])).good("mark changed"));
175
176 res!(res!(ore(&root, &["undo"])).good("undo"));
177 let now = res!(tree_of(&root));
178 assert!(!now.contains_key("new.txt"), "the created file is gone again: {:?}",
179 now.keys().collect::<Vec<&String>>());
180 assert_eq!(now.get("gone.txt").map(|v| v.as_slice()), Some(&b"going\n"[..]),
181 "and the deleted file is back");
182 assert_eq!(now.get("keep.txt").map(|v| v.as_slice()), Some(&b"kept\n"[..]));
183
184 // The bytes came back; the identity did not. There is no operation that
185 // revives a deleted file, so what the undo recorded is a create and a splice,
186 // and `ore who` dates the restored file from the undo.
187 let out = res!(ore(&root, &["who", "gone.txt"]));
188 let text = fmt!("{}", res!(out.good("who gone.txt")));
189 assert!(text.contains("going"), "the restored file holds what it held: {}", text);
190
191 // And undoing again puts both back the other way.
192 res!(res!(ore(&root, &["undo"])).good("undo again"));
193 let now = res!(tree_of(&root));
194 assert_eq!(now.get("new.txt").map(|v| v.as_slice()), Some(&b"arrived\n"[..]),
195 "the redo brings the created file back");
196 assert!(!now.contains_key("gone.txt"), "and takes the deleted one away again");
197 Ok(())
198}
199
200/// Undo with nothing to undo says so, and does nothing.
201#[test]
202fn undo_with_nothing_to_undo_says_so() -> Outcome<()> {
203 let scratch = res!(Scratch::new("undo_empty"));
204 let root = res!(scratch.sub("work"));
205 res!(res!(ore(&root, &["init"])).good("init"));
206 let out = res!(ore(&root, &["undo"]));
207 let text = fmt!("{}", res!(out.good("undo")));
208 assert!(text.contains("nothing to undo"), "a fresh repository has none: {}", text);
209
210 // A mark nobody made is an error that lists the marks that were made.
211 res!(write(&root, "a.txt", FIRST));
212 res!(res!(ore(&root, &["mark", "A"])).good("mark A"));
213 let out = res!(ore(&root, &["undo", "nowhere"]));
214 assert!(!out.ok, "undoing to a mark nobody made fails");
215 assert!(out.err.contains("\"A\""), "and lists what there is: {}", out.err);
216 Ok(())
217}
218
219/// Undo works on an imported history, where every operation was written by
220/// somebody else.
221#[test]
222fn undo_works_after_an_import() -> Outcome<()> {
223 let scratch = res!(Scratch::new("undo_import"));
224 let src = res!(scratch.sub("src"));
225 let into = res!(scratch.sub("ore"));
226
227 res!(git_init(&src));
228 res!(write(&src, "f.txt", b"alpha\nbeta\ngamma\n"));
229 res!(git(&src, &["add", "."]));
230 res!(git(&src, &["commit", "--quiet", "--no-verify", "-m", "base"]));
231 res!(write(&src, "f.txt", b"alpha\nbeta\ngamma\ndelta\n"));
232 res!(git(&src, &["add", "."]));
233 res!(git(&src, &["commit", "--quiet", "--no-verify", "-m", "one more line"]));
234
235 res!(res!(ore(&into, &["init"])).good("init"));
236 res!(res!(ore(&into, &["import", &fmt!("{}", src.display())])).good("import"));
237 let imported = res!(bytes_of(&into, "f.txt"));
238
239 // An edit of this replica's own, on top of somebody else's history.
240 res!(write(&into, "f.txt", b"alpha\nbeta\ngamma\ndelta\nepsilon\n"));
241 res!(res!(ore(&into, &["mark", "mine"])).good("mark mine"));
242
243 res!(res!(ore(&into, &["undo"])).good("undo"));
244 assert_eq!(res!(bytes_of(&into, "f.txt")), imported,
245 "the imported state comes back exactly");
246
247 res!(res!(ore(&into, &["undo"])).good("undo again"));
248 assert_eq!(res!(bytes_of(&into, "f.txt")), b"alpha\nbeta\ngamma\ndelta\nepsilon\n".to_vec(),
249 "and the redo puts this replica's edit back");
250 Ok(())
251}
252
253/// An undo of a small edit to a large file costs a small edit.
254#[test]
255fn undo_costs_what_the_edit_cost() -> Outcome<()> {
256 let scratch = res!(Scratch::new("undo_cost"));
257 let root = res!(scratch.sub("work"));
258 res!(res!(ore(&root, &["init"])).good("init"));
259
260 let mut base = Vec::new();
261 for i in 0..1000 {
262 base.extend_from_slice(fmt!("line {:04} of a file that is quite long\n", i).as_bytes());
263 }
264 res!(write(&root, "big.txt", &base));
265 res!(res!(ore(&root, &["mark", "base"])).good("mark base"));
266
267 let mut more = base.clone();
268 more.extend_from_slice(b"and one line more\n");
269 res!(write(&root, "big.txt", &more));
270 res!(res!(ore(&root, &["mark", "appended"])).good("mark appended"));
271
272 let out = res!(ore(&root, &["undo"]));
273 let text = fmt!("{}", res!(out.good("undo")));
274 assert!(text.contains("1 operation, 0 bytes inserted"),
275 "undoing an append removes bytes and inserts none: {}", text);
276 assert_eq!(res!(bytes_of(&root, "big.txt")), base,
277 "and the file is what it was");
278 Ok(())
279}