oxedyne/fe2o3/fe2o3_sbj/tests/cli.rs
16.7 KiB, 24 runs
created by r1870400018:22230, 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 authoring toolchain: the text form, the key file, and the `sbj` binary that uses them. |
| 2 | //! |
| 3 | //! The claim the round trip makes is that a document's text form is its source and its bytes are its |
| 4 | //! artefact, and that neither loses anything to the other. It is checked against the conformance |
| 5 | //! fixtures rather than against a document invented here, so that the compiler is held to the same |
| 6 | //! artefacts `SPEC.md` §7 holds the reader to: every fixture's `doc.jdat`, compiled with the |
| 7 | //! committed key at the time its `meta.jdat` declares, must give back the committed `doc.sbj`, byte |
| 8 | //! for byte. Dumped back to text and compiled again, it must give the same bytes once more. |
| 9 | //! |
| 10 | //! The binary is then run as a shell runs it, on a document it wrote itself, since a toolchain that |
| 11 | //! only works when called as a library is not a toolchain. |
| 12 | |
| 13 | mod common; |
| 14 | |
| 15 | use common::{ |
| 16 | Keys, |
| 17 | Meta, |
| 18 | DOC_JDAT, |
| 19 | DOC_SBJ, |
| 20 | META_JDAT, |
| 21 | }; |
| 22 | |
| 23 | use oxedyne_fe2o3_sbj::{ |
| 24 | doc, |
| 25 | text::{ |
| 26 | self, |
| 27 | KindDecl, |
| 28 | }, |
| 29 | validate, |
| 30 | SCHEMA_DOC, |
| 31 | }; |
| 32 | |
| 33 | use oxedyne_fe2o3_core::prelude::*; |
| 34 | use oxedyne_fe2o3_jdat::prelude::*; |
| 35 | |
| 36 | use std::{ |
| 37 | fs, |
| 38 | path::{ |
| 39 | Path, |
| 40 | PathBuf, |
| 41 | }, |
| 42 | process::Command, |
| 43 | }; |
| 44 | |
| 45 | /// The label the fixtures give the one node kind the v0 vocabulary does not know. |
| 46 | /// |
| 47 | /// A label a document invents for an unknown kind is declared rather than guessed, which is what the |
| 48 | /// compiler's `--kind` option passes. The conventional `sbj_k99` needs no declaring, and is what a |
| 49 | /// dump writes. |
| 50 | fn alien() -> Vec<KindDecl> { |
| 51 | vec![KindDecl { |
| 52 | label: common::ALIEN_LABEL.to_string(), |
| 53 | code: common::ALIEN_CODE, |
| 54 | }] |
| 55 | } |
| 56 | |
| 57 | /// Every fixture's `doc.jdat` compiles to its `doc.sbj`, and dumps back to a document that does too. |
| 58 | #[test] |
| 59 | fn test_round_trip_every_fixture() -> Outcome<()> { |
| 60 | // The JDAT text decoder is generous with a stack, and a fixture may nest to the depth limit of |
| 61 | // SPEC.md §5. The limit is the format's and does not move to suit a test, so the test moves. |
| 62 | let thread = match std::thread::Builder::new() |
| 63 | .name("sbj_round_trip".to_string()) |
| 64 | .stack_size(common::STACK_BYTES) |
| 65 | .spawn(round_trip) |
| 66 | { |
| 67 | Ok(thread) => thread, |
| 68 | Err(e) => return Err(err!(e, |
| 69 | "Could not spawn the thread the deepest fixture is compiled on."; Test, Init)), |
| 70 | }; |
| 71 | match thread.join() { |
| 72 | Ok(outcome) => outcome, |
| 73 | Err(_) => Err(err!( |
| 74 | "The thread compiling the fixtures did not return."; Test, Panic)), |
| 75 | } |
| 76 | } |
| 77 | |
| 78 | /// Compiles, reads and dumps every acceptance fixture. |
| 79 | fn round_trip() -> Outcome<()> { |
| 80 | let root = common::fixtures_dir(); |
| 81 | let keys = res!(Keys::load(&root)); |
| 82 | let mut run = 0; |
| 83 | // Acceptance fixtures the compiler has no business with, counted so the number can be asserted. |
| 84 | let mut skipped = 0; |
| 85 | |
| 86 | for entry in res!(fs::read_dir(&root), IO, File) { |
| 87 | let entry = res!(entry, IO, File); |
| 88 | let dir = entry.path(); |
| 89 | // A rejection fixture carries no `meta.jdat`, and is the conformance suite's business. |
| 90 | if !dir.is_dir() || !dir.join(META_JDAT).is_file() { |
| 91 | continue; |
| 92 | } |
| 93 | let name = match dir.file_name().and_then(|s| s.to_str()) { |
| 94 | Some(name) => name.to_string(), |
| 95 | None => return Err(err!( |
| 96 | "The fixture directory holds {}, whose name is not UTF-8.", dir.display(); |
| 97 | Test, Invalid)), |
| 98 | }; |
| 99 | let meta = res!(Meta::from_dat(&res!(common::from_jdat_plain( |
| 100 | &res!(common::read_text(&dir.join(META_JDAT))) |
| 101 | )))); |
| 102 | let committed = res!(common::read_bytes(&dir.join(DOC_SBJ))); |
| 103 | |
| 104 | // The compiler is a DOCUMENT tool: it reads the JDAT text of a node tree and writes an |
| 105 | // oxeweb document. A post and a card travel in the same container and are not node trees, |
| 106 | // so there is nothing here for it to compile and this is not the suite that tests them. |
| 107 | // |
| 108 | // Excluded by an assertion rather than by a `continue`, because a fixture skipped in |
| 109 | // silence is a fixture that could stop being run without anybody noticing. Every fixture |
| 110 | // this loop meets is either compiled or is required to be one the compiler refuses. |
| 111 | if meta.schema != SCHEMA_DOC { |
| 112 | // The claim is not that the TEXT is unreadable: a flat record is perfectly good JDAT, |
| 113 | // and the decoder parses it happily into a map. The claim is that the compiler cannot |
| 114 | // WRITE it. `doc::write` puts every schema through the node-tree validator, so the one |
| 115 | // route this tool has to an artefact is closed to a record, and it says so. |
| 116 | let src = res!(common::read_text(&dir.join(DOC_JDAT))); |
| 117 | let parsed = res!(text::decode(&src, &alien())); |
| 118 | match compile(&parsed, &meta, &keys) { |
| 119 | Ok(_) => return Err(err!( |
| 120 | "The fixture '{}' declares the schema '{}', which is not a node tree, and the \ |
| 121 | document compiler wrote an artefact for it anyway. A record put through the \ |
| 122 | tree writer is a document nobody wrote.", name, meta.schema; |
| 123 | Test, Invalid, Unexpected)), |
| 124 | Err(_) => (), |
| 125 | } |
| 126 | res!(eq(&name, "the node count of a payload that is not a tree", &meta.nodes, &None)); |
| 127 | skipped += 1; |
| 128 | continue; |
| 129 | } |
| 130 | |
| 131 | // The text form of the document is its source: it compiles to the artefact that was |
| 132 | // committed, byte for byte, or the compiler is not the writer the fixtures were written by. |
| 133 | let src = res!(common::read_text(&dir.join(DOC_JDAT))); |
| 134 | let tree = match text::decode(&src, &alien()) { |
| 135 | Ok(tree) => tree, |
| 136 | Err(e) => return Err(err!(e, |
| 137 | "The '{}' of the fixture '{}' is not readable by the compiler.", DOC_JDAT, name; |
| 138 | Test, Invalid)), |
| 139 | }; |
| 140 | let built = res!(compile(&tree, &meta, &keys)); |
| 141 | res!(same(&name, "the compiled document", &built, &committed)); |
| 142 | |
| 143 | // And what it compiles to is what a reader reads. |
| 144 | let read = res!(doc::read(&built)); |
| 145 | res!(eq(&name, "the tree that was compiled", read.tree(), &tree)); |
| 146 | let stats = res!(validate::validate(read.tree(), &read.env().schema)); |
| 147 | res!(eq(&name, "the node count", &Some(try_into!(u64, stats.nodes)), &meta.nodes)); |
| 148 | res!(eq(&name, "the depth", &Some(try_into!(u64, stats.depth)), &meta.depth)); |
| 149 | res!(eq(&name, "the address", &read.env().hash, &meta.hash)); |
| 150 | |
| 151 | // Dumped back to text, the document is the document: the tree survives, and the text it is |
| 152 | // written as compiles to the same bytes at the same address. A dump needs no declaration of |
| 153 | // the unknown kinds it carries, since it writes them under the conventional label. |
| 154 | let dumped = res!(text::encode(read.tree())); |
| 155 | let again = match text::decode(&dumped, &[]) { |
| 156 | Ok(again) => again, |
| 157 | Err(e) => return Err(err!(e, |
| 158 | "The dump of the fixture '{}' is not readable by the compiler.", name; |
| 159 | Test, Invalid)), |
| 160 | }; |
| 161 | res!(eq(&name, "the dumped tree", &again, &tree)); |
| 162 | let rebuilt = res!(compile(&again, &meta, &keys)); |
| 163 | res!(same(&name, "the recompiled dump", &rebuilt, &committed)); |
| 164 | |
| 165 | // The dump is stable: a document dumped, compiled and dumped again is the same text. |
| 166 | let dumped_again = res!(text::encode(res!(doc::read(&rebuilt)).tree())); |
| 167 | res!(eq(&name, "the text form", &dumped_again, &dumped)); |
| 168 | |
| 169 | run += 1; |
| 170 | } |
| 171 | |
| 172 | if run == 0 { |
| 173 | return Err(err!( |
| 174 | "No acceptance fixture was found in {}, so nothing was round tripped.", |
| 175 | common::fixtures_dir().display(); |
| 176 | Test, Missing)); |
| 177 | } |
| 178 | // The four fixtures of the two record schemas. Named as a number rather than left open, so that |
| 179 | // a fixture which quietly stopped being a document -- or a document that quietly stopped being |
| 180 | // compiled -- shows up here instead of passing as an exclusion. |
| 181 | if skipped != 6 { |
| 182 | return Err(err!( |
| 183 | "{} acceptance fixtures were excluded from the compiler, and six are expected: the \ |
| 184 | two posts, the two cards and the two shares. A different number means a fixture \ |
| 185 | changed schema, or one was added without this count being thought about.", skipped; |
| 186 | Test, Invalid, Mismatch)); |
| 187 | } |
| 188 | Ok(()) |
| 189 | } |
| 190 | |
| 191 | /// Compiles a tree as its fixture declares it was written. |
| 192 | fn compile( |
| 193 | tree: &Dat, |
| 194 | meta: &Meta, |
| 195 | keys: &Keys, |
| 196 | ) |
| 197 | -> Outcome<Vec<u8>> |
| 198 | { |
| 199 | let signer = res!(keys.author.signer()); |
| 200 | if meta.index { |
| 201 | doc::write_with_index(tree, &meta.schema, &signer, meta.time) |
| 202 | } else { |
| 203 | doc::write(tree, &meta.schema, &signer, meta.time) |
| 204 | } |
| 205 | } |
| 206 | |
| 207 | /// The dump of a fixture is the source it was written from, character for character. |
| 208 | /// |
| 209 | /// This is not required of the format, which cares only that the tree survives, but it is what makes |
| 210 | /// a dump worth reading: a document a reader holds and an author's source are the same text, so the |
| 211 | /// two can be compared with a diff rather than a decoder. |
| 212 | #[test] |
| 213 | fn test_a_dump_is_the_source() -> Outcome<()> { |
| 214 | let thread = match std::thread::Builder::new() |
| 215 | .name("sbj_dump".to_string()) |
| 216 | .stack_size(common::STACK_BYTES) |
| 217 | .spawn(dump_is_source) |
| 218 | { |
| 219 | Ok(thread) => thread, |
| 220 | Err(e) => return Err(err!(e, |
| 221 | "Could not spawn the thread the fixture is dumped on."; Test, Init)), |
| 222 | }; |
| 223 | match thread.join() { |
| 224 | Ok(outcome) => outcome, |
| 225 | Err(_) => Err(err!("The thread dumping the fixture did not return."; Test, Panic)), |
| 226 | } |
| 227 | } |
| 228 | |
| 229 | /// Dumps the fixture that uses every node kind, and compares the text with the fixture's source. |
| 230 | fn dump_is_source() -> Outcome<()> { |
| 231 | let dir = common::fixtures_dir().join("every_kind"); |
| 232 | let buf = res!(common::read_bytes(&dir.join(DOC_SBJ))); |
| 233 | let src = res!(common::read_text(&dir.join(DOC_JDAT))); |
| 234 | let dumped = res!(text::encode(res!(doc::read(&buf)).tree())); |
| 235 | if dumped != src { |
| 236 | return Err(err!( |
| 237 | "The dump of 'every_kind' is not its source. The source is {} characters and the dump \ |
| 238 | is {}:\n{}", src.len(), dumped.len(), dumped; |
| 239 | Test, Invalid, Mismatch)); |
| 240 | } |
| 241 | Ok(()) |
| 242 | } |
| 243 | |
| 244 | /// The binary runs, and does to a document of its own what it does to a fixture. |
| 245 | #[test] |
| 246 | fn test_the_binary_runs() -> Outcome<()> { |
| 247 | let dir = res!(scratch("binary")); |
| 248 | let source = dir.join("doc.jdat"); |
| 249 | let keyfile = dir.join("key.jdat"); |
| 250 | let artefact = dir.join("doc.sbj"); |
| 251 | let dumped = dir.join("dumped.jdat"); |
| 252 | let rebuilt = dir.join("rebuilt.sbj"); |
| 253 | res!(write_text(&source, SOURCE)); |
| 254 | |
| 255 | // Compile. The key file is not there, so it is generated and saved, and the author is told where. |
| 256 | let out = res!(sbj(&[ |
| 257 | "compile", &path(&source), |
| 258 | "-o", &path(&artefact), |
| 259 | "--key", &path(&keyfile), |
| 260 | "--time", "1752000000000", |
| 261 | ])); |
| 262 | res!(says(&out, "Generated a signing key")); |
| 263 | res!(says(&out, SCHEMA_DOC)); |
| 264 | // The doc, its heading and that heading's text, its paragraph and that paragraph's text. |
| 265 | res!(says(&out, "nodes 5, depth 3")); |
| 266 | if !keyfile.is_file() { |
| 267 | return Err(err!( |
| 268 | "The compiler signed a document and saved no key at {}.", keyfile.display(); |
| 269 | Test, Missing)); |
| 270 | } |
| 271 | |
| 272 | // Verify, which touches no content. |
| 273 | let out = res!(sbj(&["verify", &path(&artefact)])); |
| 274 | res!(says(&out, "signature good")); |
| 275 | res!(says(&out, "not decoded")); |
| 276 | |
| 277 | // Inspect, which reads the document whole. |
| 278 | let out = res!(sbj(&["inspect", &path(&artefact)])); |
| 279 | res!(says(&out, "kinds doc 1, para 1, heading 1, text 2")); |
| 280 | res!(says(&out, "styles lede: size = 1")); |
| 281 | res!(says(&out, "index none")); |
| 282 | |
| 283 | // Dump, and compile the dump: the same key, the same time, the same document, the same bytes. |
| 284 | res!(sbj(&["dump", &path(&artefact), "-o", &path(&dumped)])); |
| 285 | res!(sbj(&[ |
| 286 | "compile", &path(&dumped), |
| 287 | "-o", &path(&rebuilt), |
| 288 | "--key", &path(&keyfile), |
| 289 | "--time", "1752000000000", |
| 290 | ])); |
| 291 | let first = res!(common::read_bytes(&artefact)); |
| 292 | let second = res!(common::read_bytes(&rebuilt)); |
| 293 | res!(same("the binary", "the recompiled dump", &second, &first)); |
| 294 | |
| 295 | res!(clean(&dir)); |
| 296 | Ok(()) |
| 297 | } |
| 298 | |
| 299 | /// A document that fails is refused, the shell hears about it, and the refusal names the rule. |
| 300 | #[test] |
| 301 | fn test_the_binary_refuses_and_says_why() -> Outcome<()> { |
| 302 | let dir = res!(scratch("refuses")); |
| 303 | let source = dir.join("doc.jdat"); |
| 304 | let keyfile = dir.join("key.jdat"); |
| 305 | let artefact = dir.join("doc.sbj"); |
| 306 | let corrupt = dir.join("corrupt.sbj"); |
| 307 | |
| 308 | // A heading of level 7 never reaches a file, so it never gets an address. |
| 309 | res!(write_text(&source, &SOURCE.replace("(u8|2)", "(u8|7)"))); |
| 310 | let (code, _, err) = res!(sbj_raw(&[ |
| 311 | "compile", &path(&source), |
| 312 | "-o", &path(&artefact), |
| 313 | "--key", &path(&keyfile), |
| 314 | ])); |
| 315 | res!(exits(code, 1, "compiling a heading of level 7")); |
| 316 | res!(says(&err, "Node 1")); |
| 317 | res!(says(&err, "1..=6")); |
| 318 | |
| 319 | // A corrupted byte of the tree region is caught by the hash, before anything decodes it. |
| 320 | res!(write_text(&source, SOURCE)); |
| 321 | res!(sbj(&[ |
| 322 | "compile", &path(&source), |
| 323 | "-o", &path(&artefact), |
| 324 | "--key", &path(&keyfile), |
| 325 | ])); |
| 326 | let mut buf = res!(common::read_bytes(&artefact)); |
| 327 | let last = buf.len() - 1; |
| 328 | buf[last] ^= 0x01; |
| 329 | res!(common::write_bytes(&corrupt, &buf)); |
| 330 | |
| 331 | let (code, _, err) = res!(sbj_raw(&["verify", &path(&corrupt)])); |
| 332 | res!(exits(code, 1, "verifying a corrupted tree")); |
| 333 | res!(says(&err, "hashes to")); |
| 334 | |
| 335 | // And a subcommand that is not one, and an option that is not one. |
| 336 | let (code, _, err) = res!(sbj_raw(&["render", &path(&artefact)])); |
| 337 | res!(exits(code, 1, "a subcommand that is not one")); |
| 338 | res!(says(&err, "is not an sbj subcommand")); |
| 339 | let (code, _, err) = res!(sbj_raw(&["verify", &path(&artefact), "--quickly"])); |
| 340 | res!(exits(code, 1, "an option that is not one")); |
| 341 | res!(says(&err, "is not an sbj option")); |
| 342 | |
| 343 | res!(clean(&dir)); |
| 344 | Ok(()) |
| 345 | } |
| 346 | |
| 347 | /// The document the binary is run on: a heading, a paragraph, and a style the paragraph names. |
| 348 | const SOURCE: &'static str = "\ |
| 349 | (sbj_doc|(map|{ |
| 350 | (str|\"children\"): (list|[ |
| 351 | (sbj_heading|(map|{ |
| 352 | (str|\"children\"): (list|[(sbj_text|(str|\"Style without a cascade\"))]), |
| 353 | (str|\"level\"): (u8|2), |
| 354 | })), |
| 355 | (sbj_para|(map|{ |
| 356 | (str|\"children\"): (list|[(sbj_text|(str|\"A lede paragraph.\"))]), |
| 357 | (str|\"style\"): (str|\"lede\"), |
| 358 | })), |
| 359 | ]), |
| 360 | (str|\"lang\"): (str|\"en\"), |
| 361 | (str|\"styles\"): (map|{ |
| 362 | (str|\"lede\"): (map|{ |
| 363 | (str|\"size\"): (i8|1), |
| 364 | }), |
| 365 | }), |
| 366 | (str|\"title\"): (str|\"A document\"), |
| 367 | })) |
| 368 | "; |
| 369 | |
| 370 | /// Runs the binary, refusing a run that fails. |
| 371 | fn sbj(args: &[&str]) -> Outcome<String> { |
| 372 | let (code, out, err) = res!(sbj_raw(args)); |
| 373 | if code != 0 { |
| 374 | return Err(err!( |
| 375 | "`sbj {}` exited {}, and says: {}", args.join(" "), code, err; |
| 376 | Test, Invalid)); |
| 377 | } |
| 378 | Ok(out) |
| 379 | } |
| 380 | |
| 381 | /// Runs the binary, returning its exit code, its output, and what it said to the shell. |
| 382 | fn sbj_raw(args: &[&str]) -> Outcome<(i32, String, String)> { |
| 383 | let out = match Command::new(env!("CARGO_BIN_EXE_sbj")).args(args).output() { |
| 384 | Ok(out) => out, |
| 385 | Err(e) => return Err(err!(e, |
| 386 | "Could not run the sbj binary at {}.", env!("CARGO_BIN_EXE_sbj"); |
| 387 | Test, IO)), |
| 388 | }; |
| 389 | let code = match out.status.code() { |
| 390 | Some(code) => code, |
| 391 | None => return Err(err!( |
| 392 | "`sbj {}` was killed by a signal rather than exiting.", args.join(" "); |
| 393 | Test, Invalid)), |
| 394 | }; |
| 395 | Ok(( |
| 396 | code, |
| 397 | String::from_utf8_lossy(&out.stdout).to_string(), |
| 398 | String::from_utf8_lossy(&out.stderr).to_string(), |
| 399 | )) |
| 400 | } |
| 401 | |
| 402 | /// Requires an exit code, naming what was run. |
| 403 | fn exits( |
| 404 | code: i32, |
| 405 | want: i32, |
| 406 | what: &str, |
| 407 | ) |
| 408 | -> Outcome<()> |
| 409 | { |
| 410 | if code == want { |
| 411 | Ok(()) |
| 412 | } else { |
| 413 | Err(err!( |
| 414 | "{} exited {}, where {} was required. A document that fails is refused, and the shell \ |
| 415 | hears about it.", what, code, want; |
| 416 | Test, Invalid, Mismatch)) |
| 417 | } |
| 418 | } |
| 419 | |
| 420 | /// Requires the tool to have said something, naming what it said instead. |
| 421 | fn says( |
| 422 | out: &str, |
| 423 | what: &str, |
| 424 | ) |
| 425 | -> Outcome<()> |
| 426 | { |
| 427 | if out.contains(what) { |
| 428 | Ok(()) |
| 429 | } else { |
| 430 | Err(err!( |
| 431 | "The tool was required to say '{}', and says:\n{}", what, out; |
| 432 | Test, Invalid, Mismatch)) |
| 433 | } |
| 434 | } |
| 435 | |
| 436 | /// Requires two byte strings to be equal, naming the first byte at which they part. |
| 437 | fn same( |
| 438 | name: &str, |
| 439 | what: &str, |
| 440 | got: &[u8], |
| 441 | want: &[u8], |
| 442 | ) |
| 443 | -> Outcome<()> |
| 444 | { |
| 445 | if got == want { |
| 446 | return Ok(()); |
| 447 | } |
| 448 | Err(err!( |
| 449 | "For '{}', {} is {} bytes against the {} required{}. A document written twice is the same \ |
| 450 | document.", |
| 451 | name, what, got.len(), want.len(), |
| 452 | match common::first_diff(got, want) { |
| 453 | Some(at) => fmt!(", first differing at byte {}", at), |
| 454 | None => String::new(), |
| 455 | }; |
| 456 | Test, Invalid, Mismatch)) |
| 457 | } |
| 458 | |
| 459 | /// Requires two things to be equal, naming the fixture and what was compared. |
| 460 | fn eq<T: PartialEq + std::fmt::Debug>( |
| 461 | name: &str, |
| 462 | what: &str, |
| 463 | got: &T, |
| 464 | want: &T, |
| 465 | ) |
| 466 | -> Outcome<()> |
| 467 | { |
| 468 | if got == want { |
| 469 | Ok(()) |
| 470 | } else { |
| 471 | Err(err!( |
| 472 | "For '{}', {} is {:?}, where {:?} was required.", name, what, got, want; |
| 473 | Test, Invalid, Mismatch)) |
| 474 | } |
| 475 | } |
| 476 | |
| 477 | /// A directory to work in, emptied of whatever a previous run left there. |
| 478 | fn scratch(what: &str) -> Outcome<PathBuf> { |
| 479 | let dir = std::env::temp_dir().join(fmt!("sbj_cli_{}_{}", what, std::process::id())); |
| 480 | if dir.exists() { |
| 481 | res!(clean(&dir)); |
| 482 | } |
| 483 | match fs::create_dir_all(&dir) { |
| 484 | Ok(()) => Ok(dir), |
| 485 | Err(e) => Err(err!(e, |
| 486 | "Could not make the working directory {}.", dir.display(); |
| 487 | Test, IO, File)), |
| 488 | } |
| 489 | } |
| 490 | |
| 491 | /// Removes a working directory. |
| 492 | fn clean(dir: &Path) -> Outcome<()> { |
| 493 | match fs::remove_dir_all(dir) { |
| 494 | Ok(()) => Ok(()), |
| 495 | Err(e) => Err(err!(e, |
| 496 | "Could not remove the working directory {}.", dir.display(); |
| 497 | Test, IO, File)), |
| 498 | } |
| 499 | } |
| 500 | |
| 501 | /// Writes a text file, naming it if it will not. |
| 502 | fn write_text( |
| 503 | path: &Path, |
| 504 | src: &str, |
| 505 | ) |
| 506 | -> Outcome<()> |
| 507 | { |
| 508 | common::write_bytes(path, src.as_bytes()) |
| 509 | } |
| 510 | |
| 511 | /// A path as the shell takes it. |
| 512 | fn path(p: &Path) -> String { |
| 513 | fmt!("{}", p.display()) |
| 514 | } |