#![cfg(unix)]
use super::{
DIRECT_FIX_CYCLE_MESSAGE, DirectFixPasses, NO_WORKTREE_MESSAGE, convergence_pass, finish_fix,
fix_failure, run_direct_fix, settled_fix,
};
use crate::candidate_object::CandidateMode;
use crate::candidate_prediction::CandidateRequest;
use crate::command_test_support::os_arguments;
use crate::config_schema::PolicyConfig;
use crate::diagnostics::EngineFailureCode;
use crate::event_path::EventPath;
use crate::policy_checks::{ShippedChecks, shipped_checks};
use crate::policy_content::{Correction, LifecycleContent};
use crate::policy_convergence::{
Convergence, FIX_PASS_LIMIT_MESSAGE, FixPasses, PassResult as ConvergencePass,
};
use crate::policy_engine::{StageEnd, StageRequest, Unavailable};
use crate::policy_events::{FindingEvent, PolicyEvent};
use crate::policy_pass::PassResult;
use crate::policy_registry::{PolicyId, Severity};
use crate::policy_test_support::{ScriptedFacts, scripted_facts, with_severity};
use crate::policy_trigger::Trigger;
use crate::test_support::{executable, fixture, git, remove, repository};
use crate::wrapper_controls::no_controls;
use std::ffi::OsString;
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
use std::rc::Rc;
fn fix_request(config: PolicyConfig) -> StageRequest {
return StageRequest {
trigger: Trigger::DirectFix,
config,
controls: no_controls(),
selected: Vec::<PolicyId>::new(),
};
}
fn fix_checks(repo: &Path) -> ShippedChecks<ScriptedFacts> {
let mut facts: ScriptedFacts = scripted_facts();
facts.candidates_repository = Some(repo.to_path_buf());
return shipped_checks(
facts,
os_arguments(&[]),
LifecycleContent::Requested(CandidateRequest::Direct(vec![OsString::from(":/")])),
Vec::<PathBuf>::new(),
);
}
fn summary(passes: u64, paths: &[&str]) -> PolicyEvent {
let mut changed_paths: Vec<EventPath> = Vec::new();
for path in paths {
changed_paths.push(EventPath::from_git_bytes(path.as_bytes()));
}
return PolicyEvent::FixSummary {
trigger: Trigger::DirectFix,
passes,
changed_paths,
};
}
fn pass_with(events: Vec<PolicyEvent>, end: StageEnd) -> PassResult {
return PassResult {
arguments: Vec::new(),
events,
end,
};
}
fn untidy_repository(root: &Path) -> PathBuf {
let repo: PathBuf = repository(root, "repo");
std::fs::write(repo.join("ok.txt"), b"ok\n").expect("ok");
std::fs::write(repo.join("b.txt"), b"b\n\n").expect("b");
git(repo.as_path(), &["add", "ok.txt", "b.txt"]);
git(repo.as_path(), &["commit", "--quiet", "--message=base"]);
std::fs::write(repo.join("a.txt"), b"a").expect("a");
executable(repo.join("run.sh").as_path(), b"#!/bin/sh");
return repo;
}
#[test]
fn stage_endings_drive_the_loop() {
assert_eq!(
convergence_pass(StageEnd::Completed),
ConvergencePass::Stable
);
assert_eq!(convergence_pass(StageEnd::Stopped), ConvergencePass::Stable);
assert_eq!(
convergence_pass(StageEnd::Proposed),
ConvergencePass::Proposed
);
assert_eq!(convergence_pass(StageEnd::Failed), ConvergencePass::Failed);
assert_eq!(
convergence_pass(StageEnd::Unavailable(Unavailable::Lifecycle(
Trigger::PostCommit
))),
ConvergencePass::Failed
);
}
#[test]
fn passes_propose_apply_and_compare_states() {
let root: PathBuf = fixture("direct-fix-passes");
let repo: PathBuf = untidy_repository(root.as_path());
let request: StageRequest = fix_request(PolicyConfig::defaults());
let mut checks: ShippedChecks<ScriptedFacts> = fix_checks(repo.as_path());
let stale: Correction = Correction {
path: b"stale.txt".to_vec(),
mode: CandidateMode::Regular,
before: Rc::from(&b"x"[..]),
after: Rc::from(&b"y"[..]),
};
checks.content.propose(stale.clone());
let mut passes: DirectFixPasses<'_, ScriptedFacts> =
DirectFixPasses::new(&request, &mut checks);
assert_eq!(passes.run_pass(0), ConvergencePass::Proposed);
assert!(passes.apply(1));
assert!(!passes.snapshots_equal(0, 1));
assert!(passes.snapshots_equal(1, 1));
assert_eq!(passes.run_pass(1), ConvergencePass::Stable);
assert_eq!(
passes.last,
Some(pass_with(Vec::new(), StageEnd::Completed))
);
assert!(passes.apply(2));
assert!(passes.snapshots_equal(1, 2));
let changed: Vec<Vec<u8>> = checks
.content
.install_changes()
.into_iter()
.map(change_path)
.collect();
assert_eq!(
changed,
vec![b"a.txt".to_vec(), b"b.txt".to_vec(), b"run.sh".to_vec()],
"the stale proposal was dropped before the first pass"
);
remove(root.as_path());
}
fn change_path(change: crate::direct_fix_install::InstallChange) -> Vec<u8> {
return change.path;
}
#[test]
fn a_clean_fix_installs_and_reports_only_its_summary() {
let root: PathBuf = fixture("direct-fix-clean");
let repo: PathBuf = untidy_repository(root.as_path());
let index_before: Vec<u8> = std::fs::read(repo.join(".git/index")).expect("index");
let request: StageRequest = fix_request(PolicyConfig::defaults());
let mut checks: ShippedChecks<ScriptedFacts> = fix_checks(repo.as_path());
assert_eq!(
run_direct_fix(&request, &mut checks, Some(repo.as_path())),
pass_with(
vec![summary(1, &["a.txt", "b.txt", "run.sh"])],
StageEnd::Completed
)
);
assert_eq!(std::fs::read(repo.join("a.txt")).expect("a"), b"a\n");
assert_eq!(std::fs::read(repo.join("b.txt")).expect("b"), b"b\n");
assert_eq!(
std::fs::read(repo.join("run.sh")).expect("run"),
b"#!/bin/sh\n"
);
let mode: u32 = std::fs::metadata(repo.join("run.sh"))
.expect("run")
.permissions()
.mode();
assert_eq!(mode & 0o100, 0o100, "{mode:o}");
assert_eq!(
std::fs::read(repo.join(".git/index")).expect("index"),
index_before
);
let mut again: ShippedChecks<ScriptedFacts> = fix_checks(repo.as_path());
assert_eq!(
run_direct_fix(&request, &mut again, Some(repo.as_path())),
pass_with(Vec::new(), StageEnd::Completed)
);
remove(root.as_path());
}
#[test]
fn a_fix_with_the_wired_policy_listed_completes_and_installs() {
let root: PathBuf = fixture("direct-fix-listed");
let repo: PathBuf = untidy_repository(root.as_path());
let listed: StageRequest = fix_request(with_severity(
&PolicyConfig::defaults(),
PolicyId::DependentVersionBump,
Severity::Warn,
));
let mut checks: ShippedChecks<ScriptedFacts> = fix_checks(repo.as_path());
let result: PassResult = run_direct_fix(&listed, &mut checks, Some(repo.as_path()));
assert_eq!(result.end, StageEnd::Completed);
assert_eq!(std::fs::read(repo.join("a.txt")).expect("a"), b"a\n");
remove(root.as_path());
}
#[test]
fn a_settled_fix_that_cannot_install_reports_one_failure() {
let root: PathBuf = fixture("direct-fix-install-failure");
let repo: PathBuf = untidy_repository(root.as_path());
let request: StageRequest = fix_request(PolicyConfig::defaults());
let mut checks: ShippedChecks<ScriptedFacts> = fix_checks(repo.as_path());
assert_eq!(
run_direct_fix(&request, &mut checks, None),
fix_failure(
&[],
EngineFailureCode::TransactionFailed,
NO_WORKTREE_MESSAGE
)
);
let mut changing: ShippedChecks<ScriptedFacts> = fix_checks(repo.as_path());
changing
.content
.prepare(&changing.candidates, &mut changing.facts)
.expect("prepared");
std::fs::write(repo.join("a.txt"), b"edited meanwhile").expect("edit");
assert_eq!(
run_direct_fix(&request, &mut changing, Some(repo.as_path())),
fix_failure(
&[],
EngineFailureCode::TransactionFailed,
"cli-git fix could not install its corrections: a selected file changed while cli-git fix ran, so no file was changed. Run cli-git fix again."
)
);
assert_eq!(
std::fs::read(repo.join("a.txt")).expect("a"),
b"edited meanwhile"
);
assert_eq!(std::fs::read(repo.join("b.txt")).expect("b"), b"b\n\n");
remove(root.as_path());
}
#[test]
fn every_ending_reports_what_the_installed_wrapper_reports() {
let warned: PassResult = pass_with(
vec![PolicyEvent::WarnUnsafe {
trigger: Trigger::DirectFix,
policy: PolicyId::FinalNewline,
}],
StageEnd::Completed,
);
let checks: ShippedChecks<ScriptedFacts> = shipped_checks(
scripted_facts(),
os_arguments(&["kept"]),
LifecycleContent::None,
Vec::<PathBuf>::new(),
);
let mut arguments_kept: PassResult = warned.clone();
arguments_kept.arguments = os_arguments(&["kept"]);
assert_eq!(
finish_fix(Convergence::FixCycle, arguments_kept.clone(), &checks, None),
PassResult {
arguments: os_arguments(&["kept"]),
events: vec![PolicyEvent::EngineFailure {
code: EngineFailureCode::FixCycle,
message: String::from(DIRECT_FIX_CYCLE_MESSAGE),
trigger: Some(Trigger::DirectFix),
policy: None,
path: None,
}],
end: StageEnd::Failed,
}
);
assert_eq!(
finish_fix(Convergence::FixPassLimit, warned.clone(), &checks, None),
fix_failure(&[], EngineFailureCode::FixPassLimit, FIX_PASS_LIMIT_MESSAGE)
);
assert_eq!(
finish_fix(Convergence::Blocked, warned.clone(), &checks, None),
warned
);
assert_eq!(
finish_fix(
Convergence::Settled { changed_passes: 0 },
warned.clone(),
&checks,
None
),
warned
);
let failing: PassResult = pass_with(Vec::new(), StageEnd::Failed);
assert_eq!(settled_fix(failing.clone(), 1, &checks, None), failing);
let mut corrected: ShippedChecks<ScriptedFacts> = shipped_checks(
scripted_facts(),
os_arguments(&[]),
LifecycleContent::None,
Vec::<PathBuf>::new(),
);
corrected.content.apply(vec![Correction {
path: b"a.txt".to_vec(),
mode: CandidateMode::Regular,
before: Rc::from(&b"a"[..]),
after: Rc::from(&b"a\n"[..]),
}]);
let stopped: PassResult = pass_with(
vec![PolicyEvent::Finding(FindingEvent {
trigger: Trigger::DirectFix,
policy: PolicyId::FinalNewline,
severity: Severity::Error,
code: "noncanonical-final-newline",
message: String::from("message"),
path: Some(EventPath::from_git_bytes("a.txt".as_bytes())),
location: None,
fix_available: false,
})],
StageEnd::Stopped,
);
assert_eq!(
finish_fix(
Convergence::Settled { changed_passes: 1 },
stopped.clone(),
&corrected,
None
),
stopped
);
assert_eq!(
settled_fix(warned.clone(), 1, &corrected, None),
fix_failure(
&[],
EngineFailureCode::TransactionFailed,
NO_WORKTREE_MESSAGE
)
);
}