use super::*;
use std::collections::HashMap;
use std::sync::Mutex;
use std::time::{SystemTime, UNIX_EPOCH};
#[derive(Default)]
struct FakeBackend {
build_dir: PathBuf,
calls: Mutex<Vec<String>>,
artifacts: Mutex<Vec<PathBuf>>,
installed: Mutex<HashMap<String, String>>,
}
impl FakeBackend {
fn new(build_dir: PathBuf) -> Self {
Self {
build_dir,
calls: Mutex::new(Vec::new()),
artifacts: Mutex::new(Vec::new()),
installed: Mutex::new(HashMap::new()),
}
}
fn calls(&self) -> Vec<String> {
self.calls.lock().unwrap().clone()
}
}
impl PackageBackend for FakeBackend {
fn build_dir(&self) -> &Path {
&self.build_dir
}
async fn system_upgrade(
&self,
_refresh: bool,
_sysupgrade: bool,
_no_confirm: bool,
_dry_run: bool,
) -> Result<i32> {
self.calls.lock().unwrap().push("system_upgrade".into());
Ok(0)
}
async fn install_repo_packages(
&self,
packages: &[String],
needed: bool,
_no_confirm: bool,
as_deps: bool,
_dry_run: bool,
) -> Result<i32> {
self.calls.lock().unwrap().push(format!(
"repo needed={needed} as_deps={as_deps} {}",
packages.join(" ")
));
Ok(0)
}
async fn sync_git_repo(&self, _url: &str, dest: &Path, _dry_run: bool) -> Result<i32> {
self.calls
.lock()
.unwrap()
.push(format!("sync {}", dest.display()));
Ok(0)
}
async fn build_aur_package(
&self,
dir: &Path,
_no_confirm: bool,
_no_check: bool,
_dry_run: bool,
) -> Result<i32> {
self.calls
.lock()
.unwrap()
.push(format!("build {}", dir.display()));
Ok(0)
}
async fn package_list(&self, _dir: &Path) -> Result<Vec<PathBuf>> {
Ok(self.artifacts.lock().unwrap().clone())
}
async fn install_local_packages(
&self,
packages: &[PathBuf],
needed: bool,
_no_confirm: bool,
_dry_run: bool,
) -> Result<i32> {
self.calls.lock().unwrap().push(format!(
"local needed={needed} {}",
packages
.iter()
.map(|path| path.display().to_string())
.collect::<Vec<_>>()
.join(" ")
));
Ok(0)
}
async fn pacman_database_check(&self, _dry_run: bool) -> Result<i32> {
self.calls.lock().unwrap().push("dbcheck".into());
Ok(0)
}
async fn query_installed_version(&self, package: &str) -> Result<Option<String>> {
Ok(self.installed.lock().unwrap().get(package).cloned())
}
}
fn temp_dir(name: &str) -> PathBuf {
let path = std::env::temp_dir().join(format!(
"knott-install-{name}-{}-{}",
std::process::id(),
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos()
));
std::fs::create_dir_all(&path).unwrap();
path
}
fn options() -> Options {
Options {
needed: true,
no_confirm: true,
..Options::default()
}
}
fn tx_with_phase(phase: TransactionPhase, build_dir: PathBuf, artifact: PathBuf) -> Transaction {
let mut tx =
transaction::new_unsaved(vec!["-S".into(), "foo".into()], false, false, &options());
tx.phase = phase;
tx.repo_deps = vec!["repo-dep".into()];
tx.aur_items = vec![AurTransactionItem {
name: "foo".into(),
base: "foo".into(),
version: Some("1-1".into()),
status: match phase {
TransactionPhase::AurBuildStarted => AurItemStatus::BuildStarted,
TransactionPhase::AurBuilt => AurItemStatus::Built,
TransactionPhase::AurInstallStarted => AurItemStatus::InstallStarted,
_ => AurItemStatus::Pending,
},
build_dir,
artifacts: if matches!(
phase,
TransactionPhase::AurBuilt | TransactionPhase::AurInstallStarted
) {
vec![artifact]
} else {
Vec::new()
},
installed_as_dependency: false,
}];
tx
}
async fn save_for_resume(tx: &Transaction) {
transaction::save_current(tx).unwrap();
}
#[tokio::test]
async fn dry_run_does_not_create_durable_transaction() {
let _guard = transaction::test_env_lock();
let state = temp_dir("dry-run-state");
std::env::set_var("XDG_STATE_HOME", &state);
std::env::remove_var("XDG_RUNTIME_DIR");
let mut dry = options();
dry.dry_run = true;
let tools = FakeBackend::new(temp_dir("dry-run-build"));
let mut journal = Journal::Memory(transaction::new_unsaved(Vec::new(), false, false, &dry));
let code = install_saved_plan(&dry, &tools, None, &mut journal)
.await
.unwrap();
assert_eq!(code, 0);
assert!(transaction::load_current().unwrap().is_none());
}
#[test]
fn prefer_bin_rewrites_plain_targets_only_when_candidate_exists() {
let mut candidates = HashMap::new();
candidates.insert(
"foo-bin".into(),
AurPackage {
name: "foo-bin".into(),
..AurPackage::default()
},
);
let (targets, replacements) = apply_preferred_bin_targets(
&[
"foo".into(),
"bar".into(),
"baz-bin".into(),
"qux>=1".into(),
],
&candidates,
);
assert_eq!(targets, ["foo-bin", "bar", "baz-bin", "qux>=1"]);
assert_eq!(replacements, [("foo".into(), "foo-bin".into())]);
}
#[tokio::test]
async fn resume_from_repo_upgrade_started_refuses_aur() {
let _guard = transaction::test_env_lock();
let state = temp_dir("repo-started-state");
std::env::set_var("XDG_STATE_HOME", &state);
std::env::remove_var("XDG_RUNTIME_DIR");
let artifact = temp_dir("repo-started-build").join("foo.pkg.tar.zst");
let mut tx = tx_with_phase(
TransactionPhase::RepoUpgradeStarted,
temp_dir("repo-started-build"),
artifact,
);
save_for_resume(&tx).await;
let tools = FakeBackend::new(temp_dir("repo-started-tools"));
let code = resume_transaction(&options(), &tools, None, &mut tx)
.await
.unwrap();
assert_eq!(code, 1);
assert!(tools.calls().is_empty());
}
#[tokio::test]
async fn resume_from_repo_deps_started_reruns_with_needed() {
let _guard = transaction::test_env_lock();
let state = temp_dir("repo-deps-state");
std::env::set_var("XDG_STATE_HOME", &state);
std::env::remove_var("XDG_RUNTIME_DIR");
let artifact = temp_dir("repo-deps-build").join("foo.pkg.tar.zst");
std::fs::write(&artifact, b"pkg").unwrap();
let mut tx = tx_with_phase(
TransactionPhase::RepoDepsInstallStarted,
temp_dir("repo-deps-build"),
artifact.clone(),
);
tx.aur_items[0].status = AurItemStatus::Installed;
save_for_resume(&tx).await;
let tools = FakeBackend::new(temp_dir("repo-deps-tools"));
let code = resume_transaction(&options(), &tools, None, &mut tx)
.await
.unwrap();
assert_eq!(code, 0);
assert!(tools
.calls()
.iter()
.any(|call| call == "repo needed=true as_deps=true repo-dep"));
}
#[tokio::test]
async fn resume_from_aur_build_started_rebuilds_current_base() {
let _guard = transaction::test_env_lock();
let state = temp_dir("build-started-state");
std::env::set_var("XDG_STATE_HOME", &state);
std::env::remove_var("XDG_RUNTIME_DIR");
let build_dir = temp_dir("build-started-build");
let artifact = build_dir.join("foo.pkg.tar.zst");
std::fs::write(&artifact, b"pkg").unwrap();
let mut tx = tx_with_phase(
TransactionPhase::AurBuildStarted,
build_dir.clone(),
artifact.clone(),
);
tx.repo_deps.clear();
save_for_resume(&tx).await;
let tools = FakeBackend::new(build_dir.clone());
*tools.artifacts.lock().unwrap() = vec![artifact];
let code = resume_transaction(&options(), &tools, None, &mut tx)
.await
.unwrap();
assert_eq!(code, 0);
assert!(tools
.calls()
.iter()
.any(|call| call == &format!("build {}", build_dir.display())));
}
#[tokio::test]
async fn resume_from_aur_built_installs_existing_artifacts() {
let _guard = transaction::test_env_lock();
let state = temp_dir("aur-built-state");
std::env::set_var("XDG_STATE_HOME", &state);
std::env::remove_var("XDG_RUNTIME_DIR");
let build_dir = temp_dir("aur-built-build");
let artifact = build_dir.join("foo.pkg.tar.zst");
std::fs::write(&artifact, b"pkg").unwrap();
let mut tx = tx_with_phase(TransactionPhase::AurBuilt, build_dir, artifact.clone());
tx.repo_deps.clear();
save_for_resume(&tx).await;
let tools = FakeBackend::new(temp_dir("aur-built-tools"));
let code = resume_transaction(&options(), &tools, None, &mut tx)
.await
.unwrap();
assert_eq!(code, 0);
assert!(tools
.calls()
.iter()
.any(|call| call == &format!("local needed=true {}", artifact.display())));
}
#[tokio::test]
async fn resume_pending_current_aur_item_skips_rebuild() {
let _guard = transaction::test_env_lock();
let state = temp_dir("pending-current-state");
std::env::set_var("XDG_STATE_HOME", &state);
std::env::remove_var("XDG_RUNTIME_DIR");
let build_dir = temp_dir("pending-current-build");
let artifact = build_dir.join("foo.pkg.tar.zst");
let mut tx = tx_with_phase(TransactionPhase::AurPlanWritten, build_dir, artifact);
tx.repo_deps.clear();
save_for_resume(&tx).await;
let tools = FakeBackend::new(temp_dir("pending-current-tools"));
tools
.installed
.lock()
.unwrap()
.insert("foo".into(), "1-1".into());
let code = resume_transaction(&options(), &tools, None, &mut tx)
.await
.unwrap();
assert_eq!(code, 0);
assert!(tools.calls().is_empty());
}
#[tokio::test]
async fn resume_from_install_started_checks_installed_before_reinstall() {
let _guard = transaction::test_env_lock();
let state = temp_dir("install-started-state");
std::env::set_var("XDG_STATE_HOME", &state);
std::env::remove_var("XDG_RUNTIME_DIR");
let build_dir = temp_dir("install-started-build");
let artifact = build_dir.join("foo.pkg.tar.zst");
std::fs::write(&artifact, b"pkg").unwrap();
let mut tx = tx_with_phase(
TransactionPhase::AurInstallStarted,
build_dir,
artifact.clone(),
);
tx.repo_deps.clear();
save_for_resume(&tx).await;
let tools = FakeBackend::new(temp_dir("install-started-tools"));
tools
.installed
.lock()
.unwrap()
.insert("foo".into(), "1-1".into());
let code = resume_transaction(&options(), &tools, None, &mut tx)
.await
.unwrap();
assert_eq!(code, 0);
assert!(!tools
.calls()
.iter()
.any(|call| call.starts_with("local needed=true")));
}
#[tokio::test]
async fn resume_from_install_started_reruns_when_installed_version_is_old() {
let _guard = transaction::test_env_lock();
let state = temp_dir("install-old-state");
std::env::set_var("XDG_STATE_HOME", &state);
std::env::remove_var("XDG_RUNTIME_DIR");
let build_dir = temp_dir("install-old-build");
let artifact = build_dir.join("foo.pkg.tar.zst");
std::fs::write(&artifact, b"pkg").unwrap();
let mut tx = tx_with_phase(
TransactionPhase::AurInstallStarted,
build_dir,
artifact.clone(),
);
tx.repo_deps.clear();
save_for_resume(&tx).await;
let tools = FakeBackend::new(temp_dir("install-old-tools"));
tools
.installed
.lock()
.unwrap()
.insert("foo".into(), "0-1".into());
let code = resume_transaction(&options(), &tools, None, &mut tx)
.await
.unwrap();
assert_eq!(code, 0);
assert!(tools
.calls()
.iter()
.any(|call| call == &format!("local needed=true {}", artifact.display())));
}
#[tokio::test]
async fn resume_from_install_started_reruns_pacman_u_when_not_installed() {
let _guard = transaction::test_env_lock();
let state = temp_dir("install-rerun-state");
std::env::set_var("XDG_STATE_HOME", &state);
std::env::remove_var("XDG_RUNTIME_DIR");
let build_dir = temp_dir("install-rerun-build");
let artifact = build_dir.join("foo.pkg.tar.zst");
std::fs::write(&artifact, b"pkg").unwrap();
let mut tx = tx_with_phase(
TransactionPhase::AurInstallStarted,
build_dir,
artifact.clone(),
);
tx.repo_deps.clear();
save_for_resume(&tx).await;
let tools = FakeBackend::new(temp_dir("install-rerun-tools"));
let code = resume_transaction(&options(), &tools, None, &mut tx)
.await
.unwrap();
assert_eq!(code, 0);
assert!(tools
.calls()
.iter()
.any(|call| call == &format!("local needed=true {}", artifact.display())));
}