test(peer): consolidate temp dir helper

Move the repeated test TempDir implementations into a single peer
test_support module. The shared helper keeps the existing automatic cleanup
behavior and uses an atomic suffix plus timestamp so parallel tests do not
collide on the same path.

This is intentionally limited to test hygiene. It does not change the
availability model, split download.rs, or touch production scan/install
behavior beyond importing the shared helper from test modules.

Test Plan:
- git diff --check
- just fmt
- just clippy
- just test

Follow-up-Plan: FOLLOW_UP_2.md
This commit is contained in:
2026-05-16 09:21:43 +02:00
parent 7731a9daa0
commit 894eb5af6a
10 changed files with 103 additions and 307 deletions
+5 -32
View File
@@ -920,39 +920,12 @@ pub async fn download_game_files(
#[cfg(test)]
mod tests {
use super::*;
use crate::test_support::TempDir;
fn loopback_addr(port: u16) -> SocketAddr {
SocketAddr::from(([127, 0, 0, 1], port))
}
struct TempDir(PathBuf);
impl TempDir {
fn new() -> Self {
let mut path = std::env::temp_dir();
path.push(format!(
"lanspread-download-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_nanos()
));
std::fs::create_dir_all(&path).expect("temp dir should be created");
Self(path)
}
fn path(&self) -> &Path {
&self.0
}
}
impl Drop for TempDir {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
#[test]
fn build_peer_plans_handles_partial_final_chunk() {
let peers = vec![loopback_addr(12000), loopback_addr(12001)];
@@ -1041,7 +1014,7 @@ mod tests {
#[tokio::test]
async fn prepare_game_storage_skips_version_ini_sentinel() {
let temp = TempDir::new();
let temp = TempDir::new("lanspread-download");
let descs = vec![GameFileDescription {
game_id: "game".to_string(),
relative_path: "game/version.ini".to_string(),
@@ -1080,7 +1053,7 @@ mod tests {
#[tokio::test]
async fn commit_version_ini_writes_sentinel_last_and_sweeps_discarded() {
let temp = TempDir::new();
let temp = TempDir::new("lanspread-download");
let game_root = temp.path().join("game");
tokio::fs::create_dir_all(&game_root)
.await
@@ -1115,7 +1088,7 @@ mod tests {
#[tokio::test]
async fn begin_version_ini_transaction_parks_existing_sentinel() {
let temp = TempDir::new();
let temp = TempDir::new("lanspread-download");
let game_root = temp.path().join("game");
tokio::fs::create_dir_all(&game_root)
.await
@@ -1142,7 +1115,7 @@ mod tests {
#[tokio::test]
async fn rollback_version_ini_transaction_sweeps_transients() {
let temp = TempDir::new();
let temp = TempDir::new("lanspread-download");
let game_root = temp.path().join("game");
tokio::fs::create_dir_all(&game_root)
.await
+2 -34
View File
@@ -652,46 +652,14 @@ mod tests {
collections::HashSet,
path::{Path, PathBuf},
sync::Arc,
time::{Duration, SystemTime, UNIX_EPOCH},
time::Duration,
};
use tokio::sync::mpsc;
use tokio_util::{sync::CancellationToken, task::TaskTracker};
use super::*;
use crate::{UnpackFuture, Unpacker};
struct TempDir(PathBuf);
impl TempDir {
fn new(prefix: &str) -> Self {
let mut path = std::env::temp_dir();
path.push(format!(
"{prefix}-{}-{}",
std::process::id(),
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_nanos()
));
std::fs::create_dir_all(&path).expect("temp dir should be created");
Self(path)
}
fn path(&self) -> &Path {
&self.0
}
fn game_root(&self) -> PathBuf {
self.0.join("game")
}
}
impl Drop for TempDir {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
use crate::{UnpackFuture, Unpacker, test_support::TempDir};
struct FakeUnpacker;
+6 -33
View File
@@ -119,39 +119,12 @@ fn sync_parent_dir(_path: &Path) -> std::io::Result<()> {
#[cfg(test)]
mod tests {
use std::path::{Path, PathBuf};
use super::*;
struct TempDir(PathBuf);
impl TempDir {
fn new() -> Self {
let mut path = std::env::temp_dir();
path.push(format!(
"lanspread-intent-{}-{}",
std::process::id(),
now_unix_secs()
));
path.push(format!("{:?}", std::thread::current().id()).replace(['(', ')'], ""));
std::fs::create_dir_all(&path).expect("temp dir should be created");
Self(path)
}
fn path(&self) -> &Path {
&self.0
}
}
impl Drop for TempDir {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
use crate::test_support::TempDir;
#[tokio::test]
async fn tmp_write_without_rename_leaves_previous_intent_intact() {
let temp = TempDir::new();
let temp = TempDir::new("lanspread-intent");
let previous = InstallIntent::new(
"game",
InstallIntentState::Updating,
@@ -180,7 +153,7 @@ mod tests {
#[tokio::test]
async fn schema_mismatch_is_treated_as_missing() {
let temp = TempDir::new();
let temp = TempDir::new("lanspread-intent");
tokio::fs::write(
intent_path(temp.path()),
r#"{"schema_version":2,"id":"game","recorded_at":0,"state":"Updating"}"#,
@@ -194,7 +167,7 @@ mod tests {
#[tokio::test]
async fn mismatched_id_is_treated_as_missing() {
let temp = TempDir::new();
let temp = TempDir::new("lanspread-intent");
tokio::fs::write(
intent_path(temp.path()),
r#"{"schema_version":1,"id":"other","recorded_at":0,"state":"Updating"}"#,
@@ -208,7 +181,7 @@ mod tests {
#[tokio::test]
async fn corrupt_intent_is_treated_as_missing() {
let temp = TempDir::new();
let temp = TempDir::new("lanspread-intent");
tokio::fs::write(intent_path(temp.path()), b"not json")
.await
.expect("intent should be written");
@@ -219,7 +192,7 @@ mod tests {
#[tokio::test]
async fn old_manifest_hash_field_is_ignored_and_new_writes_omit_it() {
let temp = TempDir::new();
let temp = TempDir::new("lanspread-intent");
tokio::fs::write(
intent_path(temp.path()),
r#"{"schema_version":1,"id":"game","recorded_at":0,"state":"Updating","eti_version":"20240101","manifest_hash":42}"#,
@@ -464,15 +464,11 @@ mod tests {
use std::{
collections::HashSet,
path::{Path, PathBuf},
sync::{
Arc,
Mutex,
atomic::{AtomicU64, Ordering},
},
sync::{Arc, Mutex},
};
use super::*;
use crate::install::unpack::UnpackFuture;
use crate::{install::unpack::UnpackFuture, test_support::TempDir};
#[derive(Default)]
struct FakeUnpacker {
@@ -523,38 +519,6 @@ 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-{}-{}-{}",
std::process::id(),
unique_id,
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_nanos()
));
std::fs::create_dir_all(&path).expect("temp dir should be created");
Self(path)
}
fn game_root(&self) -> PathBuf {
self.0.join("game")
}
}
impl Drop for TempDir {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
fn write_file(path: &Path, bytes: &[u8]) {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).expect("parent dir should be created");
@@ -568,7 +532,7 @@ mod tests {
#[tokio::test]
async fn install_success_promotes_staging_and_clears_intent() {
let temp = TempDir::new();
let temp = TempDir::new("lanspread-install");
let root = temp.game_root();
write_file(&root.join("game.eti"), b"archive");
write_file(&root.join("version.ini"), b"20250101");
@@ -585,7 +549,7 @@ mod tests {
#[tokio::test]
async fn install_unpacks_multiple_root_eti_archives_in_sorted_order() {
let temp = TempDir::new();
let temp = TempDir::new("lanspread-install");
let root = temp.game_root();
write_file(&root.join("b.eti"), b"archive");
write_file(&root.join("a.eti"), b"archive");
@@ -608,7 +572,7 @@ mod tests {
#[tokio::test]
async fn update_failure_restores_previous_local() {
let temp = TempDir::new();
let temp = TempDir::new("lanspread-install");
let root = temp.game_root();
write_file(&root.join("game.eti"), b"archive");
write_file(&root.join("version.ini"), b"20250101");
@@ -628,7 +592,7 @@ mod tests {
#[tokio::test]
async fn update_commit_rename_failure_restores_previous_local() {
let temp = TempDir::new();
let temp = TempDir::new("lanspread-install");
let root = temp.game_root();
write_file(&root.join("game.eti"), b"archive");
write_file(&root.join("version.ini"), b"20250101");
@@ -656,7 +620,7 @@ mod tests {
#[tokio::test]
async fn update_success_promotes_new_local_and_removes_backup() {
let temp = TempDir::new();
let temp = TempDir::new("lanspread-install");
let root = temp.game_root();
write_file(&root.join("game.eti"), b"archive");
write_file(&root.join("version.ini"), b"20250101");
@@ -676,7 +640,7 @@ mod tests {
#[tokio::test]
async fn uninstall_removes_only_local_install() {
let temp = TempDir::new();
let temp = TempDir::new("lanspread-install");
let root = temp.game_root();
write_file(&root.join("game.eti"), b"archive");
write_file(&root.join("version.ini"), b"20250101");
@@ -696,7 +660,7 @@ mod tests {
async fn uninstall_delete_failure_restores_backup() {
use std::os::unix::fs::PermissionsExt;
let temp = TempDir::new();
let temp = TempDir::new("lanspread-install");
let root = temp.game_root();
let locked_dir = root.join("local").join("locked");
write_file(&root.join("version.ini"), b"20250101");
@@ -881,7 +845,7 @@ mod tests {
];
for case in cases {
let temp = TempDir::new();
let temp = TempDir::new("lanspread-install");
let root = temp.game_root();
seed_recovery_case(&root, &case);
write_intent(
@@ -909,7 +873,7 @@ mod tests {
#[tokio::test]
async fn none_recovery_leaves_markerless_reserved_dirs_untouched() {
let temp = TempDir::new();
let temp = TempDir::new("lanspread-install");
let root = temp.game_root();
write_file(&root.join(".local.backup").join("user.txt"), b"user");
@@ -922,7 +886,7 @@ mod tests {
#[tokio::test]
async fn download_recovery_sweeps_reserved_version_files() {
let temp = TempDir::new();
let temp = TempDir::new("lanspread-install");
let root = temp.game_root();
write_file(&root.join(VERSION_TMP_FILE), b"tmp");
write_file(&root.join(VERSION_DISCARDED_FILE), b"old");
@@ -937,13 +901,13 @@ mod tests {
#[tokio::test]
async fn startup_recovery_skips_active_game_roots() {
let temp = TempDir::new();
let active_root = temp.0.join("active");
let inactive_root = temp.0.join("inactive");
let temp = TempDir::new("lanspread-install");
let active_root = temp.path().join("active");
let inactive_root = temp.path().join("inactive");
write_file(&active_root.join(VERSION_TMP_FILE), b"tmp");
write_file(&inactive_root.join(VERSION_TMP_FILE), b"tmp");
recover_on_startup(&temp.0, &HashSet::from(["active".to_string()]))
recover_on_startup(temp.path(), &HashSet::from(["active".to_string()]))
.await
.expect("recovery should succeed");
+2
View File
@@ -29,6 +29,8 @@ mod peer_db;
mod remote_peer;
mod services;
mod startup;
#[cfg(test)]
mod test_support;
// =============================================================================
// Public re-exports
+5 -33
View File
@@ -620,41 +620,13 @@ pub async fn get_game_file_descriptions(
mod tests {
use std::{
collections::{HashMap, HashSet},
path::{Path, PathBuf},
path::Path,
};
use lanspread_proto::Availability;
use super::*;
use crate::context::OperationKind;
struct TempDir(PathBuf);
impl TempDir {
fn new() -> Self {
let mut path = std::env::temp_dir();
path.push(format!(
"lanspread-local-games-{}-{}",
std::process::id(),
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_nanos()
));
std::fs::create_dir_all(&path).expect("temp dir should be created");
Self(path)
}
fn path(&self) -> &Path {
&self.0
}
}
impl Drop for TempDir {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
use crate::{context::OperationKind, test_support::TempDir};
fn write_file(path: &Path, bytes: &[u8]) {
if let Some(parent) = path.parent() {
@@ -665,7 +637,7 @@ mod tests {
#[tokio::test]
async fn scan_uses_version_ini_and_local_dir_as_independent_state() {
let temp = TempDir::new();
let temp = TempDir::new("lanspread-local-games");
let catalog = HashSet::from([
"ready".to_string(),
"local-only".to_string(),
@@ -718,7 +690,7 @@ mod tests {
#[tokio::test]
async fn rescan_promotes_installed_only_game_to_ready_when_sentinel_appears() {
let temp = TempDir::new();
let temp = TempDir::new("lanspread-local-games");
let catalog = HashSet::from(["game".to_string()]);
std::fs::create_dir_all(temp.path().join("game").join("local"))
.expect("local install dir should be created");
@@ -751,7 +723,7 @@ mod tests {
#[tokio::test]
async fn local_download_available_gates_on_catalog_operation_and_sentinel() {
let temp = TempDir::new();
let temp = TempDir::new("lanspread-local-games");
let game_root = temp.path().join("game");
write_file(&game_root.join("version.ini"), b"20250101");
+9 -42
View File
@@ -99,44 +99,11 @@ pub fn validate_game_file_path(game_dir: &Path, relative_path: &str) -> eyre::Re
#[cfg(test)]
mod tests {
use super::*;
fn create_temp_dir() -> std::io::Result<std::path::PathBuf> {
let mut dir = std::env::temp_dir();
let unique = format!(
"lanspread_test_{}_{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_nanos()
);
dir.push(unique);
std::fs::create_dir_all(&dir)?;
Ok(dir)
}
struct TempDir(std::path::PathBuf);
impl TempDir {
fn new() -> std::io::Result<Self> {
let path = create_temp_dir()?;
Ok(TempDir(path))
}
fn path(&self) -> &std::path::Path {
&self.0
}
}
impl Drop for TempDir {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
use crate::test_support::TempDir;
#[test]
fn test_valid_paths() {
let temp_dir = TempDir::new().expect("Failed to create temp dir for test");
let temp_dir = TempDir::new("lanspread-path-validation");
let base = temp_dir.path();
// Valid relative paths
@@ -156,7 +123,7 @@ mod tests {
#[test]
fn test_traversal_attempts() {
let temp_dir = TempDir::new().expect("Failed to create temp dir for test");
let temp_dir = TempDir::new("lanspread-path-validation");
let base = temp_dir.path();
// These should all fail
@@ -167,7 +134,7 @@ mod tests {
#[test]
fn test_double_dot_in_filename_allowed() {
let temp_dir = TempDir::new().expect("Failed to create temp dir for test");
let temp_dir = TempDir::new("lanspread-path-validation");
let base = temp_dir.path();
assert!(validate_game_file_path(base, "data/file..txt").is_ok());
@@ -175,7 +142,7 @@ mod tests {
#[test]
fn test_missing_file_stays_within_base() {
let temp_dir = TempDir::new().expect("Failed to create temp dir for test");
let temp_dir = TempDir::new("lanspread-path-validation");
let base = temp_dir.path();
#[allow(clippy::unwrap_used)]
@@ -191,7 +158,7 @@ mod tests {
#[test]
fn test_absolute_paths() {
let temp_dir = TempDir::new().expect("Failed to create temp dir for test");
let temp_dir = TempDir::new("lanspread-path-validation");
let base = temp_dir.path();
// Absolute paths should fail
@@ -203,7 +170,7 @@ mod tests {
#[test]
fn test_windows_specific() {
let temp_dir = TempDir::new().expect("Failed to create temp dir for test");
let temp_dir = TempDir::new("lanspread-path-validation");
let base = temp_dir.path();
// Windows-specific paths that should fail
@@ -217,8 +184,8 @@ mod tests {
fn test_symlink_escape_rejected() {
use std::os::unix::fs::symlink;
let base_dir = TempDir::new().expect("Failed to create base temp dir");
let outside_dir = TempDir::new().expect("Failed to create outside temp dir");
let base_dir = TempDir::new("lanspread-path-validation-base");
let outside_dir = TempDir::new("lanspread-path-validation-outside");
let base = base_dir.path();
let outside = outside_dir.path();
@@ -334,11 +334,8 @@ mod tests {
use std::{
collections::HashSet,
path::{Path, PathBuf},
sync::{
Arc,
atomic::{AtomicU64, Ordering},
},
time::{Duration, SystemTime, UNIX_EPOCH},
sync::Arc,
time::Duration,
};
use notify::EventKind;
@@ -346,39 +343,13 @@ mod tests {
use tokio_util::{sync::CancellationToken, task::TaskTracker};
use super::*;
use crate::{UnpackFuture, Unpacker, context::OperationKind, peer_db::PeerGameDB};
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-local-monitor-{}-{}-{}",
std::process::id(),
unique_id,
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_nanos()
));
std::fs::create_dir_all(&path).expect("temp dir should be created");
Self(path)
}
fn path(&self) -> &Path {
&self.0
}
}
impl Drop for TempDir {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
use crate::{
UnpackFuture,
Unpacker,
context::OperationKind,
peer_db::PeerGameDB,
test_support::TempDir,
};
struct NoopUnpacker;
@@ -467,7 +438,7 @@ mod tests {
#[tokio::test]
async fn watch_event_for_active_game_is_dropped() {
let temp = TempDir::new();
let temp = TempDir::new("lanspread-local-monitor");
let ctx = test_ctx(
temp.path().to_path_buf(),
HashSet::from(["game".to_string()]),
@@ -501,7 +472,7 @@ mod tests {
#[tokio::test]
async fn burst_watch_events_collapse_to_two_rescans_for_same_game() {
let temp = TempDir::new();
let temp = TempDir::new("lanspread-local-monitor");
let game_root = temp.path().join("game");
write_file(&game_root.join("version.ini"), b"20250101");
let ctx = test_ctx(
@@ -538,7 +509,7 @@ mod tests {
#[tokio::test]
async fn fallback_scan_picks_up_sideloaded_catalog_game() {
let temp = TempDir::new();
let temp = TempDir::new("lanspread-local-monitor");
write_file(&temp.path().join("game").join("version.ini"), b"20250101");
let ctx = test_ctx(
temp.path().to_path_buf(),
@@ -560,7 +531,7 @@ mod tests {
#[tokio::test]
async fn fallback_scan_ignores_non_catalog_game_without_library_delta() {
let temp = TempDir::new();
let temp = TempDir::new("lanspread-local-monitor");
write_file(
&temp.path().join("non-catalog").join("version.ini"),
b"20250101",
+4 -39
View File
@@ -398,11 +398,7 @@ mod tests {
use std::{
collections::HashSet,
path::{Path, PathBuf},
sync::{
Arc,
atomic::{AtomicU64, Ordering},
},
time::{SystemTime, UNIX_EPOCH},
sync::Arc,
};
use tokio::sync::{RwLock, mpsc};
@@ -414,40 +410,9 @@ mod tests {
Unpacker,
context::{Ctx, OperationKind},
peer_db::PeerGameDB,
test_support::TempDir,
};
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-stream-{}-{}-{}",
std::process::id(),
unique_id,
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_nanos()
));
std::fs::create_dir_all(&path).expect("temp dir should be created");
Self(path)
}
fn path(&self) -> &Path {
&self.0
}
}
impl Drop for TempDir {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
struct NoopUnpacker;
impl Unpacker for NoopUnpacker {
@@ -488,7 +453,7 @@ mod tests {
#[tokio::test]
async fn get_game_response_respects_serve_gates() {
let temp = TempDir::new();
let temp = TempDir::new("lanspread-stream");
write_file(&temp.path().join("ready").join("version.ini"), b"20250101");
write_file(
&temp.path().join("non-catalog").join("version.ini"),
@@ -531,7 +496,7 @@ mod tests {
#[tokio::test]
async fn file_transfer_dispatch_respects_serve_gates() {
let temp = TempDir::new();
let temp = TempDir::new("lanspread-stream");
write_file(&temp.path().join("ready").join("version.ini"), b"20250101");
write_file(
&temp.path().join("non-catalog").join("version.ini"),
+41
View File
@@ -0,0 +1,41 @@
use std::{
path::{Path, PathBuf},
sync::atomic::{AtomicU64, Ordering},
time::{SystemTime, UNIX_EPOCH},
};
static NEXT_TEMP_ID: AtomicU64 = AtomicU64::new(0);
pub(crate) struct TempDir(PathBuf);
impl TempDir {
pub(crate) fn new(prefix: &str) -> Self {
let mut path = std::env::temp_dir();
let unique_id = NEXT_TEMP_ID.fetch_add(1, Ordering::Relaxed);
path.push(format!(
"{prefix}-{}-{}-{}",
std::process::id(),
unique_id,
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_nanos()
));
std::fs::create_dir_all(&path).expect("temp dir should be created");
Self(path)
}
pub(crate) fn path(&self) -> &Path {
&self.0
}
pub(crate) fn game_root(&self) -> PathBuf {
self.0.join("game")
}
}
impl Drop for TempDir {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}