Compare commits
3
Commits
c00e6eae84
...
0693db94b2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0693db94b2
|
||
|
|
80c50c0db3
|
||
|
|
05cac57f3a
|
@@ -225,12 +225,16 @@ mod tests {
|
|||||||
assert_eq!(chunk_counts.get(&peers[0]), Some(&2));
|
assert_eq!(chunk_counts.get(&peers[0]), Some(&2));
|
||||||
assert_eq!(chunk_counts.get(&peers[1]), Some(&2));
|
assert_eq!(chunk_counts.get(&peers[1]), Some(&2));
|
||||||
|
|
||||||
let peer_a_bytes = byte_counts.get(&peers[0]).copied().unwrap_or_default();
|
let assigned_bytes = [
|
||||||
let peer_b_bytes = byte_counts.get(&peers[1]).copied().unwrap_or_default();
|
byte_counts.get(&peers[0]).copied().unwrap_or_default(),
|
||||||
assert_eq!(peer_a_bytes + peer_b_bytes, file_size);
|
byte_counts.get(&peers[1]).copied().unwrap_or_default(),
|
||||||
|
];
|
||||||
|
assert_eq!(assigned_bytes.iter().sum::<u64>(), file_size);
|
||||||
assert!(
|
assert!(
|
||||||
peer_a_bytes.abs_diff(peer_b_bytes) <= CHUNK_SIZE,
|
assigned_bytes[0].abs_diff(assigned_bytes[1]) <= CHUNK_SIZE,
|
||||||
"large file bytes should be balanced within one chunk: {peer_a_bytes} vs {peer_b_bytes}"
|
"large file bytes should be balanced within one chunk: {} vs {}",
|
||||||
|
assigned_bytes[0],
|
||||||
|
assigned_bytes[1]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1588,11 +1588,12 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn test_ctx(game_dir: PathBuf) -> Ctx {
|
fn test_ctx(game_dir: PathBuf) -> Ctx {
|
||||||
|
let state_dir = game_dir.join(".test-state");
|
||||||
Ctx::new(
|
Ctx::new(
|
||||||
Arc::new(RwLock::new(PeerGameDB::new())),
|
Arc::new(RwLock::new(PeerGameDB::new())),
|
||||||
"peer".to_string(),
|
"peer".to_string(),
|
||||||
game_dir.clone(),
|
game_dir,
|
||||||
game_dir.join(".test-state"),
|
state_dir,
|
||||||
Arc::new(FakeUnpacker),
|
Arc::new(FakeUnpacker),
|
||||||
CancellationToken::new(),
|
CancellationToken::new(),
|
||||||
TaskTracker::new(),
|
TaskTracker::new(),
|
||||||
@@ -1688,15 +1689,15 @@ mod tests {
|
|||||||
game
|
game
|
||||||
}
|
}
|
||||||
|
|
||||||
fn assert_active_update(event: PeerEvent, expected: Vec<ActiveOperation>) {
|
fn assert_active_update(event: PeerEvent, expected: &[ActiveOperation]) {
|
||||||
let PeerEvent::ActiveOperationsChanged { active_operations } = event else {
|
let PeerEvent::ActiveOperationsChanged { active_operations } = event else {
|
||||||
panic!("expected ActiveOperationsChanged");
|
panic!("expected ActiveOperationsChanged");
|
||||||
};
|
};
|
||||||
assert_eq!(active_operations, expected);
|
assert_eq!(active_operations, expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn active_update(id: &str, operation: ActiveOperationKind) -> Vec<ActiveOperation> {
|
fn active_update(id: &str, operation: ActiveOperationKind) -> [ActiveOperation; 1] {
|
||||||
vec![ActiveOperation {
|
[ActiveOperation {
|
||||||
id: id.to_string(),
|
id: id.to_string(),
|
||||||
operation,
|
operation,
|
||||||
}]
|
}]
|
||||||
@@ -1934,7 +1935,7 @@ mod tests {
|
|||||||
);
|
);
|
||||||
assert_active_update(
|
assert_active_update(
|
||||||
recv_event(&mut rx).await,
|
recv_event(&mut rx).await,
|
||||||
vec![ActiveOperation {
|
&[ActiveOperation {
|
||||||
id: "game".to_string(),
|
id: "game".to_string(),
|
||||||
operation: ActiveOperationKind::Updating,
|
operation: ActiveOperationKind::Updating,
|
||||||
}],
|
}],
|
||||||
@@ -1971,12 +1972,12 @@ mod tests {
|
|||||||
assert!(token.is_cancelled());
|
assert!(token.is_cancelled());
|
||||||
assert_active_update(
|
assert_active_update(
|
||||||
recv_event(&mut rx).await,
|
recv_event(&mut rx).await,
|
||||||
vec![ActiveOperation {
|
&[ActiveOperation {
|
||||||
id: "game".to_string(),
|
id: "game".to_string(),
|
||||||
operation: ActiveOperationKind::Updating,
|
operation: ActiveOperationKind::Updating,
|
||||||
}],
|
}],
|
||||||
);
|
);
|
||||||
assert_active_update(recv_event(&mut rx).await, Vec::new());
|
assert_active_update(recv_event(&mut rx).await, &[]);
|
||||||
assert!(
|
assert!(
|
||||||
!ctx.active_operations.read().await.contains_key("game"),
|
!ctx.active_operations.read().await.contains_key("game"),
|
||||||
"timed-out drain should not leave the operation stuck active"
|
"timed-out drain should not leave the operation stuck active"
|
||||||
@@ -2029,7 +2030,7 @@ mod tests {
|
|||||||
|
|
||||||
assert_active_update(
|
assert_active_update(
|
||||||
recv_event(&mut rx).await,
|
recv_event(&mut rx).await,
|
||||||
active_update("game", ActiveOperationKind::Updating),
|
&active_update("game", ActiveOperationKind::Updating),
|
||||||
);
|
);
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
recv_event(&mut rx).await,
|
recv_event(&mut rx).await,
|
||||||
@@ -2038,7 +2039,7 @@ mod tests {
|
|||||||
operation: InstallOperation::Updating
|
operation: InstallOperation::Updating
|
||||||
} if id == "game"
|
} if id == "game"
|
||||||
));
|
));
|
||||||
assert_active_update(recv_event(&mut rx).await, Vec::new());
|
assert_active_update(recv_event(&mut rx).await, &[]);
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
recv_event(&mut rx).await,
|
recv_event(&mut rx).await,
|
||||||
PeerEvent::InstallGameFinished { id } if id == "game"
|
PeerEvent::InstallGameFinished { id } if id == "game"
|
||||||
@@ -2060,7 +2061,7 @@ mod tests {
|
|||||||
|
|
||||||
assert_active_update(
|
assert_active_update(
|
||||||
recv_event(&mut rx).await,
|
recv_event(&mut rx).await,
|
||||||
active_update("game", ActiveOperationKind::Installing),
|
&active_update("game", ActiveOperationKind::Installing),
|
||||||
);
|
);
|
||||||
match recv_event(&mut rx).await {
|
match recv_event(&mut rx).await {
|
||||||
PeerEvent::InstallGameBegin { id, operation } => {
|
PeerEvent::InstallGameBegin { id, operation } => {
|
||||||
@@ -2070,7 +2071,7 @@ mod tests {
|
|||||||
_ => panic!("expected InstallGameBegin"),
|
_ => panic!("expected InstallGameBegin"),
|
||||||
}
|
}
|
||||||
assert_local_update(recv_event(&mut rx).await, true, true);
|
assert_local_update(recv_event(&mut rx).await, true, true);
|
||||||
assert_active_update(recv_event(&mut rx).await, Vec::new());
|
assert_active_update(recv_event(&mut rx).await, &[]);
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
recv_event(&mut rx).await,
|
recv_event(&mut rx).await,
|
||||||
PeerEvent::InstallGameFinished { id } if id == "game"
|
PeerEvent::InstallGameFinished { id } if id == "game"
|
||||||
@@ -2121,7 +2122,7 @@ mod tests {
|
|||||||
|
|
||||||
assert_active_update(
|
assert_active_update(
|
||||||
recv_event(&mut rx).await,
|
recv_event(&mut rx).await,
|
||||||
active_update("game", ActiveOperationKind::Installing),
|
&active_update("game", ActiveOperationKind::Installing),
|
||||||
);
|
);
|
||||||
match recv_event(&mut rx).await {
|
match recv_event(&mut rx).await {
|
||||||
PeerEvent::InstallGameBegin { id, operation } => {
|
PeerEvent::InstallGameBegin { id, operation } => {
|
||||||
@@ -2131,7 +2132,7 @@ mod tests {
|
|||||||
_ => panic!("expected InstallGameBegin"),
|
_ => panic!("expected InstallGameBegin"),
|
||||||
}
|
}
|
||||||
assert_local_update(recv_event(&mut rx).await, true, true);
|
assert_local_update(recv_event(&mut rx).await, true, true);
|
||||||
assert_active_update(recv_event(&mut rx).await, Vec::new());
|
assert_active_update(recv_event(&mut rx).await, &[]);
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
recv_event(&mut rx).await,
|
recv_event(&mut rx).await,
|
||||||
PeerEvent::InstallGameFinished { id } if id == "game"
|
PeerEvent::InstallGameFinished { id } if id == "game"
|
||||||
@@ -2181,7 +2182,7 @@ mod tests {
|
|||||||
|
|
||||||
assert_active_update(
|
assert_active_update(
|
||||||
recv_event(&mut rx).await,
|
recv_event(&mut rx).await,
|
||||||
active_update("game", ActiveOperationKind::Updating),
|
&active_update("game", ActiveOperationKind::Updating),
|
||||||
);
|
);
|
||||||
match recv_event(&mut rx).await {
|
match recv_event(&mut rx).await {
|
||||||
PeerEvent::InstallGameBegin { id, operation } => {
|
PeerEvent::InstallGameBegin { id, operation } => {
|
||||||
@@ -2191,7 +2192,7 @@ mod tests {
|
|||||||
_ => panic!("expected InstallGameBegin"),
|
_ => panic!("expected InstallGameBegin"),
|
||||||
}
|
}
|
||||||
assert_local_update(recv_event(&mut rx).await, true, true);
|
assert_local_update(recv_event(&mut rx).await, true, true);
|
||||||
assert_active_update(recv_event(&mut rx).await, Vec::new());
|
assert_active_update(recv_event(&mut rx).await, &[]);
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
recv_event(&mut rx).await,
|
recv_event(&mut rx).await,
|
||||||
PeerEvent::InstallGameFinished { id } if id == "game"
|
PeerEvent::InstallGameFinished { id } if id == "game"
|
||||||
@@ -2212,7 +2213,7 @@ mod tests {
|
|||||||
run_install_operation(&ctx, &tx, "game".to_string()).await;
|
run_install_operation(&ctx, &tx, "game".to_string()).await;
|
||||||
assert_active_update(
|
assert_active_update(
|
||||||
recv_event(&mut rx).await,
|
recv_event(&mut rx).await,
|
||||||
active_update("game", ActiveOperationKind::Installing),
|
&active_update("game", ActiveOperationKind::Installing),
|
||||||
);
|
);
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
recv_event(&mut rx).await,
|
recv_event(&mut rx).await,
|
||||||
@@ -2223,7 +2224,7 @@ mod tests {
|
|||||||
));
|
));
|
||||||
let game = local_update_game(recv_event(&mut rx).await, true, true);
|
let game = local_update_game(recv_event(&mut rx).await, true, true);
|
||||||
assert_eq!(game.local_version.as_deref(), Some("20240101"));
|
assert_eq!(game.local_version.as_deref(), Some("20240101"));
|
||||||
assert_active_update(recv_event(&mut rx).await, Vec::new());
|
assert_active_update(recv_event(&mut rx).await, &[]);
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
recv_event(&mut rx).await,
|
recv_event(&mut rx).await,
|
||||||
PeerEvent::InstallGameFinished { id } if id == "game"
|
PeerEvent::InstallGameFinished { id } if id == "game"
|
||||||
@@ -2235,7 +2236,7 @@ mod tests {
|
|||||||
run_install_operation(&ctx, &tx, "game".to_string()).await;
|
run_install_operation(&ctx, &tx, "game".to_string()).await;
|
||||||
assert_active_update(
|
assert_active_update(
|
||||||
recv_event(&mut rx).await,
|
recv_event(&mut rx).await,
|
||||||
active_update("game", ActiveOperationKind::Updating),
|
&active_update("game", ActiveOperationKind::Updating),
|
||||||
);
|
);
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
recv_event(&mut rx).await,
|
recv_event(&mut rx).await,
|
||||||
@@ -2246,7 +2247,7 @@ mod tests {
|
|||||||
));
|
));
|
||||||
let game = local_update_game(recv_event(&mut rx).await, true, true);
|
let game = local_update_game(recv_event(&mut rx).await, true, true);
|
||||||
assert_eq!(game.local_version.as_deref(), Some("20250101"));
|
assert_eq!(game.local_version.as_deref(), Some("20250101"));
|
||||||
assert_active_update(recv_event(&mut rx).await, Vec::new());
|
assert_active_update(recv_event(&mut rx).await, &[]);
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
recv_event(&mut rx).await,
|
recv_event(&mut rx).await,
|
||||||
PeerEvent::InstallGameFinished { id } if id == "game"
|
PeerEvent::InstallGameFinished { id } if id == "game"
|
||||||
@@ -2255,7 +2256,7 @@ mod tests {
|
|||||||
run_uninstall_operation(&ctx, &tx, "game".to_string()).await;
|
run_uninstall_operation(&ctx, &tx, "game".to_string()).await;
|
||||||
assert_active_update(
|
assert_active_update(
|
||||||
recv_event(&mut rx).await,
|
recv_event(&mut rx).await,
|
||||||
active_update("game", ActiveOperationKind::Uninstalling),
|
&active_update("game", ActiveOperationKind::Uninstalling),
|
||||||
);
|
);
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
recv_event(&mut rx).await,
|
recv_event(&mut rx).await,
|
||||||
@@ -2263,7 +2264,7 @@ mod tests {
|
|||||||
));
|
));
|
||||||
let game = local_update_game(recv_event(&mut rx).await, false, true);
|
let game = local_update_game(recv_event(&mut rx).await, false, true);
|
||||||
assert_eq!(game.local_version.as_deref(), Some("20250101"));
|
assert_eq!(game.local_version.as_deref(), Some("20250101"));
|
||||||
assert_active_update(recv_event(&mut rx).await, Vec::new());
|
assert_active_update(recv_event(&mut rx).await, &[]);
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
recv_event(&mut rx).await,
|
recv_event(&mut rx).await,
|
||||||
PeerEvent::UninstallGameFinished { id } if id == "game"
|
PeerEvent::UninstallGameFinished { id } if id == "game"
|
||||||
@@ -2286,14 +2287,14 @@ mod tests {
|
|||||||
|
|
||||||
assert_active_update(
|
assert_active_update(
|
||||||
recv_event(&mut rx).await,
|
recv_event(&mut rx).await,
|
||||||
active_update("game", ActiveOperationKind::Uninstalling),
|
&active_update("game", ActiveOperationKind::Uninstalling),
|
||||||
);
|
);
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
recv_event(&mut rx).await,
|
recv_event(&mut rx).await,
|
||||||
PeerEvent::UninstallGameBegin { id } if id == "game"
|
PeerEvent::UninstallGameBegin { id } if id == "game"
|
||||||
));
|
));
|
||||||
assert_local_update(recv_event(&mut rx).await, false, true);
|
assert_local_update(recv_event(&mut rx).await, false, true);
|
||||||
assert_active_update(recv_event(&mut rx).await, Vec::new());
|
assert_active_update(recv_event(&mut rx).await, &[]);
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
recv_event(&mut rx).await,
|
recv_event(&mut rx).await,
|
||||||
PeerEvent::UninstallGameFinished { id } if id == "game"
|
PeerEvent::UninstallGameFinished { id } if id == "game"
|
||||||
@@ -2321,7 +2322,7 @@ mod tests {
|
|||||||
|
|
||||||
assert_active_update(
|
assert_active_update(
|
||||||
recv_event(&mut rx).await,
|
recv_event(&mut rx).await,
|
||||||
active_update("game", ActiveOperationKind::RemovingDownload),
|
&active_update("game", ActiveOperationKind::RemovingDownload),
|
||||||
);
|
);
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
recv_event(&mut rx).await,
|
recv_event(&mut rx).await,
|
||||||
@@ -2331,7 +2332,7 @@ mod tests {
|
|||||||
panic!("expected LocalLibraryChanged");
|
panic!("expected LocalLibraryChanged");
|
||||||
};
|
};
|
||||||
assert!(games.is_empty());
|
assert!(games.is_empty());
|
||||||
assert_active_update(recv_event(&mut rx).await, Vec::new());
|
assert_active_update(recv_event(&mut rx).await, &[]);
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
recv_event(&mut rx).await,
|
recv_event(&mut rx).await,
|
||||||
PeerEvent::RemoveDownloadedGameFinished { id } if id == "game"
|
PeerEvent::RemoveDownloadedGameFinished { id } if id == "game"
|
||||||
|
|||||||
@@ -1044,53 +1044,8 @@ mod tests {
|
|||||||
const INSTALLING_PAYLOAD: &[u8] = b"installing";
|
const INSTALLING_PAYLOAD: &[u8] = b"installing";
|
||||||
const BACKUP_PAYLOAD: &[u8] = b"backup";
|
const BACKUP_PAYLOAD: &[u8] = b"backup";
|
||||||
|
|
||||||
fn seed_recovery_case(root: &Path, case: &RecoveryCase) {
|
fn recovery_cases() -> [RecoveryCase; 10] {
|
||||||
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_covers_install_matrix_rows() {
|
|
||||||
let cases = [
|
|
||||||
RecoveryCase {
|
RecoveryCase {
|
||||||
name: "installing_commit_landed",
|
name: "installing_commit_landed",
|
||||||
intent_state: InstallIntentState::Installing,
|
intent_state: InstallIntentState::Installing,
|
||||||
@@ -1171,27 +1126,71 @@ mod tests {
|
|||||||
has_backup: false,
|
has_backup: false,
|
||||||
expected_local_payload: None,
|
expected_local_payload: None,
|
||||||
},
|
},
|
||||||
];
|
]
|
||||||
|
}
|
||||||
|
|
||||||
let state = test_state();
|
fn seed_recovery_case(root: &Path, case: &RecoveryCase) {
|
||||||
for case in cases {
|
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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn assert_recovery_case(state_dir: &Path, case: RecoveryCase) {
|
||||||
let temp = TempDir::new("lanspread-install");
|
let temp = TempDir::new("lanspread-install");
|
||||||
let root = temp.game_root();
|
let root = temp.game_root();
|
||||||
seed_recovery_case(&root, &case);
|
seed_recovery_case(&root, &case);
|
||||||
write_intent(
|
write_intent(
|
||||||
state.path(),
|
state_dir,
|
||||||
"game",
|
"game",
|
||||||
&InstallIntent::new("game", case.intent_state.clone(), Some("20250101".into())),
|
&InstallIntent::new("game", case.intent_state.clone(), Some("20250101".into())),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap_or_else(|err| panic!("{} intent should be written: {err}", case.name));
|
.unwrap_or_else(|err| panic!("{} intent should be written: {err}", case.name));
|
||||||
|
|
||||||
recover_game_root(&root, state.path(), "game")
|
recover_game_root(&root, state_dir, "game")
|
||||||
.await
|
.await
|
||||||
.unwrap_or_else(|err| panic!("{} recovery should succeed: {err}", case.name));
|
.unwrap_or_else(|err| panic!("{} recovery should succeed: {err}", case.name));
|
||||||
|
|
||||||
assert_recovered_case(&root, &case);
|
assert_recovered_case(&root, &case);
|
||||||
let intent = read_intent(state.path(), "game").await;
|
let intent = read_intent(state_dir, "game").await;
|
||||||
assert_eq!(intent.state, InstallIntentState::None, "{}", case.name);
|
assert_eq!(intent.state, InstallIntentState::None, "{}", case.name);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
intent.eti_version.as_deref(),
|
intent.eti_version.as_deref(),
|
||||||
@@ -1200,6 +1199,13 @@ mod tests {
|
|||||||
case.name
|
case.name
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn recovery_covers_install_matrix_rows() {
|
||||||
|
let state = test_state();
|
||||||
|
for case in recovery_cases() {
|
||||||
|
assert_recovery_case(state.path(), case).await;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|||||||
@@ -374,11 +374,12 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn test_ctx(game_dir: PathBuf, catalog: GameCatalog) -> Ctx {
|
fn test_ctx(game_dir: PathBuf, catalog: GameCatalog) -> Ctx {
|
||||||
|
let state_dir = game_dir.join(".test-state");
|
||||||
Ctx::new(
|
Ctx::new(
|
||||||
Arc::new(RwLock::new(PeerGameDB::new())),
|
Arc::new(RwLock::new(PeerGameDB::new())),
|
||||||
"peer".to_string(),
|
"peer".to_string(),
|
||||||
game_dir.clone(),
|
game_dir,
|
||||||
game_dir.join(".test-state"),
|
state_dir,
|
||||||
Arc::new(NoopUnpacker),
|
Arc::new(NoopUnpacker),
|
||||||
CancellationToken::new(),
|
CancellationToken::new(),
|
||||||
TaskTracker::new(),
|
TaskTracker::new(),
|
||||||
@@ -388,8 +389,8 @@ mod tests {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn watch_event(path: PathBuf) -> notify::Result<Event> {
|
fn watch_event(path: PathBuf) -> Event {
|
||||||
Ok(Event::new(EventKind::Any).add_path(path))
|
Event::new(EventKind::Any).add_path(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn recv_local_update(
|
async fn recv_local_update(
|
||||||
@@ -460,7 +461,7 @@ mod tests {
|
|||||||
&ctx,
|
&ctx,
|
||||||
&tx,
|
&tx,
|
||||||
&gate,
|
&gate,
|
||||||
watch_event(temp.path().join("game").join("version.ini")),
|
Ok(watch_event(temp.path().join("game").join("version.ini"))),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
ctx.task_tracker.close();
|
ctx.task_tracker.close();
|
||||||
|
|||||||
@@ -479,11 +479,12 @@ mod tests {
|
|||||||
|
|
||||||
fn test_ctx(game_dir: PathBuf, catalog: GameCatalog) -> PeerCtx {
|
fn test_ctx(game_dir: PathBuf, catalog: GameCatalog) -> PeerCtx {
|
||||||
let (tx_notify_ui, _rx) = mpsc::unbounded_channel();
|
let (tx_notify_ui, _rx) = mpsc::unbounded_channel();
|
||||||
|
let state_dir = game_dir.join(".test-state");
|
||||||
Ctx::new(
|
Ctx::new(
|
||||||
Arc::new(RwLock::new(PeerGameDB::new())),
|
Arc::new(RwLock::new(PeerGameDB::new())),
|
||||||
"peer".to_string(),
|
"peer".to_string(),
|
||||||
game_dir.clone(),
|
game_dir,
|
||||||
game_dir.join(".test-state"),
|
state_dir,
|
||||||
Arc::new(NoopUnpacker),
|
Arc::new(NoopUnpacker),
|
||||||
CancellationToken::new(),
|
CancellationToken::new(),
|
||||||
TaskTracker::new(),
|
TaskTracker::new(),
|
||||||
|
|||||||
@@ -838,7 +838,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn parses_unrar_technical_listing() {
|
fn parses_unrar_technical_listing() {
|
||||||
let listing = parse_unrar_listing(
|
let listing = parse_unrar_listing(
|
||||||
r#"
|
r"
|
||||||
Archive: game.eti
|
Archive: game.eti
|
||||||
Details: RAR 5, solid
|
Details: RAR 5, solid
|
||||||
|
|
||||||
@@ -849,7 +849,7 @@ Details: RAR 5, solid
|
|||||||
|
|
||||||
Name: bin
|
Name: bin
|
||||||
Type: Directory
|
Type: Directory
|
||||||
"#,
|
",
|
||||||
)
|
)
|
||||||
.expect("listing should parse");
|
.expect("listing should parse");
|
||||||
|
|
||||||
@@ -876,14 +876,14 @@ Details: RAR 5, solid
|
|||||||
#[test]
|
#[test]
|
||||||
fn rejects_unrar_file_entries_without_crc32() {
|
fn rejects_unrar_file_entries_without_crc32() {
|
||||||
let err = parse_unrar_listing(
|
let err = parse_unrar_listing(
|
||||||
r#"
|
r"
|
||||||
Archive: game.eti
|
Archive: game.eti
|
||||||
Details: RAR 5
|
Details: RAR 5
|
||||||
|
|
||||||
Name: bin/payload.bin
|
Name: bin/payload.bin
|
||||||
Type: File
|
Type: File
|
||||||
Size: 123
|
Size: 123
|
||||||
"#,
|
",
|
||||||
)
|
)
|
||||||
.expect_err("file entries without CRC32 should be rejected");
|
.expect_err("file entries without CRC32 should be rejected");
|
||||||
|
|
||||||
@@ -893,14 +893,14 @@ Details: RAR 5
|
|||||||
#[test]
|
#[test]
|
||||||
fn accepts_zero_size_unrar_file_entries_without_crc32() {
|
fn accepts_zero_size_unrar_file_entries_without_crc32() {
|
||||||
let listing = parse_unrar_listing(
|
let listing = parse_unrar_listing(
|
||||||
r#"
|
r"
|
||||||
Archive: game.eti
|
Archive: game.eti
|
||||||
Details: RAR 5
|
Details: RAR 5
|
||||||
|
|
||||||
Name: bin/empty.cfg
|
Name: bin/empty.cfg
|
||||||
Type: File
|
Type: File
|
||||||
Size: 0
|
Size: 0
|
||||||
"#,
|
",
|
||||||
)
|
)
|
||||||
.expect("empty file without CRC32 should parse as CRC32 zero");
|
.expect("empty file without CRC32 should parse as CRC32 zero");
|
||||||
|
|
||||||
@@ -918,15 +918,11 @@ Details: RAR 5
|
|||||||
#[test]
|
#[test]
|
||||||
fn sender_archive_integrity_accepts_matching_size_and_crc32() {
|
fn sender_archive_integrity_accepts_matching_size_and_crc32() {
|
||||||
let bytes = b"payload";
|
let bytes = b"payload";
|
||||||
let integrity =
|
let byte_len = u64::try_from(bytes.len()).expect("test payload length should fit in u64");
|
||||||
SenderArchiveIntegrity::new(u64::try_from(bytes.len()).unwrap(), crc32_of(bytes));
|
let integrity = SenderArchiveIntegrity::new(byte_len, crc32_of(bytes));
|
||||||
|
|
||||||
integrity
|
integrity
|
||||||
.verify(
|
.verify("bin/payload.bin", byte_len, crc32_of(bytes))
|
||||||
"bin/payload.bin",
|
|
||||||
u64::try_from(bytes.len()).unwrap(),
|
|
||||||
crc32_of(bytes),
|
|
||||||
)
|
|
||||||
.expect("matching sender archive metadata should verify");
|
.expect("matching sender archive metadata should verify");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2266,6 +2266,82 @@ fn handle_download_finished(app_handle: &AppHandle, id: String) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::missing_panics_doc)]
|
||||||
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
|
pub fn run() {
|
||||||
|
// channel to receive events from the peer
|
||||||
|
let (tx_peer_event, rx_peer_event) = tokio::sync::mpsc::unbounded_channel::<PeerEvent>();
|
||||||
|
|
||||||
|
tauri::Builder::default()
|
||||||
|
.plugin(tauri_plugin_store::Builder::new().build())
|
||||||
|
.plugin(tauri_plugin_dialog::init())
|
||||||
|
.plugin(tauri_plugin_shell::init())
|
||||||
|
.invoke_handler(tauri::generate_handler![
|
||||||
|
request_games,
|
||||||
|
install_game,
|
||||||
|
stream_install_game,
|
||||||
|
run_game,
|
||||||
|
start_server,
|
||||||
|
game_directory_exists,
|
||||||
|
update_game_directory,
|
||||||
|
update_game,
|
||||||
|
uninstall_game,
|
||||||
|
remove_downloaded_game,
|
||||||
|
cancel_download,
|
||||||
|
open_game_files,
|
||||||
|
get_peer_count,
|
||||||
|
get_game_thumbnail,
|
||||||
|
get_unpack_logs,
|
||||||
|
get_main_logs
|
||||||
|
])
|
||||||
|
.manage(LanSpreadState::default())
|
||||||
|
.manage(PeerEventTx(tx_peer_event))
|
||||||
|
.setup(move |app| {
|
||||||
|
let state_dir = app.path().app_data_dir()?;
|
||||||
|
std::fs::create_dir_all(&state_dir)?;
|
||||||
|
let main_log_sink = MainLogSink::new(app.handle().clone(), main_log_path(&state_dir));
|
||||||
|
let state = app.state::<LanSpreadState>();
|
||||||
|
if state.main_log_sink.set(main_log_sink.clone()).is_err() {
|
||||||
|
log::warn!("main log sink was already initialized");
|
||||||
|
}
|
||||||
|
init_main_logging(main_log_sink)?;
|
||||||
|
let unpack_logs = load_unpack_logs(&state_dir);
|
||||||
|
tauri::async_runtime::block_on(async {
|
||||||
|
*state.unpack_logs.write().await = unpack_logs;
|
||||||
|
});
|
||||||
|
if state.state_dir.set(state_dir).is_err() {
|
||||||
|
log::warn!("app state directory was already initialized");
|
||||||
|
}
|
||||||
|
spawn_peer_event_loop(app.handle().clone(), rx_peer_event);
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.build(tauri::generate_context!())
|
||||||
|
.expect("error while building tauri application")
|
||||||
|
.run(|app_handle, event| {
|
||||||
|
if matches!(event, tauri::RunEvent::Exit) {
|
||||||
|
shutdown_peer_runtime(app_handle);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn shutdown_peer_runtime(app_handle: &AppHandle) {
|
||||||
|
let state = app_handle.state::<LanSpreadState>();
|
||||||
|
let peer_runtime = state.peer_runtime.clone();
|
||||||
|
|
||||||
|
tauri::async_runtime::block_on(async move {
|
||||||
|
let Some(mut handle) = peer_runtime.write().await.take() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
handle.shutdown();
|
||||||
|
if tokio::time::timeout(std::time::Duration::from_secs(2), handle.wait_stopped())
|
||||||
|
.await
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
log::warn!("Peer runtime did not stop within 2s of shutdown request");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -2430,7 +2506,7 @@ mod tests {
|
|||||||
trim_main_log_file_to_limit(&path, 21).expect("main log should trim");
|
trim_main_log_file_to_limit(&path, 21).expect("main log should trim");
|
||||||
|
|
||||||
let trimmed = std::fs::read_to_string(&path).expect("trimmed log should remain utf-8");
|
let trimmed = std::fs::read_to_string(&path).expect("trimmed log should remain utf-8");
|
||||||
assert!(trimmed.as_bytes().len() <= 21);
|
assert!(trimmed.len() <= 21);
|
||||||
assert!(trimmed.starts_with('é'));
|
assert!(trimmed.starts_with('é'));
|
||||||
assert!(trimmed.ends_with('é'));
|
assert!(trimmed.ends_with('é'));
|
||||||
|
|
||||||
@@ -2735,79 +2811,3 @@ mod tests {
|
|||||||
assert!(game_db.get_game_by_id("unknown").is_none());
|
assert!(game_db.get_game_by_id("unknown").is_none());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::missing_panics_doc)]
|
|
||||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
|
||||||
pub fn run() {
|
|
||||||
// channel to receive events from the peer
|
|
||||||
let (tx_peer_event, rx_peer_event) = tokio::sync::mpsc::unbounded_channel::<PeerEvent>();
|
|
||||||
|
|
||||||
tauri::Builder::default()
|
|
||||||
.plugin(tauri_plugin_store::Builder::new().build())
|
|
||||||
.plugin(tauri_plugin_dialog::init())
|
|
||||||
.plugin(tauri_plugin_shell::init())
|
|
||||||
.invoke_handler(tauri::generate_handler![
|
|
||||||
request_games,
|
|
||||||
install_game,
|
|
||||||
stream_install_game,
|
|
||||||
run_game,
|
|
||||||
start_server,
|
|
||||||
game_directory_exists,
|
|
||||||
update_game_directory,
|
|
||||||
update_game,
|
|
||||||
uninstall_game,
|
|
||||||
remove_downloaded_game,
|
|
||||||
cancel_download,
|
|
||||||
open_game_files,
|
|
||||||
get_peer_count,
|
|
||||||
get_game_thumbnail,
|
|
||||||
get_unpack_logs,
|
|
||||||
get_main_logs
|
|
||||||
])
|
|
||||||
.manage(LanSpreadState::default())
|
|
||||||
.manage(PeerEventTx(tx_peer_event))
|
|
||||||
.setup(move |app| {
|
|
||||||
let state_dir = app.path().app_data_dir()?;
|
|
||||||
std::fs::create_dir_all(&state_dir)?;
|
|
||||||
let main_log_sink = MainLogSink::new(app.handle().clone(), main_log_path(&state_dir));
|
|
||||||
let state = app.state::<LanSpreadState>();
|
|
||||||
if state.main_log_sink.set(main_log_sink.clone()).is_err() {
|
|
||||||
log::warn!("main log sink was already initialized");
|
|
||||||
}
|
|
||||||
init_main_logging(main_log_sink)?;
|
|
||||||
let unpack_logs = load_unpack_logs(&state_dir);
|
|
||||||
tauri::async_runtime::block_on(async {
|
|
||||||
*state.unpack_logs.write().await = unpack_logs;
|
|
||||||
});
|
|
||||||
if state.state_dir.set(state_dir).is_err() {
|
|
||||||
log::warn!("app state directory was already initialized");
|
|
||||||
}
|
|
||||||
spawn_peer_event_loop(app.handle().clone(), rx_peer_event);
|
|
||||||
Ok(())
|
|
||||||
})
|
|
||||||
.build(tauri::generate_context!())
|
|
||||||
.expect("error while building tauri application")
|
|
||||||
.run(|app_handle, event| {
|
|
||||||
if matches!(event, tauri::RunEvent::Exit) {
|
|
||||||
shutdown_peer_runtime(app_handle);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn shutdown_peer_runtime(app_handle: &AppHandle) {
|
|
||||||
let state = app_handle.state::<LanSpreadState>();
|
|
||||||
let peer_runtime = state.peer_runtime.clone();
|
|
||||||
|
|
||||||
tauri::async_runtime::block_on(async move {
|
|
||||||
let Some(mut handle) = peer_runtime.write().await.take() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
handle.shutdown();
|
|
||||||
if tokio::time::timeout(std::time::Duration::from_secs(2), handle.wait_stopped())
|
|
||||||
.await
|
|
||||||
.is_err()
|
|
||||||
{
|
|
||||||
log::warn!("Peer runtime did not stop within 2s of shutdown request");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -23,16 +23,16 @@ fmt:
|
|||||||
just --fmt
|
just --fmt
|
||||||
|
|
||||||
_fix:
|
_fix:
|
||||||
cargo fix
|
cargo fix --workspace --all-targets --all-features
|
||||||
cargo clippy --fix
|
cargo clippy --workspace --all-targets --all-features --fix
|
||||||
|
|
||||||
fix: _fix fmt
|
fix: _fix fmt
|
||||||
|
|
||||||
clippy:
|
clippy:
|
||||||
cargo clippy
|
cargo clippy --workspace --all-targets --all-features -- -D warnings
|
||||||
|
|
||||||
test:
|
test:
|
||||||
cargo test --workspace
|
cargo test --workspace --all-targets --all-features
|
||||||
|
|
||||||
frontend-test:
|
frontend-test:
|
||||||
cd crates/lanspread-tauri-deno-ts && deno test --unstable-sloppy-imports tests
|
cd crates/lanspread-tauri-deno-ts && deno test --unstable-sloppy-imports tests
|
||||||
|
|||||||
Reference in New Issue
Block a user