#![cfg(unix)]
use super::{ContentState, Correction, LifecycleContent, candidate_failure};
use crate::candidate_error::{CandidateError, CandidateFailure};
use crate::candidate_object::CandidateMode;
use crate::candidate_prediction::CandidateRequest;
use crate::candidate_version::{Candidate, CandidateVersion};
use crate::diagnostics::EngineFailureCode;
use crate::direct_fix_install::InstallChange;
use crate::policy_engine::PolicyOutcome;
use crate::policy_test_support::{ScriptedFacts, scripted_facts};
use crate::scanner_adapter::CandidateScanner;
use crate::scanner_run::ScanRunError;
use crate::scanner_selection::rules_source;
use crate::scanner_test_support::{needle, rules_file, run_isolated};
use crate::test_support::{fixture, remove, repository};
use forbidden_strings::CandidateScan;
use std::ffi::OsString;
use std::path::Path;
use std::path::PathBuf;
use std::rc::Rc;
fn whole_worktree() -> LifecycleContent {
return LifecycleContent::Requested(CandidateRequest::Direct(vec![OsString::from(":/")]));
}
fn not_prepared() -> PolicyOutcome {
return PolicyOutcome::Failed {
code: EngineFailureCode::PolicyIncomplete,
message: String::from(
"cli-git read candidate content before the candidates were prepared; this is a defect in cli-git.",
),
path: None,
};
}
#[test]
fn no_candidates_prepare_nothing() {
let mut facts: ScriptedFacts = scripted_facts();
let mut content: ContentState = ContentState::default();
assert!(
content
.prepare(&LifecycleContent::None, &mut facts)
.expect("nothing to prepare")
.is_none()
);
assert!(
content
.version(&LifecycleContent::None, &mut facts)
.expect("nothing to read")
.is_none()
);
assert_eq!(facts.asked, Vec::<String>::new());
}
#[test]
fn candidates_are_prepared_once_and_shared() {
let root: PathBuf = fixture("content-shared");
let repo: PathBuf = repository(root.as_path(), "repo");
std::fs::write(repo.join("a.txt"), b"bytes\n").expect("file");
let mut facts: ScriptedFacts = scripted_facts();
facts.candidates_repository = Some(repo.clone());
let mut content: ContentState = ContentState::new();
let first: Rc<CandidateVersion> = content
.prepare(&whole_worktree(), &mut facts)
.expect("prepared")
.expect("candidates");
let second: Rc<CandidateVersion> = content
.version(&whole_worktree(), &mut facts)
.expect("read")
.expect("candidates");
assert!(Rc::ptr_eq(&first, &second), "one version is shared");
assert_eq!(facts.asked, vec![String::from("candidates")]);
let candidate: Candidate = first.candidates()[0].clone();
assert_eq!(&content.bytes(&candidate).expect("bytes")[..], b"bytes\n");
let object: String = candidate
.object
.clone()
.expect("object")
.as_str()
.to_owned();
std::fs::remove_file(
repo.join(".git/objects")
.join(&object[..2])
.join(&object[2..]),
)
.expect("remove the loose object");
let mut fresh: ContentState = ContentState::new();
let mut fresh_facts: ScriptedFacts = scripted_facts();
fresh_facts.candidates_repository = Some(repo.clone());
let version: Rc<CandidateVersion> = fresh
.version(&whole_worktree(), &mut fresh_facts)
.expect("listing needs no blob")
.expect("candidates");
let again: String = version.candidates()[0]
.object
.clone()
.expect("object")
.as_str()
.to_owned();
assert_eq!(again, object, "the same bytes name the same object");
std::fs::remove_file(
repo.join(".git/objects")
.join(&object[..2])
.join(&object[2..]),
)
.expect("remove the rewritten loose object");
match fresh.bytes(&version.candidates()[0]) {
Err(PolicyOutcome::Failed { code, message, .. }) => {
assert_eq!(code, EngineFailureCode::ContentUnavailable);
assert!(!message.contains("a.txt"), "{message}");
}
other => panic!("expected unreadable content, got {other:?}"),
}
remove(root.as_path());
}
#[test]
fn a_failed_preparation_is_remembered() {
let mut facts: ScriptedFacts = scripted_facts();
let mut content: ContentState = ContentState::new();
let raw: CandidateError = content
.prepare(&whole_worktree(), &mut facts)
.expect_err("scripted facts prepare nothing");
assert_eq!(raw.failure, CandidateFailure::GitNotStarted);
let expected: PolicyOutcome = PolicyOutcome::Failed {
code: EngineFailureCode::ContentUnavailable,
message: String::from("the scripted facts prepare no candidates"),
path: None,
};
for _attempt in 0..2 {
assert_eq!(
content
.version(&whole_worktree(), &mut facts)
.expect_err("remembered failure"),
expected
);
}
assert_eq!(facts.asked, vec![String::from("candidates")]);
let version_less: Candidate = Candidate {
identity: crate::candidate_version::CandidateIdentity {
generation: 0,
index: 0,
},
path: b"a.txt".to_vec(),
mode: crate::candidate_object::CandidateMode::Regular,
change: crate::candidate_record::CandidateChange::Added,
object: None,
};
assert_eq!(content.bytes(&version_less), Err(not_prepared()));
assert_eq!(
ContentState::new().bytes(&version_less),
Err(not_prepared())
);
}
#[test]
fn candidate_failures_carry_the_code_of_their_cause() {
assert_eq!(
candidate_failure(&CandidateError::new(CandidateFailure::GitFailed, "listing")),
PolicyOutcome::Failed {
code: EngineFailureCode::ContentUnavailable,
message: String::from("listing"),
path: None,
}
);
assert_eq!(
candidate_failure(&CandidateError::new(
CandidateFailure::StaleCandidate,
"stale"
)),
PolicyOutcome::Failed {
code: EngineFailureCode::PolicyIncomplete,
message: String::from("stale"),
path: None,
}
);
}
fn correction(path: &str, before: &[u8], after: &[u8]) -> Correction {
return Correction {
path: path.as_bytes().to_vec(),
mode: CandidateMode::Regular,
before: Rc::from(before),
after: Rc::from(after),
};
}
fn change(path: &str, original: &[u8], replacement: &[u8]) -> InstallChange {
return InstallChange {
path: path.as_bytes().to_vec(),
mode: CandidateMode::Regular,
original: Rc::from(original),
replacement: Rc::from(replacement),
};
}
#[test]
fn corrections_overlay_the_prepared_bytes() {
let root: PathBuf = fixture("content-corrections");
let repo: PathBuf = repository(root.as_path(), "repo");
std::fs::write(repo.join("b.txt"), b"b").expect("b");
std::fs::write(repo.join("a.txt"), b"a").expect("a");
let mut facts: ScriptedFacts = scripted_facts();
facts.candidates_repository = Some(repo.clone());
let mut content: ContentState = ContentState::new();
assert_eq!(content.real_index(), None);
let version: Rc<CandidateVersion> = content
.version(&whole_worktree(), &mut facts)
.expect("read")
.expect("candidates");
assert_eq!(
content.real_index(),
Some(repo.join(".git/index").as_path())
);
content.propose(correction("b.txt", b"b", b"b\n"));
content.propose(correction("a.txt", b"a", b"a\n"));
let proposals: Vec<Correction> = content.take_proposals();
assert_eq!(proposals.len(), 2);
assert_eq!(content.take_proposals(), Vec::<Correction>::new());
assert_eq!(
&content.bytes(&version.candidates()[0]).expect("a")[..],
b"a"
);
content.apply(proposals);
assert_eq!(
&content.bytes(&version.candidates()[0]).expect("a")[..],
b"a\n"
);
assert_eq!(
&content.bytes(&version.candidates()[1]).expect("b")[..],
b"b\n"
);
assert_eq!(
content.install_changes(),
vec![change("a.txt", b"a", b"a\n"), change("b.txt", b"b", b"b\n")]
);
content.apply(vec![
correction("a.txt", b"a\n", b"a\n\n"),
correction("b.txt", b"b\n", b"b"),
]);
assert_eq!(
content.install_changes(),
vec![change("a.txt", b"a", b"a\n\n")]
);
assert_eq!(
&content.bytes(&version.candidates()[1]).expect("b")[..],
b"b"
);
let state = content.corrected_state();
assert_eq!(state.len(), 1);
assert_eq!(
state.get(b"a.txt".as_slice()),
Some(&change("a.txt", b"a", b"a\n\n"))
);
remove(root.as_path());
}
#[test]
fn the_scan_reads_corrected_bytes() {
run_isolated(
"policy_content::tests::the_scan_reads_corrected_bytes",
"content-scan-overlay",
scan_overlay,
);
}
fn scan_overlay(work: &Path) {
let repo: PathBuf = repository(work, "repo");
std::fs::write(repo.join("a.txt"), format!("{}\n", needle())).expect("needle");
let mut facts: ScriptedFacts = scripted_facts();
facts.candidates_repository = Some(repo.clone());
let mut content: ContentState = ContentState::new();
let scanner: CandidateScanner = CandidateScanner::load(
&rules_source(None, Some(rules_file(work).as_os_str()), &repo),
false,
)
.expect("rules");
assert!(matches!(
content.scan(&scanner, None),
Err(ScanRunError::Candidate(_))
));
content
.version(&whole_worktree(), &mut facts)
.expect("read")
.expect("candidates");
let before: Vec<CandidateScan> = content.scan(&scanner, None).expect("scan");
assert_eq!(before.len(), 1);
assert_eq!(before[0].findings.len(), 1);
content.apply(vec![Correction {
path: b"a.txt".to_vec(),
mode: CandidateMode::Regular,
before: Rc::from(format!("{}\n", needle()).as_bytes()),
after: Rc::from(&b"clean\n"[..]),
}]);
let after: Vec<CandidateScan> = content.scan(&scanner, None).expect("scan");
assert_eq!(after.len(), 1);
assert_eq!(after[0].findings, Vec::new());
}