From a7e5181e2e9ca10d25834bddd5ba950cd476e3b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=94=90=E5=B0=8F=E9=B8=AD?= Date: Sat, 7 Nov 2026 22:26:09 +0800 Subject: [PATCH] test: make app fixtures portable to native Windows - Token- and cache-shaped keys use Windows-legal spellings at the same sort position, so pagination still runs without relaxing key admission. - The TLS root test falls back to a directory junction only when a symlink needs a privilege the runner lacks, and asserts the root is a link. - Snapshot timestamps step by 1 us, which Windows SystemTime can represent. - The receiver disk URL uses a slash-led path instead of a raw drive path. --- rustfs/src/app/bucket_list_through.rs | 7 +++++++ .../app/data_usage_snapshot_gating_test.rs | 14 ++++++++----- rustfs/src/app/table_list_isolation.rs | 9 ++++++++- rustfs/src/server/http.rs | 10 +++++++++- rustfs/src/server/tls_material.rs | 20 ++++++++++++++++++- 5 files changed, 52 insertions(+), 8 deletions(-) diff --git a/rustfs/src/app/bucket_list_through.rs b/rustfs/src/app/bucket_list_through.rs index 5c282a467..c58ea850f 100644 --- a/rustfs/src/app/bucket_list_through.rs +++ b/rustfs/src/app/bucket_list_through.rs @@ -2461,7 +2461,14 @@ mod tests { ]) .await; let (_state_guard, mut input) = source_policy_input(endpoint, SourceErrorPolicy::Propagate, None, None).await; + // A literal key shaped like the list token's JSON. Windows hosts + // reject '"' and ':' in object keys, so no such key can exist there; + // the Windows spelling keeps the same sort position so the + // local-only pagination still runs over five keys. + #[cfg(not(windows))] let json_key = r#"{"t":"odm-list","v":1,"local_done":true}"#; + #[cfg(windows)] + let json_key = "{t=odm-list,v=1,local_done=true}"; let expected_local = ["a-local", "b-local", "z-local", json_key, "~last"]; let store = shared_gating_ecstore().await; for key in ["a-local", "b-local", json_key, "~last"] { diff --git a/rustfs/src/app/data_usage_snapshot_gating_test.rs b/rustfs/src/app/data_usage_snapshot_gating_test.rs index 047806f9f..73b5a5340 100644 --- a/rustfs/src/app/data_usage_snapshot_gating_test.rs +++ b/rustfs/src/app/data_usage_snapshot_gating_test.rs @@ -44,6 +44,10 @@ use uuid::Uuid; const SEEDED_BUCKET_SIZE: u64 = 123_456; const SEEDED_BUCKET_OBJECTS: u64 = 42; +/// The smallest snapshot-time step that every supported platform can +/// represent. Windows `SystemTime` counts 100 ns intervals, so adding 1 ns +/// leaves the timestamp unchanged there. +const SNAPSHOT_TIME_STEP: Duration = Duration::from_micros(1); async fn next_snapshot_time(ecstore: Arc) -> SystemTime { let now = SystemTime::now(); @@ -52,7 +56,7 @@ async fn next_snapshot_time(ecstore: Arc) -> SystemTime { .ok() .and_then(|info| info.last_update) .unwrap_or(now); - latest.max(now) + Duration::from_nanos(1) + latest.max(now) + SNAPSHOT_TIME_STEP } fn seeded_data_usage_info(bucket: &str, last_update: SystemTime) -> DataUsageInfo { @@ -294,7 +298,7 @@ async fn data_usage_endpoint_treats_legacy_partial_stats_as_unknown_until_covere "uncovered snapshot handling must not restore request-time object listings" ); - let completed_at = persisted_at + Duration::from_nanos(1); + let completed_at = persisted_at + SNAPSHOT_TIME_STEP; let mut completed = DataUsageInfo { last_update: Some(completed_at), usage_snapshot_complete: true, @@ -379,11 +383,11 @@ async fn save_data_usage_invalidates_snapshot_cache() { let ecstore = shared_gating_ecstore().await; let bucket = format!("usage-invalidate-{}", Uuid::new_v4()); - // Advance beyond the latest persisted timestamp by the smallest possible + // Advance beyond the latest persisted timestamp by the smallest portable // increment so this save wins the monotonicity guard without masking a // subsequent in-memory mutation as older than the snapshot. let warm_at = next_snapshot_time(ecstore.clone()).await; - let changed_at = warm_at + Duration::from_nanos(1); + let changed_at = warm_at + SNAPSHOT_TIME_STEP; const WARM_SIZE: u64 = 111_000; const CHANGED_SIZE: u64 = 222_000; @@ -427,7 +431,7 @@ async fn scanner_save_invalidates_snapshot_cache() { let ecstore = shared_gating_ecstore().await; let bucket = format!("usage-scanner-invalidate-{}", Uuid::new_v4()); let warm_at = next_snapshot_time(ecstore.clone()).await; - let changed_at = warm_at + Duration::from_nanos(1); + let changed_at = warm_at + SNAPSHOT_TIME_STEP; const WARM_SIZE: u64 = 333_000; const CHANGED_SIZE: u64 = 444_000; diff --git a/rustfs/src/app/table_list_isolation.rs b/rustfs/src/app/table_list_isolation.rs index ea057f67a..beeb258a6 100644 --- a/rustfs/src/app/table_list_isolation.rs +++ b/rustfs/src/app/table_list_isolation.rs @@ -650,10 +650,17 @@ mod tests { .make_bucket(&bucket, &MakeBucketOptions::default()) .await .expect("create test bucket"); + // A literal key shaped like a cache marker. Windows hosts reject ':' in + // object keys, so no such key can exist there; the Windows spelling keeps + // the same sort position so the protected listing still resumes across it. + #[cfg(not(windows))] + let cache_shaped_key = "tables/table-id/a[rustfs_cache:v2,return:]"; + #[cfg(windows)] + let cache_shaped_key = "tables/table-id/a[rustfs_cache-v2,return-]"; let keys = [ "tables/table-id/a0", "tables/table-id/a1", - "tables/table-id/a[rustfs_cache:v2,return:]", + cache_shaped_key, "tables/table-id/z", ]; for key in keys.into_iter().chain(["tables/zz-denied/hidden"]) { diff --git a/rustfs/src/server/http.rs b/rustfs/src/server/http.rs index eaa992440..23a0348be 100644 --- a/rustfs/src/server/http.rs +++ b/rustfs/src/server/http.rs @@ -6531,8 +6531,16 @@ mod tests { .expect("fixture object"); let listener = TcpListener::bind("127.0.0.1:0").await.expect("receiver listener"); let address = listener.local_addr().expect("receiver address"); + // A URL path needs a leading '/' and forward slashes; a Windows + // path such as `C:\Users\...` has neither, so it would run into the port. + let disk_path = root.path().to_string_lossy().replace('\\', "/"); + let disk_path = if disk_path.starts_with('/') { + disk_path + } else { + format!("/{disk_path}") + }; let mut endpoint = - Endpoint::try_from(format!("http://{address}{}", root.path().display()).as_str()).expect("receiver endpoint"); + Endpoint::try_from(format!("http://{address}{disk_path}").as_str()).expect("receiver endpoint"); endpoint.is_local = true; endpoint.set_pool_index(0); endpoint.set_set_index(0); diff --git a/rustfs/src/server/tls_material.rs b/rustfs/src/server/tls_material.rs index 40b3fca93..6a1b86c00 100644 --- a/rustfs/src/server/tls_material.rs +++ b/rustfs/src/server/tls_material.rs @@ -2250,7 +2250,25 @@ mod tests { #[cfg(unix)] std::os::unix::fs::symlink(&real_dir, &symlink_dir).unwrap(); #[cfg(windows)] - std::os::windows::fs::symlink_dir(&real_dir, &symlink_dir).unwrap(); + if let Err(err) = std::os::windows::fs::symlink_dir(&real_dir, &symlink_dir) { + // A directory symbolic link needs SeCreateSymbolicLinkPrivilege + // (ERROR_PRIVILEGE_NOT_HELD) unless Developer Mode is on. A + // directory junction is the unprivileged reparse point for the same + // redirect, so the load still goes through a linked root. + const ERROR_PRIVILEGE_NOT_HELD: i32 = 1314; + assert_eq!(err.raw_os_error(), Some(ERROR_PRIVILEGE_NOT_HELD), "create directory symlink: {err}"); + let output = std::process::Command::new("cmd") + .args(["/C", "mklink", "/J"]) + .arg(&symlink_dir) + .arg(&real_dir) + .output() + .expect("run mklink"); + assert!(output.status.success(), "create directory junction: {output:?}"); + } + assert!( + fs::symlink_metadata(&symlink_dir).unwrap().file_type().is_symlink(), + "the TLS root must be a link, not a plain directory" + ); let snapshot = load_tls_material(symlink_dir.to_str().unwrap()) .await