use super::*; struct Dir(PathBuf); impl Dir { fn new() -> Self { for _ in 0..100 { let p = std::env::temp_dir().join(format!( "nanobrush-cli-test-{}-{}", std::process::id(), NEXT_TEMP.fetch_add(1, Ordering::Relaxed) )); match fs::create_dir(&p) { Ok(()) => return Self(p), Err(e) if e.kind() == io::ErrorKind::AlreadyExists => (), Err(e) => panic!("{e}"), } } panic!("temp dir collision") } fn file(&self, name: &str, bytes: &[u8]) -> PathBuf { let p = self.0.join(name); fs::write(&p, bytes).unwrap(); p } } impl Drop for Dir { fn drop(&mut self) { let _ = fs::remove_dir_all(&self.0); } } fn call(args: &[&std::ffi::OsStr]) -> Result> { let mut out = Vec::new(); run(args.iter().map(|s| s.to_os_string()), &mut out)?; Ok(out) } const EMPTY: &[u8] = b"world(computedFeatures false)"; #[test] fn inspect_validate_and_both_versions() { let d = Dir::new(); let input = d.file("é space.txt", EMPTY); let summary = call(&["inspect".as_ref(), input.as_os_str()]).unwrap(); assert!( String::from_utf8(summary) .unwrap() .contains("\"dat_version\":null") ); for format in ["dat-201501280", "dat-201512200"] { let target = d.0.join(format); call(&[ "convert".as_ref(), input.as_os_str(), target.as_os_str(), "--to".as_ref(), format.as_ref(), ]) .unwrap(); assert!(mb3_dat::decode(&fs::read(&target).unwrap(), &Limits::default()).is_ok()); call(&["validate".as_ref(), target.as_os_str()]).unwrap(); let txt = d.0.join(format!("{format}.txt")); call(&[ "convert".as_ref(), target.as_os_str(), txt.as_os_str(), "--to".as_ref(), "txt".as_ref(), ]) .unwrap(); assert_eq!( mb3_txt::decode(&fs::read(txt).unwrap(), &Limits::default()).unwrap(), World::default() ); } } #[test] fn usage_and_explicit_limits() { for args in [ vec![], vec!["x"], vec!["inspect"], vec!["convert", "in", "out"], vec!["inspect", "in", "--force"], vec!["inspect", "in", "--max-items", "-1"], vec!["inspect", "in", "--from", "txt", "--from", "dat"], ] { let args: Vec<_> = args.iter().map(std::ffi::OsStr::new).collect(); assert!(call(&args).unwrap_err().usage); } assert!(!call(&["--help".as_ref()]).unwrap().is_empty()); let d = Dir::new(); let p = d.file("in", EMPTY); assert!( !call(&[ "validate".as_ref(), p.as_os_str(), "--max-bytes".as_ref(), "1".as_ref() ]) .unwrap_err() .usage ); assert!( call(&[ "validate".as_ref(), p.as_os_str(), "--from".as_ref(), "dat".as_ref() ]) .is_err() ); } #[test] fn no_clobber_force_and_same_file_protection() { let d = Dir::new(); let input = d.file("in", EMPTY); let output = d.file("out", b"ORIGINAL"); assert!(save(&input, &output, b"NEW", false).is_err()); assert_eq!(fs::read(&output).unwrap(), b"ORIGINAL"); save(&input, &output, b"NEW", true).unwrap(); assert_eq!(fs::read(&output).unwrap(), b"NEW"); assert!(save(&input, &input, b"BAD", true).is_err()); let link = d.0.join("hardlink"); fs::hard_link(&input, &link).unwrap(); assert!(save(&input, &link, b"BAD", true).is_err()); assert_eq!(fs::read(input).unwrap(), EMPTY); assert_eq!(fs::read_dir(&d.0).unwrap().count(), 3); } #[test] fn failed_validation_creates_no_output_or_temporary() { let d = Dir::new(); let input = d.file( "in", b"world(computedFeatures false stringProperties[\"p\"])", ); let output = d.0.join("out"); assert!( call(&[ "convert".as_ref(), input.as_os_str(), output.as_os_str(), "--to".as_ref(), "dat-201501280".as_ref() ]) .is_err() ); assert!(!output.exists()); assert_eq!(fs::read_dir(&d.0).unwrap().count(), 1); } #[cfg(unix)] #[test] fn symlink_destinations_rejected() { let d = Dir::new(); let input = d.file("in", EMPTY); let target = d.file("target", b"ORIGINAL"); let link = d.0.join("link"); std::os::unix::fs::symlink(&target, &link).unwrap(); assert!(save(&input, &link, b"BAD", true).is_err()); assert_eq!(fs::read(target).unwrap(), b"ORIGINAL"); } #[test] fn failed_publication_cleans_temporary_and_preserves_existing() { let d = Dir::new(); let input = d.file("in", EMPTY); let output = d.0.join("out"); fs::create_dir(&output).unwrap(); assert!(save(&input, &output, b"NEW", false).is_err()); assert_eq!(fs::read_dir(&d.0).unwrap().count(), 2); } #[test] fn publication_race_is_no_clobber_and_owned_temp_is_cleaned() { let d = Dir::new(); let output = d.file("winner", b"EXISTING"); let path = d.file("staged", b"REPLACEMENT"); assert!(publish(Temp(path.clone()), &output, false).is_err()); assert!(!path.exists()); assert_eq!(fs::read(output).unwrap(), b"EXISTING"); } #[test] fn invalid_conversion_with_force_keeps_existing_destination() { let d = Dir::new(); let input = d.file( "unicode.txt", "world(computedFeatures false stringProperties[\"é\"])".as_bytes(), ); let output = d.file("out.dat", b"KEEP"); assert!( call(&[ "convert".as_ref(), input.as_os_str(), output.as_os_str(), "--to".as_ref(), "dat-201512200".as_ref(), "--force".as_ref() ]) .is_err() ); assert_eq!(fs::read(output).unwrap(), b"KEEP"); assert_eq!(fs::read_dir(&d.0).unwrap().count(), 2); }