oxedyne/ore/cli/tests/imports.rs
9.9 KiB, 1 run
created by r2848102244:742, 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 `ore import` does about a credential in the git history it is reading. |
| 2 | //! |
| 3 | //! Every key here is spelled in two pieces and joined at run time, so that the scanners which read |
| 4 | //! this very file -- the git pre-commit hook, and the guard under test -- find nothing in it. The |
| 5 | //! same trick, and for the same reason, as `guard.rs` beside it. |
| 6 | //! |
| 7 | //! The two answers are different because the two situations are: a credential still at a branch |
| 8 | //! tip is live and the import is refused before anything is written, and a credential only the |
| 9 | //! history carries is already wherever it got to and is reported instead. So each test asserts |
| 10 | //! what the store holds afterwards as well as what was said, since a refusal that still wrote and |
| 11 | //! a report that refused would both pass a test that only read the message. |
| 12 | |
| 13 | mod support; |
| 14 | |
| 15 | use support::{ |
| 16 | git, |
| 17 | git_init, |
| 18 | ore, |
| 19 | write, |
| 20 | Scratch, |
| 21 | }; |
| 22 | |
| 23 | use oxedyne_fe2o3_core::prelude::*; |
| 24 | |
| 25 | use std::fs; |
| 26 | use std::path::Path; |
| 27 | |
| 28 | |
| 29 | // A key of a shape that is a credential and nothing else, in two pieces. |
| 30 | const KEY: (&str, &str) = ("fw", "_3ZjKq81mAbCdEfGhIjKlMnOpQrSt"); |
| 31 | |
| 32 | /// The whole of a key, as it stands in a file. |
| 33 | fn key(pieces: (&str, &str)) -> String { |
| 34 | fmt!("{}{}", pieces.0, pieces.1) |
| 35 | } |
| 36 | |
| 37 | /// A line of source assigning one. |
| 38 | fn line(pieces: (&str, &str)) -> String { |
| 39 | fmt!("let key = \"{}\";\n", key(pieces)) |
| 40 | } |
| 41 | |
| 42 | /// Reads a directory, recursing, looking for the bytes. |
| 43 | /// |
| 44 | /// Asked of the bytes on disk rather than through a verb, because there is no verb that does not |
| 45 | /// capture: asking one would write. |
| 46 | fn found(dir: &Path, needle: &[u8]) |
| 47 | -> Outcome<bool> |
| 48 | { |
| 49 | for entry in res!(fs::read_dir(dir)) { |
| 50 | let entry = res!(entry); |
| 51 | let kind = res!(entry.file_type()); |
| 52 | if kind.is_dir() { |
| 53 | if res!(found(&entry.path(), needle)) { |
| 54 | return Ok(true); |
| 55 | } |
| 56 | continue; |
| 57 | } |
| 58 | if !kind.is_file() { |
| 59 | continue; |
| 60 | } |
| 61 | let bytes = res!(fs::read(entry.path())); |
| 62 | if bytes.windows(needle.len()).any(|w| w == needle) { |
| 63 | return Ok(true); |
| 64 | } |
| 65 | } |
| 66 | Ok(false) |
| 67 | } |
| 68 | |
| 69 | /// How many bytes of operations the log holds. |
| 70 | /// |
| 71 | /// Zero is what a refused import must leave, and it is asked of the segments themselves rather |
| 72 | /// than of a verb's word for it. |
| 73 | fn logged(root: &Path) |
| 74 | -> Outcome<u64> |
| 75 | { |
| 76 | let log = root.join(".ore").join("log"); |
| 77 | if !log.is_dir() { |
| 78 | return Ok(0); |
| 79 | } |
| 80 | let mut total = 0; |
| 81 | for entry in res!(fs::read_dir(&log)) { |
| 82 | let entry = res!(entry); |
| 83 | if res!(entry.file_type()).is_file() { |
| 84 | total += res!(entry.metadata()).len(); |
| 85 | } |
| 86 | } |
| 87 | Ok(total) |
| 88 | } |
| 89 | |
| 90 | /// Commits a file's contents under a message, in a repository already started. |
| 91 | fn commit(src: &Path, name: &str, holds: &str, said: &str) |
| 92 | -> Outcome<()> |
| 93 | { |
| 94 | res!(write(src, name, holds.as_bytes())); |
| 95 | res!(git(src, &["add", "."])); |
| 96 | res!(git(src, &["commit", "--quiet", "--no-verify", "-m", said])); |
| 97 | Ok(()) |
| 98 | } |
| 99 | |
| 100 | |
| 101 | /// A key added and then scrubbed is reported by mark, and the import goes through. |
| 102 | /// |
| 103 | /// It is in the history whatever anybody does now, and refusing would only mean a repository that |
| 104 | /// cannot be imported at all while protecting nothing. What did not exist before is being told, |
| 105 | /// at the moment of the import, which commit put it there. |
| 106 | #[test] |
| 107 | fn a_credential_scrubbed_before_the_tip_is_reported_and_imported() -> Outcome<()> { |
| 108 | let scratch = res!(Scratch::new("import_reported")); |
| 109 | let src = res!(scratch.sub("src")); |
| 110 | let into = res!(scratch.sub("ore")); |
| 111 | res!(git_init(&src)); |
| 112 | res!(commit(&src, "example.rs", "fn main() {}\n", "the example")); |
| 113 | res!(commit(&src, "example.rs", |
| 114 | &fmt!("fn main() {{}}\n{}", line(KEY)), "wire the example up")); |
| 115 | res!(commit(&src, "example.rs", "fn main() {}\n", "scrub the key")); |
| 116 | |
| 117 | res!(res!(ore(&into, &["init"])).good("init")); |
| 118 | let ran = res!(ore(&into, &["import", &fmt!("{}", src.display())])); |
| 119 | assert!(ran.ok, "the import went through: {}{}", ran.out, ran.err); |
| 120 | assert!(ran.err.contains("example.rs"), |
| 121 | "stderr names the file: {}", ran.err); |
| 122 | assert!(ran.err.contains("line 2"), |
| 123 | "and the line: {}", ran.err); |
| 124 | assert!(ran.err.contains("Fireworks key"), |
| 125 | "and what shape it is: {}", ran.err); |
| 126 | assert!(ran.err.contains("wire the example up"), |
| 127 | "and the mark that carries it: {}", ran.err); |
| 128 | assert!(!ran.err.contains(&key(KEY)), |
| 129 | "and never the value itself: {}", ran.err); |
| 130 | // Not stderr alone. Somebody who sent stdout to a file and never read stderr still has this. |
| 131 | assert!(ran.out.contains("credential finding"), |
| 132 | "stdout carries the count: {}", ran.out); |
| 133 | assert!(!ran.out.contains(&key(KEY)), |
| 134 | "and it does not carry the value either: {}", ran.out); |
| 135 | // The tip is clean, which is why this was a report and not a refusal. |
| 136 | let tip = res!(fs::read(into.join("example.rs"))); |
| 137 | assert!(!tip.windows(key(KEY).len()).any(|w| w == key(KEY).as_bytes()), |
| 138 | "the working copy the import wrote holds no key"); |
| 139 | // And the history does hold it, which is the thing being reported rather than prevented. A |
| 140 | // test that skipped this would pass against an import that silently dropped the commit. |
| 141 | assert!(res!(found(&into.join(".ore"), key(KEY).as_bytes())), |
| 142 | "the history carries what the report says it carries"); |
| 143 | Ok(()) |
| 144 | } |
| 145 | |
| 146 | /// The same history without the scrub is refused, and nothing at all is written. |
| 147 | #[test] |
| 148 | fn a_credential_at_the_tip_refuses_the_import() -> Outcome<()> { |
| 149 | let scratch = res!(Scratch::new("import_refused")); |
| 150 | let src = res!(scratch.sub("src")); |
| 151 | let into = res!(scratch.sub("ore")); |
| 152 | res!(git_init(&src)); |
| 153 | res!(commit(&src, "example.rs", "fn main() {}\n", "the example")); |
| 154 | res!(commit(&src, "example.rs", |
| 155 | &fmt!("fn main() {{}}\n{}", line(KEY)), "wire the example up")); |
| 156 | |
| 157 | res!(res!(ore(&into, &["init"])).good("init")); |
| 158 | let before = res!(logged(&into)); |
| 159 | let ran = res!(ore(&into, &["import", &fmt!("{}", src.display())])); |
| 160 | assert!(!ran.ok, "the import was refused: {}{}", ran.out, ran.err); |
| 161 | assert!(ran.err.contains("example.rs:2"), |
| 162 | "and it names the file and the line: {}", ran.err); |
| 163 | assert!(ran.err.contains("Fireworks key"), |
| 164 | "and what it found: {}", ran.err); |
| 165 | assert!(!ran.err.contains(&key(KEY)), |
| 166 | "and never the value itself: {}", ran.err); |
| 167 | assert_eq!(res!(logged(&into)), before, |
| 168 | "not one byte of operations was written"); |
| 169 | assert!(!res!(found(&into.join(".ore"), key(KEY).as_bytes())), |
| 170 | "and nothing of the key reached the store"); |
| 171 | assert!(!into.join("example.rs").exists(), |
| 172 | "and no working copy was laid down"); |
| 173 | Ok(()) |
| 174 | } |
| 175 | |
| 176 | /// A long answer is grouped rather than listed, and the totals are still exact. |
| 177 | /// |
| 178 | /// Forty commits each carrying the same line of the same file is one thing to fix and forty |
| 179 | /// findings. Listed a line each it is a wall somebody scrolls past, which is the same as not |
| 180 | /// having been told. |
| 181 | #[test] |
| 182 | fn a_long_report_is_grouped_and_still_counts_right() -> Outcome<()> { |
| 183 | let scratch = res!(Scratch::new("import_long")); |
| 184 | let src = res!(scratch.sub("src")); |
| 185 | let into = res!(scratch.sub("ore")); |
| 186 | res!(git_init(&src)); |
| 187 | res!(commit(&src, "example.rs", "fn main() {}\n", "the example")); |
| 188 | for n in 0..40 { |
| 189 | res!(commit(&src, "example.rs", |
| 190 | &fmt!("fn main() {{}}\n{}// edit {}\n", line(KEY), n), |
| 191 | &fmt!("edit number {}", n))); |
| 192 | } |
| 193 | res!(commit(&src, "example.rs", "fn main() {}\n", "scrub the key")); |
| 194 | |
| 195 | res!(res!(ore(&into, &["init"])).good("init")); |
| 196 | let ran = res!(ore(&into, &["import", &fmt!("{}", src.display())])); |
| 197 | assert!(ran.ok, "the import went through: {}{}", ran.out, ran.err); |
| 198 | assert!(ran.err.contains("40 findings over 1 file and 40 marks"), |
| 199 | "the totals are exact: {}", ran.err); |
| 200 | assert!(ran.err.contains("in 40 marks, from \"edit number 0\" to \"edit number 39\""), |
| 201 | "and the span is one line naming both ends: {}", ran.err); |
| 202 | // Grouped, not listed: forty findings must not be forty lines. |
| 203 | let said = ran.err.lines().filter(|l| l.contains("Fireworks key")).count(); |
| 204 | assert_eq!(said, 1, "one line for the forty: {}", ran.err); |
| 205 | assert!(ran.err.lines().count() < 10, |
| 206 | "and the whole report is short enough to read: {}", ran.err); |
| 207 | assert!(!ran.err.contains(&key(KEY)), |
| 208 | "and never the value itself: {}", ran.err); |
| 209 | Ok(()) |
| 210 | } |
| 211 | |
| 212 | /// A tip big enough to fill both pipes is still scanned. |
| 213 | /// |
| 214 | /// The scan hands git a list of object names down one pipe and reads the answers back up another. |
| 215 | /// Written whole and then read, that deadlocks -- but only when both are full at once, which is |
| 216 | /// why it took the real repository to find. git drains the list while it can; its answers fill the |
| 217 | /// 64 kB output pipe and it stops reading; and only then does an unwritten remainder larger than |
| 218 | /// the 64 kB input pipe leave both ends waiting on each other. |
| 219 | /// |
| 220 | /// So the fixture needs **many objects and large ones**: 1,800 names is 74 kB of list, over the |
| 221 | /// input pipe, and a kilobyte apiece fills the output pipe after the first sixty-odd. Eighteen |
| 222 | /// hundred tiny files does not do it -- git reads the whole list before its answers fill anything |
| 223 | /// -- and neither does four hundred large ones, whose list fits the buffer. fe2o3 has 1,785 files |
| 224 | /// averaging 17 kB and hung for ever. |
| 225 | #[test] |
| 226 | fn a_tip_big_enough_to_fill_both_pipes_is_scanned() -> Outcome<()> { |
| 227 | let scratch = res!(Scratch::new("import_wide")); |
| 228 | let src = res!(scratch.sub("src")); |
| 229 | let into = res!(scratch.sub("ore")); |
| 230 | res!(git_init(&src)); |
| 231 | let filler = "x".repeat(1000); |
| 232 | for n in 0..1800 { |
| 233 | res!(write(&src, &fmt!("f{:04}.rs", n), |
| 234 | fmt!("// {} {}\n{}", n, filler, if n == 1250 { line(KEY) } else { fmt!("") }) |
| 235 | .as_bytes())); |
| 236 | } |
| 237 | res!(git(&src, &["add", "."])); |
| 238 | res!(git(&src, &["commit", "--quiet", "--no-verify", "-m", "the wide commit"])); |
| 239 | |
| 240 | res!(res!(ore(&into, &["init"])).good("init")); |
| 241 | let before = res!(logged(&into)); |
| 242 | let ran = res!(ore(&into, &["import", &fmt!("{}", src.display())])); |
| 243 | assert!(!ran.ok, "the import was refused: {}{}", ran.out, ran.err); |
| 244 | assert!(ran.err.contains("f1250.rs:2"), |
| 245 | "and it found the one file in eighteen hundred that holds it: {}", ran.err); |
| 246 | assert!(!ran.err.contains(&key(KEY)), |
| 247 | "and never the value itself: {}", ran.err); |
| 248 | assert_eq!(res!(logged(&into)), before, "not one byte of operations was written"); |
| 249 | assert!(!res!(found(&into.join(".ore"), key(KEY).as_bytes())), |
| 250 | "and nothing of the key reached the store"); |
| 251 | Ok(()) |
| 252 | } |