test(peer): cover install recovery matrix
FOLLOW_UP_2.md called out that recovery only covered one intent-driven update row. Replace that single-case assertion with a table over the ten recovery rows documented in PLAN.md, spanning Installing, Updating, and Uninstalling intents across local, staging, and backup directory states. The cases intentionally use markerless reserved directories while an intent is present. That pins the contract that the intent log proves Lanspread ownership during crash recovery, including the crash windows before ownership markers are dropped. The test still keeps the existing None-intent markerless case separate so user-owned reserved names remain protected. Running the larger table in parallel exposed that this module's TempDir helper could collide on pid plus timestamp paths. Add a local atomic suffix so these tests stop deleting each other's directories without doing the broader helper consolidation reserved for the later hygiene phase. Test Plan: - git diff --check - just fmt - just clippy - just test Follow-up-Plan: FOLLOW_UP_2.md
This commit is contained in:
@@ -464,7 +464,11 @@ mod tests {
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
path::{Path, PathBuf},
|
||||
sync::{Arc, Mutex},
|
||||
sync::{
|
||||
Arc,
|
||||
Mutex,
|
||||
atomic::{AtomicU64, Ordering},
|
||||
},
|
||||
};
|
||||
|
||||
use super::*;
|
||||
@@ -503,12 +507,16 @@ mod tests {
|
||||
|
||||
struct TempDir(PathBuf);
|
||||
|
||||
static NEXT_TEMP_ID: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
impl TempDir {
|
||||
fn new() -> Self {
|
||||
let mut path = std::env::temp_dir();
|
||||
let unique_id = NEXT_TEMP_ID.fetch_add(1, Ordering::Relaxed);
|
||||
path.push(format!(
|
||||
"lanspread-install-{}-{}",
|
||||
"lanspread-install-{}-{}-{}",
|
||||
std::process::id(),
|
||||
unique_id,
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
@@ -637,38 +645,175 @@ mod tests {
|
||||
assert!(root.join("version.ini").is_file());
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct RecoveryCase {
|
||||
name: &'static str,
|
||||
intent_state: InstallIntentState,
|
||||
has_local: bool,
|
||||
has_installing: bool,
|
||||
has_backup: bool,
|
||||
expected_local_payload: Option<&'static [u8]>,
|
||||
}
|
||||
|
||||
const LOCAL_PAYLOAD: &[u8] = b"local";
|
||||
const INSTALLING_PAYLOAD: &[u8] = b"installing";
|
||||
const BACKUP_PAYLOAD: &[u8] = b"backup";
|
||||
|
||||
fn seed_recovery_case(root: &Path, case: &RecoveryCase) {
|
||||
write_file(&root.join("version.ini"), b"20250101");
|
||||
if case.has_local {
|
||||
write_file(&root.join(LOCAL_DIR).join("payload.txt"), LOCAL_PAYLOAD);
|
||||
}
|
||||
if case.has_installing {
|
||||
write_file(
|
||||
&root.join(INSTALLING_DIR).join("payload.txt"),
|
||||
INSTALLING_PAYLOAD,
|
||||
);
|
||||
}
|
||||
if case.has_backup {
|
||||
write_file(&root.join(BACKUP_DIR).join("payload.txt"), BACKUP_PAYLOAD);
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_recovered_case(root: &Path, case: &RecoveryCase) {
|
||||
let local_payload = root.join(LOCAL_DIR).join("payload.txt");
|
||||
match case.expected_local_payload {
|
||||
Some(expected) => assert_eq!(
|
||||
std::fs::read(&local_payload)
|
||||
.unwrap_or_else(|err| panic!("{} local payload: {err}", case.name)),
|
||||
expected,
|
||||
"{} local payload",
|
||||
case.name
|
||||
),
|
||||
None => assert!(
|
||||
!root.join(LOCAL_DIR).exists(),
|
||||
"{} local dir should be absent",
|
||||
case.name
|
||||
),
|
||||
}
|
||||
assert!(
|
||||
!root.join(INSTALLING_DIR).exists(),
|
||||
"{} installing dir should be absent",
|
||||
case.name
|
||||
);
|
||||
assert!(
|
||||
!root.join(BACKUP_DIR).exists(),
|
||||
"{} backup dir should be absent",
|
||||
case.name
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn recovery_restores_backup_for_interrupted_update() {
|
||||
async fn recovery_covers_install_matrix_rows() {
|
||||
let cases = [
|
||||
RecoveryCase {
|
||||
name: "installing_commit_landed",
|
||||
intent_state: InstallIntentState::Installing,
|
||||
has_local: true,
|
||||
has_installing: false,
|
||||
has_backup: false,
|
||||
expected_local_payload: Some(LOCAL_PAYLOAD),
|
||||
},
|
||||
RecoveryCase {
|
||||
name: "installing_staging_only",
|
||||
intent_state: InstallIntentState::Installing,
|
||||
has_local: false,
|
||||
has_installing: true,
|
||||
has_backup: false,
|
||||
expected_local_payload: None,
|
||||
},
|
||||
RecoveryCase {
|
||||
name: "installing_before_staging",
|
||||
intent_state: InstallIntentState::Installing,
|
||||
has_local: false,
|
||||
has_installing: false,
|
||||
has_backup: false,
|
||||
expected_local_payload: None,
|
||||
},
|
||||
RecoveryCase {
|
||||
name: "updating_restore_backup",
|
||||
intent_state: InstallIntentState::Updating,
|
||||
has_local: false,
|
||||
has_installing: true,
|
||||
has_backup: true,
|
||||
expected_local_payload: Some(BACKUP_PAYLOAD),
|
||||
},
|
||||
RecoveryCase {
|
||||
name: "updating_commit_landed_with_stale_dirs",
|
||||
intent_state: InstallIntentState::Updating,
|
||||
has_local: true,
|
||||
has_installing: true,
|
||||
has_backup: true,
|
||||
expected_local_payload: Some(LOCAL_PAYLOAD),
|
||||
},
|
||||
RecoveryCase {
|
||||
name: "updating_commit_landed_before_backup_cleanup",
|
||||
intent_state: InstallIntentState::Updating,
|
||||
has_local: true,
|
||||
has_installing: false,
|
||||
has_backup: true,
|
||||
expected_local_payload: Some(LOCAL_PAYLOAD),
|
||||
},
|
||||
RecoveryCase {
|
||||
name: "updating_commit_and_cleanup_landed",
|
||||
intent_state: InstallIntentState::Updating,
|
||||
has_local: true,
|
||||
has_installing: false,
|
||||
has_backup: false,
|
||||
expected_local_payload: Some(LOCAL_PAYLOAD),
|
||||
},
|
||||
RecoveryCase {
|
||||
name: "uninstalling_delete_backup",
|
||||
intent_state: InstallIntentState::Uninstalling,
|
||||
has_local: false,
|
||||
has_installing: false,
|
||||
has_backup: true,
|
||||
expected_local_payload: None,
|
||||
},
|
||||
RecoveryCase {
|
||||
name: "uninstalling_already_clean",
|
||||
intent_state: InstallIntentState::Uninstalling,
|
||||
has_local: false,
|
||||
has_installing: false,
|
||||
has_backup: false,
|
||||
expected_local_payload: None,
|
||||
},
|
||||
RecoveryCase {
|
||||
name: "uninstalling_before_fs_work",
|
||||
intent_state: InstallIntentState::Uninstalling,
|
||||
has_local: true,
|
||||
has_installing: false,
|
||||
has_backup: false,
|
||||
expected_local_payload: None,
|
||||
},
|
||||
];
|
||||
|
||||
for case in cases {
|
||||
let temp = TempDir::new();
|
||||
let root = temp.game_root();
|
||||
write_file(&root.join("version.ini"), b"20250101");
|
||||
write_file(&root.join(".local.backup").join("old.txt"), b"old");
|
||||
write_file(&root.join(".local.installing").join("new.txt"), b"new");
|
||||
write_file(&root.join(".local.backup").join(OWNED_MARKER), b"");
|
||||
write_file(&root.join(".local.installing").join(OWNED_MARKER), b"");
|
||||
seed_recovery_case(&root, &case);
|
||||
write_intent(
|
||||
&root,
|
||||
&InstallIntent::new(
|
||||
"game",
|
||||
InstallIntentState::Updating,
|
||||
Some("20250101".into()),
|
||||
),
|
||||
&InstallIntent::new("game", case.intent_state.clone(), Some("20250101".into())),
|
||||
)
|
||||
.await
|
||||
.expect("intent should be written");
|
||||
.unwrap_or_else(|err| panic!("{} intent should be written: {err}", case.name));
|
||||
|
||||
recover_game_root(&root, "game")
|
||||
.await
|
||||
.expect("recovery should succeed");
|
||||
.unwrap_or_else(|err| panic!("{} recovery should succeed: {err}", case.name));
|
||||
|
||||
assert!(root.join("local").join("old.txt").is_file());
|
||||
assert!(!root.join(".local.installing").exists());
|
||||
assert!(!root.join(".local.backup").exists());
|
||||
assert_recovered_case(&root, &case);
|
||||
let intent = read_intent(&root, "game").await;
|
||||
assert_eq!(intent.state, InstallIntentState::None, "{}", case.name);
|
||||
assert_eq!(
|
||||
read_intent(&root, "game").await.state,
|
||||
InstallIntentState::None
|
||||
intent.eti_version.as_deref(),
|
||||
Some("20250101"),
|
||||
"{}",
|
||||
case.name
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn none_recovery_leaves_markerless_reserved_dirs_untouched() {
|
||||
|
||||
Reference in New Issue
Block a user