fix(peer): settle local state before clearing operations
Install, update, uninstall, and downloaded-file removal used to clear the active operation before publishing the settled local-library snapshot. That allowed the UI bridge to emit a snapshot with no active operation but stale local state, which could briefly make an installing game look not installed. Refresh the ending game while its operation is still active, but exempt only that game from the active-operation freeze. Other active games keep the existing scan-preservation behavior. Lifecycle finished/failed events are now emitted after the local snapshot and active-operation clear, so the status snapshot remains the source of truth. Test Plan: - git diff --check - just fmt - just test Refs: local install/download status snapshot cleanup
This commit is contained in:
@@ -508,28 +508,33 @@ async fn run_started_install_operation(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
end_operation(ctx, tx_notify_ui, &id).await;
|
|
||||||
operation_guard.disarm();
|
|
||||||
|
|
||||||
match result {
|
match result {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
|
if let Err(err) = refresh_local_game_for_ending_operation(ctx, tx_notify_ui, &id).await
|
||||||
|
{
|
||||||
|
log::error!("Failed to refresh local library after install: {err}");
|
||||||
|
}
|
||||||
|
end_operation(ctx, tx_notify_ui, &id).await;
|
||||||
|
operation_guard.disarm();
|
||||||
events::send(
|
events::send(
|
||||||
tx_notify_ui,
|
tx_notify_ui,
|
||||||
PeerEvent::InstallGameFinished { id: id.clone() },
|
PeerEvent::InstallGameFinished { id: id.clone() },
|
||||||
);
|
);
|
||||||
if let Err(err) = refresh_local_game(ctx, tx_notify_ui, &id).await {
|
|
||||||
log::error!("Failed to refresh local library after install: {err}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::error!("Install operation failed for {id}: {err}");
|
log::error!("Install operation failed for {id}: {err}");
|
||||||
|
if let Err(refresh_err) =
|
||||||
|
refresh_local_game_for_ending_operation(ctx, tx_notify_ui, &id).await
|
||||||
|
{
|
||||||
|
log::error!("Failed to refresh local library after install failure: {refresh_err}");
|
||||||
|
}
|
||||||
|
end_operation(ctx, tx_notify_ui, &id).await;
|
||||||
|
operation_guard.disarm();
|
||||||
events::send(
|
events::send(
|
||||||
tx_notify_ui,
|
tx_notify_ui,
|
||||||
PeerEvent::InstallGameFailed { id: id.clone() },
|
PeerEvent::InstallGameFailed { id: id.clone() },
|
||||||
);
|
);
|
||||||
if let Err(refresh_err) = refresh_local_game(ctx, tx_notify_ui, &id).await {
|
|
||||||
log::error!("Failed to refresh local library after install failure: {refresh_err}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -559,11 +564,15 @@ async fn run_uninstall_operation(ctx: &Ctx, tx_notify_ui: &UnboundedSender<PeerE
|
|||||||
|
|
||||||
install::uninstall(&game_root, &id).await
|
install::uninstall(&game_root, &id).await
|
||||||
};
|
};
|
||||||
end_operation(ctx, tx_notify_ui, &id).await;
|
|
||||||
operation_guard.disarm();
|
|
||||||
|
|
||||||
match result {
|
match result {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
|
if let Err(err) = refresh_local_game_for_ending_operation(ctx, tx_notify_ui, &id).await
|
||||||
|
{
|
||||||
|
log::error!("Failed to refresh local library after uninstall: {err}");
|
||||||
|
}
|
||||||
|
end_operation(ctx, tx_notify_ui, &id).await;
|
||||||
|
operation_guard.disarm();
|
||||||
events::send(
|
events::send(
|
||||||
tx_notify_ui,
|
tx_notify_ui,
|
||||||
PeerEvent::UninstallGameFinished { id: id.clone() },
|
PeerEvent::UninstallGameFinished { id: id.clone() },
|
||||||
@@ -571,16 +580,21 @@ async fn run_uninstall_operation(ctx: &Ctx, tx_notify_ui: &UnboundedSender<PeerE
|
|||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::error!("Uninstall operation failed for {id}: {err}");
|
log::error!("Uninstall operation failed for {id}: {err}");
|
||||||
|
if let Err(refresh_err) =
|
||||||
|
refresh_local_game_for_ending_operation(ctx, tx_notify_ui, &id).await
|
||||||
|
{
|
||||||
|
log::error!(
|
||||||
|
"Failed to refresh local library after uninstall failure: {refresh_err}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
end_operation(ctx, tx_notify_ui, &id).await;
|
||||||
|
operation_guard.disarm();
|
||||||
events::send(
|
events::send(
|
||||||
tx_notify_ui,
|
tx_notify_ui,
|
||||||
PeerEvent::UninstallGameFailed { id: id.clone() },
|
PeerEvent::UninstallGameFailed { id: id.clone() },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Err(err) = refresh_local_game(ctx, tx_notify_ui, &id).await {
|
|
||||||
log::error!("Failed to refresh local library after uninstall: {err}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn run_remove_downloaded_operation(
|
async fn run_remove_downloaded_operation(
|
||||||
@@ -612,11 +626,15 @@ async fn run_remove_downloaded_operation(
|
|||||||
|
|
||||||
install::remove_downloaded(&game_dir, &id).await
|
install::remove_downloaded(&game_dir, &id).await
|
||||||
};
|
};
|
||||||
end_operation(ctx, tx_notify_ui, &id).await;
|
|
||||||
operation_guard.disarm();
|
|
||||||
|
|
||||||
match result {
|
match result {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
|
if let Err(err) = refresh_local_game_for_ending_operation(ctx, tx_notify_ui, &id).await
|
||||||
|
{
|
||||||
|
log::error!("Failed to refresh local library after downloaded-file removal: {err}");
|
||||||
|
}
|
||||||
|
end_operation(ctx, tx_notify_ui, &id).await;
|
||||||
|
operation_guard.disarm();
|
||||||
events::send(
|
events::send(
|
||||||
tx_notify_ui,
|
tx_notify_ui,
|
||||||
PeerEvent::RemoveDownloadedGameFinished { id: id.clone() },
|
PeerEvent::RemoveDownloadedGameFinished { id: id.clone() },
|
||||||
@@ -624,16 +642,21 @@ async fn run_remove_downloaded_operation(
|
|||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::error!("Downloaded-file removal failed for {id}: {err}");
|
log::error!("Downloaded-file removal failed for {id}: {err}");
|
||||||
|
if let Err(refresh_err) =
|
||||||
|
refresh_local_game_for_ending_operation(ctx, tx_notify_ui, &id).await
|
||||||
|
{
|
||||||
|
log::error!(
|
||||||
|
"Failed to refresh local library after downloaded-file removal failure: {refresh_err}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
end_operation(ctx, tx_notify_ui, &id).await;
|
||||||
|
operation_guard.disarm();
|
||||||
events::send(
|
events::send(
|
||||||
tx_notify_ui,
|
tx_notify_ui,
|
||||||
PeerEvent::RemoveDownloadedGameFailed { id: id.clone() },
|
PeerEvent::RemoveDownloadedGameFailed { id: id.clone() },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Err(err) = refresh_local_game(ctx, tx_notify_ui, &id).await {
|
|
||||||
log::error!("Failed to refresh local library after downloaded-file removal: {err}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn begin_operation(
|
async fn begin_operation(
|
||||||
@@ -809,7 +832,7 @@ async fn scan_and_announce_local_library(
|
|||||||
) -> eyre::Result<()> {
|
) -> eyre::Result<()> {
|
||||||
let catalog = ctx.catalog.read().await.clone();
|
let catalog = ctx.catalog.read().await.clone();
|
||||||
let scan = scan_local_library(game_dir, &catalog).await?;
|
let scan = scan_local_library(game_dir, &catalog).await?;
|
||||||
update_and_announce_games_with_policy(ctx, tx_notify_ui, scan, event_policy).await;
|
update_and_announce_games_with_policy(ctx, tx_notify_ui, scan, event_policy, None).await;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -826,6 +849,28 @@ async fn refresh_local_game(
|
|||||||
tx_notify_ui,
|
tx_notify_ui,
|
||||||
scan,
|
scan,
|
||||||
LocalLibraryEventPolicy::OnChange,
|
LocalLibraryEventPolicy::OnChange,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Refreshes the game whose operation has completed before clearing its
|
||||||
|
/// active-operation snapshot, while preserving freeze behavior for other games.
|
||||||
|
async fn refresh_local_game_for_ending_operation(
|
||||||
|
ctx: &Ctx,
|
||||||
|
tx_notify_ui: &UnboundedSender<PeerEvent>,
|
||||||
|
id: &str,
|
||||||
|
) -> eyre::Result<()> {
|
||||||
|
let game_dir = { ctx.game_dir.read().await.clone() };
|
||||||
|
let catalog = ctx.catalog.read().await.clone();
|
||||||
|
let scan = rescan_local_game(&game_dir, &catalog, id).await?;
|
||||||
|
update_and_announce_games_with_policy(
|
||||||
|
ctx,
|
||||||
|
tx_notify_ui,
|
||||||
|
scan,
|
||||||
|
LocalLibraryEventPolicy::OnChange,
|
||||||
|
Some(id),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -878,6 +923,7 @@ pub async fn update_and_announce_games(
|
|||||||
tx_notify_ui,
|
tx_notify_ui,
|
||||||
scan,
|
scan,
|
||||||
LocalLibraryEventPolicy::OnChange,
|
LocalLibraryEventPolicy::OnChange,
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
@@ -887,6 +933,7 @@ async fn update_and_announce_games_with_policy(
|
|||||||
tx_notify_ui: &UnboundedSender<PeerEvent>,
|
tx_notify_ui: &UnboundedSender<PeerEvent>,
|
||||||
scan: LocalLibraryScan,
|
scan: LocalLibraryScan,
|
||||||
event_policy: LocalLibraryEventPolicy,
|
event_policy: LocalLibraryEventPolicy,
|
||||||
|
ending_operation_id: Option<&str>,
|
||||||
) {
|
) {
|
||||||
let LocalLibraryScan {
|
let LocalLibraryScan {
|
||||||
mut game_db,
|
mut game_db,
|
||||||
@@ -894,7 +941,10 @@ async fn update_and_announce_games_with_policy(
|
|||||||
revision,
|
revision,
|
||||||
} = scan;
|
} = scan;
|
||||||
|
|
||||||
let active_operation_ids = active_operation_ids(ctx).await;
|
let mut active_operation_ids = active_operation_ids(ctx).await;
|
||||||
|
if let Some(id) = ending_operation_id {
|
||||||
|
active_operation_ids.remove(id);
|
||||||
|
}
|
||||||
if !active_operation_ids.is_empty() {
|
if !active_operation_ids.is_empty() {
|
||||||
let previous = ctx.local_library.read().await.games.clone();
|
let previous = ctx.local_library.read().await.games.clone();
|
||||||
for id in &active_operation_ids {
|
for id in &active_operation_ids {
|
||||||
@@ -1361,7 +1411,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn install_refreshes_settled_state_after_guard_release() {
|
async fn install_refreshes_settled_state_before_operation_clear() {
|
||||||
let temp = TempDir::new("lanspread-handler-install");
|
let temp = TempDir::new("lanspread-handler-install");
|
||||||
let root = temp.game_root();
|
let root = temp.game_root();
|
||||||
write_file(&root.join("version.ini"), b"20250101");
|
write_file(&root.join("version.ini"), b"20250101");
|
||||||
@@ -1383,13 +1433,13 @@ mod tests {
|
|||||||
}
|
}
|
||||||
_ => panic!("expected InstallGameBegin"),
|
_ => panic!("expected InstallGameBegin"),
|
||||||
}
|
}
|
||||||
|
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, Vec::new());
|
||||||
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"
|
||||||
));
|
));
|
||||||
assert!(ctx.active_operations.read().await.is_empty());
|
assert!(ctx.active_operations.read().await.is_empty());
|
||||||
assert_local_update(recv_event(&mut rx).await, true, true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -1444,6 +1494,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
_ => panic!("expected InstallGameBegin"),
|
_ => panic!("expected InstallGameBegin"),
|
||||||
}
|
}
|
||||||
|
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, Vec::new());
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
recv_event(&mut rx).await,
|
recv_event(&mut rx).await,
|
||||||
@@ -1451,11 +1502,10 @@ mod tests {
|
|||||||
));
|
));
|
||||||
assert!(ctx.active_operations.read().await.is_empty());
|
assert!(ctx.active_operations.read().await.is_empty());
|
||||||
assert!(ctx.active_downloads.read().await.is_empty());
|
assert!(ctx.active_downloads.read().await.is_empty());
|
||||||
assert_local_update(recv_event(&mut rx).await, true, true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn update_refreshes_settled_state_after_guard_release() {
|
async fn update_refreshes_settled_state_before_operation_clear() {
|
||||||
let temp = TempDir::new("lanspread-handler-update");
|
let temp = TempDir::new("lanspread-handler-update");
|
||||||
let root = temp.game_root();
|
let root = temp.game_root();
|
||||||
write_file(&root.join("version.ini"), b"20250101");
|
write_file(&root.join("version.ini"), b"20250101");
|
||||||
@@ -1478,13 +1528,13 @@ mod tests {
|
|||||||
}
|
}
|
||||||
_ => panic!("expected InstallGameBegin"),
|
_ => panic!("expected InstallGameBegin"),
|
||||||
}
|
}
|
||||||
|
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, Vec::new());
|
||||||
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"
|
||||||
));
|
));
|
||||||
assert!(ctx.active_operations.read().await.is_empty());
|
assert!(ctx.active_operations.read().await.is_empty());
|
||||||
assert_local_update(recv_event(&mut rx).await, true, true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -1509,13 +1559,13 @@ mod tests {
|
|||||||
operation: InstallOperation::Installing
|
operation: InstallOperation::Installing
|
||||||
} if id == "game"
|
} if id == "game"
|
||||||
));
|
));
|
||||||
|
let game = local_update_game(recv_event(&mut rx).await, true, true);
|
||||||
|
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, Vec::new());
|
||||||
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"
|
||||||
));
|
));
|
||||||
let game = local_update_game(recv_event(&mut rx).await, true, true);
|
|
||||||
assert_eq!(game.local_version.as_deref(), Some("20240101"));
|
|
||||||
|
|
||||||
write_file(&root.join("version.ini"), b"20250101");
|
write_file(&root.join("version.ini"), b"20250101");
|
||||||
write_file(&root.join("game.eti"), b"new archive");
|
write_file(&root.join("game.eti"), b"new archive");
|
||||||
@@ -1532,13 +1582,13 @@ mod tests {
|
|||||||
operation: InstallOperation::Updating
|
operation: InstallOperation::Updating
|
||||||
} if id == "game"
|
} if id == "game"
|
||||||
));
|
));
|
||||||
|
let game = local_update_game(recv_event(&mut rx).await, true, true);
|
||||||
|
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, Vec::new());
|
||||||
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"
|
||||||
));
|
));
|
||||||
let game = local_update_game(recv_event(&mut rx).await, true, true);
|
|
||||||
assert_eq!(game.local_version.as_deref(), Some("20250101"));
|
|
||||||
|
|
||||||
run_uninstall_operation(&ctx, &tx, "game".to_string()).await;
|
run_uninstall_operation(&ctx, &tx, "game".to_string()).await;
|
||||||
assert_active_update(
|
assert_active_update(
|
||||||
@@ -1549,18 +1599,18 @@ mod tests {
|
|||||||
recv_event(&mut rx).await,
|
recv_event(&mut rx).await,
|
||||||
PeerEvent::UninstallGameBegin { id } if id == "game"
|
PeerEvent::UninstallGameBegin { id } if id == "game"
|
||||||
));
|
));
|
||||||
|
let game = local_update_game(recv_event(&mut rx).await, false, true);
|
||||||
|
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, Vec::new());
|
||||||
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"
|
||||||
));
|
));
|
||||||
let game = local_update_game(recv_event(&mut rx).await, false, true);
|
|
||||||
assert_eq!(game.local_version.as_deref(), Some("20250101"));
|
|
||||||
assert!(ctx.active_operations.read().await.is_empty());
|
assert!(ctx.active_operations.read().await.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn uninstall_refreshes_settled_state_after_guard_release() {
|
async fn uninstall_refreshes_settled_state_before_operation_clear() {
|
||||||
let temp = TempDir::new("lanspread-handler-uninstall");
|
let temp = TempDir::new("lanspread-handler-uninstall");
|
||||||
let root = temp.game_root();
|
let root = temp.game_root();
|
||||||
write_file(&root.join("version.ini"), b"20250101");
|
write_file(&root.join("version.ini"), b"20250101");
|
||||||
@@ -1580,17 +1630,17 @@ mod tests {
|
|||||||
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_active_update(recv_event(&mut rx).await, Vec::new());
|
assert_active_update(recv_event(&mut rx).await, Vec::new());
|
||||||
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"
|
||||||
));
|
));
|
||||||
assert!(ctx.active_operations.read().await.is_empty());
|
assert!(ctx.active_operations.read().await.is_empty());
|
||||||
assert_local_update(recv_event(&mut rx).await, false, true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn remove_downloaded_refreshes_settled_state_after_guard_release() {
|
async fn remove_downloaded_refreshes_settled_state_before_operation_clear() {
|
||||||
let temp = TempDir::new("lanspread-handler-remove-downloaded");
|
let temp = TempDir::new("lanspread-handler-remove-downloaded");
|
||||||
let root = temp.game_root();
|
let root = temp.game_root();
|
||||||
write_file(&root.join("version.ini"), b"20250101");
|
write_file(&root.join("version.ini"), b"20250101");
|
||||||
@@ -1615,15 +1665,15 @@ mod tests {
|
|||||||
recv_event(&mut rx).await,
|
recv_event(&mut rx).await,
|
||||||
PeerEvent::RemoveDownloadedGameBegin { id } if id == "game"
|
PeerEvent::RemoveDownloadedGameBegin { id } if id == "game"
|
||||||
));
|
));
|
||||||
|
let PeerEvent::LocalLibraryChanged { games } = recv_event(&mut rx).await else {
|
||||||
|
panic!("expected LocalLibraryChanged");
|
||||||
|
};
|
||||||
|
assert!(games.is_empty());
|
||||||
assert_active_update(recv_event(&mut rx).await, Vec::new());
|
assert_active_update(recv_event(&mut rx).await, Vec::new());
|
||||||
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"
|
||||||
));
|
));
|
||||||
let PeerEvent::LocalLibraryChanged { games } = recv_event(&mut rx).await else {
|
|
||||||
panic!("expected LocalLibraryChanged");
|
|
||||||
};
|
|
||||||
assert!(games.is_empty());
|
|
||||||
assert!(!root.exists());
|
assert!(!root.exists());
|
||||||
assert!(ctx.active_operations.read().await.is_empty());
|
assert!(ctx.active_operations.read().await.is_empty());
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user