Oregami
Repositories/oxedyne/ore

oxedyne/ore/cli/tests/notes.rs

9.4 KiB, 1 run

created by r2848102244:132, 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//! Anchored notes, written from the tool.
2//!
3//! A note names content, not a line, and every test here is about the difference
4//! between those two things. Writing one is the easy half; what has to be proved
5//! is that the note is still about the same bytes after the file around it has
6//! been rewritten, after those bytes have moved to another file, and that it says
7//! so plainly when the bytes are gone.
8//!
9//! # Coordinates are asserted by construction
10//!
11//! Every file here is written line by line, so the line a note should end up on is
12//! known before the tool runs rather than read back off what the tool said. A test
13//! that copied the output would pass over a coordinate that was off by one in
14//! every case.
15
16mod support;
17
18use support::{
19 ore,
20 write,
21 Ran,
22 Scratch,
23};
24
25use oxedyne_fe2o3_core::prelude::*;
26
27use std::path::Path;
28
29
30/// Eight lines, so that a note on the fourth has lines either side of it to be
31/// disturbed by.
32const EIGHT: &[u8] =
33 b"one\ntwo\nthree\nfour\nfive\nsix\nseven\neight\n";
34
35
36/// Runs a verb and fails the test if it did not succeed.
37fn ran(dir: &Path, args: &[&str])
38 -> Outcome<String>
39{
40 let out: Ran = res!(ore(dir, args));
41 Ok(fmt!("{}", res!(out.good(&args.join(" ")))))
42}
43
44/// Makes a repository holding one file, and returns where it is.
45fn started(scratch: &Scratch, name: &str, file: &str, text: &[u8])
46 -> Outcome<std::path::PathBuf>
47{
48 let dir = res!(scratch.sub(name));
49 res!(ran(&dir, &["init"]));
50 res!(write(&dir, file, text));
51 res!(ran(&dir, &["mark", "start"]));
52 Ok(dir)
53}
54
55
56/// A note lands on the line it names, and `ore who` shows it there.
57#[test]
58fn a_note_lands_where_it_is_put()
59 -> Outcome<()>
60{
61 let scratch = res!(Scratch::new("note_lands"));
62 let dir = res!(started(&scratch, "r", "a.txt", EIGHT));
63 let said = res!(ran(&dir, &["note", "a.txt:4", "the fourth line"]));
64 // Line four is "four\n": five bytes, its newline included.
65 assert!(said.contains("about 5 bytes at a.txt:4"),
66 "the note did not say where it went: {}", said);
67 let seen = res!(ran(&dir, &["who", "a.txt"]));
68 assert!(seen.contains("a.txt:4"),
69 "who did not place the note on line four: {}", seen);
70 assert!(seen.contains("the fourth line"),
71 "who did not say what the note says: {}", seen);
72 Ok(())
73}
74
75/// A note on a range of lines covers all of them and no more.
76#[test]
77fn a_note_on_a_range_covers_it()
78 -> Outcome<()>
79{
80 let scratch = res!(Scratch::new("note_range"));
81 let dir = res!(started(&scratch, "r", "a.txt", EIGHT));
82 let said = res!(ran(&dir, &["note", "a.txt:2-4", "the middle"]));
83 // "two\nthree\nfour\n" is 4 + 6 + 5 bytes.
84 assert!(said.contains("about 15 bytes at a.txt:2-4"),
85 "the range was not measured as written: {}", said);
86 Ok(())
87}
88
89/// **The flagship**: a note follows its content into another file.
90///
91/// The content is moved by rewriting both files, which is what a person editing
92/// does; capture's move detection turns that into an `Op::Move`, and the note --
93/// which named the bytes and never the file -- resolves into wherever those bytes
94/// now are.
95#[test]
96fn a_note_follows_its_content_to_another_file()
97 -> Outcome<()>
98{
99 let scratch = res!(Scratch::new("note_moves"));
100 let dir = res!(scratch.sub("r"));
101 res!(ran(&dir, &["init"]));
102 // A block long enough for capture to pair it as a move rather than as an
103 // unrelated deletion and insertion.
104 let block = b"fn placement(&self) -> Placement {\n\
105 \tlet mut found = Vec::new();\n\
106 \tfor file in self.files.iter().rev() {\n\
107 \t\tfound.push(file.spans());\n\
108 \t}\n\
109 \tPlacement::new(found)\n\
110 }\n";
111 let mut from: Vec<u8> = b"// the top of the first file\n\n".to_vec();
112 from.extend_from_slice(block);
113 from.extend_from_slice(b"\n// the bottom of the first file\n");
114 res!(write(&dir, "a.txt", &from));
115 res!(write(&dir, "b.txt", b"// the second file\n"));
116 res!(ran(&dir, &["mark", "start"]));
117
118 // The block starts on line 3 and is seven lines long.
119 let said = res!(ran(&dir, &["note", "a.txt:3-9", "this reads the render backwards"]));
120 assert!(said.contains("at a.txt:3-9"), "the note did not land: {}", said);
121
122 // Now move the block: out of one file, into the other.
123 res!(write(&dir, "a.txt",
124 b"// the top of the first file\n\n\n// the bottom of the first file\n"));
125 let mut to: Vec<u8> = b"// the second file\n".to_vec();
126 to.extend_from_slice(block);
127 res!(write(&dir, "b.txt", &to));
128
129 // The note is now about content in the other file, and nothing was told to
130 // update it.
131 let listed = res!(ran(&dir, &["note"]));
132 assert!(listed.contains("b.txt:"),
133 "the note did not follow its content into b.txt: {}", listed);
134 assert!(!listed.contains("every byte it was about has been deleted"),
135 "the note was reported dead when its content still renders: {}", listed);
136 assert!(listed.contains("this reads the render backwards"),
137 "the note lost its text: {}", listed);
138 Ok(())
139}
140
141/// A note narrows to what survived an edit, rather than covering what replaced
142/// it.
143#[test]
144fn a_note_narrows_under_an_edit()
145 -> Outcome<()>
146{
147 let scratch = res!(Scratch::new("note_narrows"));
148 let dir = res!(started(&scratch, "r", "a.txt", EIGHT));
149 res!(ran(&dir, &["note", "a.txt:3-5", "three to five"]));
150 // "three\n" + "four\n" + "five\n" is 6 + 5 + 5 bytes.
151 let before = res!(ran(&dir, &["note"]));
152 assert!(before.contains("a.txt:3-5, 16 bytes"),
153 "the note did not start out about sixteen bytes: {}", before);
154 // Replace the middle of the noted region, leaving its first and last lines.
155 res!(write(&dir, "a.txt",
156 b"one\ntwo\nthree\nFOUR IS DIFFERENT NOW\nfive\nsix\nseven\neight\n"));
157 let seen = res!(ran(&dir, &["who", "a.txt"]));
158 assert!(seen.contains("three to five"), "the note is gone: {}", seen);
159 // It narrowed to what survived, and did not spread over the longer line that
160 // replaced the one between. The coordinate alone cannot show this: it still
161 // reads 3-5 either way.
162 //
163 // Twelve and not eleven, because capture's diff is surgical: both versions of
164 // the fourth line end in a newline, so only the four bytes of "four" were
165 // deleted and the newline that ended the line survived the edit. What the note
166 // still covers is "three\n", that newline, and "five\n".
167 let after = res!(ran(&dir, &["note"]));
168 assert!(after.contains("a.txt:3-5, 12 bytes"),
169 "the note did not narrow to the twelve bytes that survived: {}", after);
170 Ok(())
171}
172
173/// A note whose content is deleted says so, and is still readable.
174#[test]
175fn a_note_on_deleted_content_says_so()
176 -> Outcome<()>
177{
178 let scratch = res!(Scratch::new("note_dead"));
179 let dir = res!(started(&scratch, "r", "a.txt", EIGHT));
180 res!(ran(&dir, &["note", "a.txt:4", "the fourth line"]));
181 // Delete exactly the noted line.
182 res!(write(&dir, "a.txt", b"one\ntwo\nthree\nfive\nsix\nseven\neight\n"));
183 let listed = res!(ran(&dir, &["note"]));
184 assert!(listed.contains("every byte it was about has been deleted"),
185 "a note on dead content did not say so: {}", listed);
186 // It is not lost: what it said is still there to read.
187 assert!(listed.contains("the fourth line"),
188 "a dead note lost its text: {}", listed);
189 Ok(())
190}
191
192/// A note crosses a sync, because it is history like anything else.
193#[test]
194fn a_note_syncs()
195 -> Outcome<()>
196{
197 let scratch = res!(Scratch::new("note_syncs"));
198 let one = res!(started(&scratch, "one", "a.txt", EIGHT));
199 let two = res!(scratch.sub("two"));
200 res!(ran(&two, &["init"]));
201 res!(ran(&two, &["sync", &fmt!("{}", one.display())]));
202 res!(ran(&one, &["note", "a.txt:4", "seen from the other end"]));
203 res!(ran(&two, &["sync", &fmt!("{}", one.display())]));
204 let listed = res!(ran(&two, &["note"]));
205 assert!(listed.contains("seen from the other end"),
206 "the note did not cross the sync: {}", listed);
207 assert!(listed.contains("a.txt:4"),
208 "the note did not resolve to line four at the far end: {}", listed);
209 Ok(())
210}
211
212/// A line number in a file that is not text is refused, and the byte spelling is
213/// offered by name.
214#[test]
215fn a_binary_file_has_no_lines()
216 -> Outcome<()>
217{
218 let scratch = res!(Scratch::new("note_binary"));
219 let dir = res!(started(&scratch, "r", "a.bin", b"\x00\x01\n\x02binary\x00\xff\n"));
220 let refused = res!(ore(&dir, &["note", "a.bin:2", "the second line"]));
221 assert!(!refused.ok, "a line number in a binary file was accepted");
222 let says = fmt!("{}{}", refused.out, refused.err);
223 assert!(says.contains("a.bin:0..64") || says.contains("Name the bytes instead"),
224 "the refusal did not offer the byte spelling: {}", says);
225 // The byte spelling works on the same file.
226 let said = res!(ran(&dir, &["note", "a.bin:0..4", "the header"]));
227 assert!(said.contains("about 4 bytes"), "the byte range was not taken: {}", said);
228 Ok(())
229}
230
231/// A coordinate naming a line the file does not have is refused, and says how
232/// many it has.
233#[test]
234fn a_line_past_the_end_is_refused()
235 -> Outcome<()>
236{
237 let scratch = res!(Scratch::new("note_past_end"));
238 let dir = res!(started(&scratch, "r", "a.txt", EIGHT));
239 let refused = res!(ore(&dir, &["note", "a.txt:9", "there is no ninth line"]));
240 assert!(!refused.ok, "a line past the end of the file was accepted");
241 let says = fmt!("{}{}", refused.out, refused.err);
242 assert!(says.contains("8 lines"),
243 "the refusal did not say how many lines there are: {}", says);
244 Ok(())
245}
246
247/// With nothing to list, the verb says so and says how to write one.
248#[test]
249fn no_notes_says_so()
250 -> Outcome<()>
251{
252 let scratch = res!(Scratch::new("note_none"));
253 let dir = res!(started(&scratch, "r", "a.txt", EIGHT));
254 let listed = res!(ran(&dir, &["note"]));
255 assert!(listed.contains("no notes"), "an empty repository did not say so: {}", listed);
256 Ok(())
257}