use super::{CandidateChange, CandidateRecord, parse_raw_records};
use crate::candidate_error::{CandidateError, CandidateFailure};
use crate::candidate_object::{CandidateMode, ObjectId, parse_object_id};
const ZERO: &str = "0000000000000000000000000000000000000000";
const BLOB: &str = "5626abf0f72e58d7a153368ba57db4c673c0e171";
const OTHER: &str = "646377bf16c0f4b10249686e46f203a6cb55bfa5";
fn object(text: &str) -> ObjectId {
return parse_object_id(text.as_bytes()).expect("fixture name");
}
fn raw(metadata: &str, path: &[u8]) -> Vec<u8> {
let mut bytes: Vec<u8> = metadata.as_bytes().to_vec();
bytes.push(0);
bytes.extend_from_slice(path);
bytes.push(0);
return bytes;
}
fn failure_of(output: &[u8]) -> CandidateError {
let error: CandidateError = parse_raw_records(output).expect_err("refused listing");
assert!(
error
.message
.starts_with("cli-git could not list candidate files: changed-path record "),
"{error}"
);
return error;
}
#[test]
fn empty_output_lists_nothing() {
assert_eq!(parse_raw_records(b"").expect("empty"), Vec::new());
}
#[test]
fn each_status_reads_the_side_it_describes() {
let mut output: Vec<u8> = raw(&format!(":000000 100644 {ZERO} {BLOB} A"), b"new.txt");
output.extend(raw(
&format!(":100644 100755 {BLOB} {OTHER} M"),
b"dir/changed.sh",
));
output.extend(raw(
&format!(":100644 120000 {BLOB} {OTHER} T"),
b"now-a-link",
));
output.extend(raw(&format!(":100755 000000 {BLOB} {ZERO} D"), b"gone"));
output.extend(raw(&format!(":000000 160000 {ZERO} {OTHER} A"), b"sub"));
assert_eq!(
parse_raw_records(output.as_slice()).expect("records"),
vec![
CandidateRecord {
path: b"new.txt".to_vec(),
mode: CandidateMode::Regular,
change: CandidateChange::Added,
object: Some(object(BLOB)),
},
CandidateRecord {
path: b"dir/changed.sh".to_vec(),
mode: CandidateMode::Executable,
change: CandidateChange::Modified,
object: Some(object(OTHER)),
},
CandidateRecord {
path: b"now-a-link".to_vec(),
mode: CandidateMode::Symlink,
change: CandidateChange::Modified,
object: Some(object(OTHER)),
},
CandidateRecord {
path: b"gone".to_vec(),
mode: CandidateMode::Executable,
change: CandidateChange::Deleted,
object: None,
},
CandidateRecord {
path: b"sub".to_vec(),
mode: CandidateMode::Gitlink,
change: CandidateChange::Added,
object: Some(object(OTHER)),
},
]
);
}
#[test]
fn pathname_bytes_are_never_decoded() {
let path: &[u8] = b"d\xff\xfe/ sp\tace\nline:colon\"quote\\";
let output: Vec<u8> = raw(&format!(":000000 100644 {ZERO} {BLOB} A"), path);
let records: Vec<CandidateRecord> = parse_raw_records(output.as_slice()).expect("record");
assert_eq!(records.len(), 1);
assert_eq!(records[0].path, path);
}
#[test]
fn sha256_names_are_accepted() {
let zero: String = "0".repeat(64);
let name: String =
"0b3131207073aeabcc6ae5b5d57a4875e562bf4ef3b757adb6b75defb2ec9df1".to_owned();
let output: Vec<u8> = raw(&format!(":000000 100644 {zero} {name} A"), b"q");
let records: Vec<CandidateRecord> = parse_raw_records(output.as_slice()).expect("record");
assert_eq!(records[0].object, Some(object(name.as_str())));
}
#[test]
fn unmerged_entry_is_refused() {
let mut output: Vec<u8> = raw(&format!(":000000 100644 {ZERO} {BLOB} A"), b"ok");
output.extend(raw(
&format!(":100644 000000 {BLOB} {ZERO} U"),
b"private-name",
));
let error: CandidateError = failure_of(output.as_slice());
assert_eq!(error.failure, CandidateFailure::UnmergedPath);
assert!(error.message.contains("record 1 is an unmerged"), "{error}");
assert!(!error.message.contains("private-name"), "{error}");
}
#[test]
fn other_statuses_are_refused() {
for status in ["R100", "C75", "X", "B", "", "AM", "a", "m", "t", "d", "DD"] {
let output: Vec<u8> = raw(&format!(":100644 100644 {BLOB} {OTHER} {status}"), b"p");
let error: CandidateError = failure_of(output.as_slice());
assert_eq!(
error.failure,
CandidateFailure::UnsupportedStatus,
"{status:?}"
);
}
}
#[test]
fn unsupported_modes_are_refused() {
for metadata in [
format!(":000000 040000 {ZERO} {BLOB} A"),
format!(":100644 000000 {BLOB} {OTHER} M"),
format!(":100644 100664 {BLOB} {OTHER} M"),
format!(":000000 100644 {BLOB} {ZERO} D"),
format!(":040000 100644 {BLOB} {ZERO} D"),
] {
let output: Vec<u8> = raw(metadata.as_str(), b"p");
assert_eq!(
failure_of(output.as_slice()).failure,
CandidateFailure::UnsupportedMode,
"{metadata}"
);
}
let accepted: Vec<u8> = raw(&format!(":100644 000000 {BLOB} {ZERO} D"), b"p");
assert_eq!(
parse_raw_records(accepted.as_slice())
.expect("deleted")
.len(),
1
);
}
#[test]
fn malformed_records_are_refused() {
for (metadata, path) in [
(format!("000000 100644 {ZERO} {BLOB} A"), b"p".as_slice()),
(format!(":000000 100644 {ZERO} {BLOB}"), b"p"),
(format!(":000000 100644 {ZERO} {BLOB} A extra"), b"p"),
(format!(":000000 100644 {ZERO} {BLOB} A"), b""),
(format!(":000000 100644 {ZERO} {} A", &BLOB[..39]), b"p"),
(format!(":000000 100644 {} {BLOB} A", &ZERO[..39]), b"p"),
(
format!(":000000 100644 {ZERO} {} A", BLOB.to_uppercase()),
b"p",
),
(format!(":100644 000000 {} {ZERO} D", &BLOB[..39]), b"p"),
(format!(":100644 000000 {BLOB} {} D", &ZERO[..39]), b"p"),
(String::from(":"), b"p"),
(String::new(), b"p"),
] {
let output: Vec<u8> = raw(metadata.as_str(), path);
assert_eq!(
failure_of(output.as_slice()).failure,
CandidateFailure::ListingMalformed,
"{metadata:?} {path:?}"
);
}
}
#[test]
fn cut_short_listings_are_refused() {
let complete: Vec<u8> = raw(&format!(":000000 100644 {ZERO} {BLOB} A"), b"first");
let without_terminator: &[u8] = &complete[..complete.len() - 1];
let error: CandidateError = failure_of(without_terminator);
assert_eq!(error.failure, CandidateFailure::ListingMalformed);
assert!(
error.message.contains("does not end with a NUL byte"),
"{error}"
);
let mut odd: Vec<u8> = complete.clone();
odd.extend(raw(&format!(":000000 100644 {ZERO} {OTHER} A"), b"second"));
odd.extend_from_slice(format!(":000000 100644 {ZERO} {BLOB} A").as_bytes());
odd.push(0);
let odd_error: CandidateError = failure_of(odd.as_slice());
assert_eq!(odd_error.failure, CandidateFailure::ListingMalformed);
assert!(
odd_error.message.contains("record 2 has no pathname token"),
"{odd_error}"
);
assert_eq!(failure_of(&[0]).failure, CandidateFailure::ListingMalformed);
let mut second_bad: Vec<u8> = complete.clone();
second_bad.extend(raw("not-a-record", b"p"));
assert!(
failure_of(second_bad.as_slice())
.message
.contains("record 1 does not start with a colon")
);
}