mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-22 04:16:38 +00:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1871628568 | |||
| 2f0918f60b | |||
| 5b951de2b7 | |||
| 86d8509826 |
@@ -57,6 +57,13 @@ pub const DEFAULT_MAX_IO_EVENTS_PER_TICK: usize = 1024;
|
|||||||
pub const DEFAULT_EVENT_INTERVAL: u32 = 61;
|
pub const DEFAULT_EVENT_INTERVAL: u32 = 61;
|
||||||
pub const DEFAULT_RNG_SEED: Option<u64> = None; // None means random
|
pub const DEFAULT_RNG_SEED: Option<u64> = None; // None means random
|
||||||
|
|
||||||
|
/// Dedicated blocking thread pool for fsync/fdatasync operations.
|
||||||
|
/// When > 1, fsync operations are isolated from the main blocking pool to
|
||||||
|
/// prevent device-bound fsync from starving read operations (pread/stat/open).
|
||||||
|
/// Default 0 means auto (no isolation, use main runtime).
|
||||||
|
pub const ENV_FSYNC_BLOCKING_THREADS: &str = "RUSTFS_RUNTIME_FSYNC_BLOCKING_THREADS";
|
||||||
|
pub const DEFAULT_FSYNC_BLOCKING_THREADS: usize = 0;
|
||||||
|
|
||||||
// Dial9 Tokio Telemetry Default values
|
// Dial9 Tokio Telemetry Default values
|
||||||
pub const DEFAULT_RUNTIME_DIAL9_ENABLED: bool = false; // Disabled by default
|
pub const DEFAULT_RUNTIME_DIAL9_ENABLED: bool = false; // Disabled by default
|
||||||
pub const DEFAULT_RUNTIME_DIAL9_OUTPUT_DIR: &str = "/var/log/rustfs/telemetry";
|
pub const DEFAULT_RUNTIME_DIAL9_OUTPUT_DIR: &str = "/var/log/rustfs/telemetry";
|
||||||
|
|||||||
@@ -315,7 +315,7 @@ pub async fn fsync_dir(dir: impl AsRef<Path>) -> io::Result<()> {
|
|||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
{
|
{
|
||||||
let dir = dir.as_ref().to_path_buf();
|
let dir = dir.as_ref().to_path_buf();
|
||||||
tokio::task::spawn_blocking(move || fsync_dir_std(dir)).await?
|
fsync_spawn_blocking(move || fsync_dir_std(dir)).await?
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(unix))]
|
#[cfg(not(unix))]
|
||||||
@@ -683,7 +683,7 @@ async fn fsync_open_dst_dir_group(group: &DstDirFsyncGroup) -> io::Result<()> {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
let dir = group.dir.clone();
|
let dir = group.dir.clone();
|
||||||
let dir_file = group.dir_file.clone();
|
let dir_file = group.dir_file.clone();
|
||||||
tokio::task::spawn_blocking(move || {
|
fsync_spawn_blocking(move || {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
{
|
{
|
||||||
if let Some(kind) = fsync_dir_recorder::take_grouped_failure(&dir) {
|
if let Some(kind) = fsync_dir_recorder::take_grouped_failure(&dir) {
|
||||||
@@ -1080,6 +1080,44 @@ const TEST_GLOBAL_FILE_SYNCS: usize = 64;
|
|||||||
|
|
||||||
static FILE_SYNC_PERMITS: LazyLock<Semaphore> = LazyLock::new(|| Semaphore::new(global_file_sync_limit()));
|
static FILE_SYNC_PERMITS: LazyLock<Semaphore> = LazyLock::new(|| Semaphore::new(global_file_sync_limit()));
|
||||||
static DISK_FILE_SYNC_LIMITERS: LazyLock<Mutex<HashMap<PathBuf, Weak<Semaphore>>>> = LazyLock::new(|| Mutex::new(HashMap::new()));
|
static DISK_FILE_SYNC_LIMITERS: LazyLock<Mutex<HashMap<PathBuf, Weak<Semaphore>>>> = LazyLock::new(|| Mutex::new(HashMap::new()));
|
||||||
|
|
||||||
|
/// Dedicated tokio runtime for fsync/fdatasync blocking operations. When
|
||||||
|
/// configured with >1 threads, isolates device-bound fsync from the main
|
||||||
|
/// blocking pool so reads (pread/stat/open) are not starved. `None` means
|
||||||
|
/// fall back to the main runtime (zero behavior change).
|
||||||
|
static FSYNC_RUNTIME: LazyLock<Option<tokio::runtime::Runtime>> = LazyLock::new(|| {
|
||||||
|
let threads =
|
||||||
|
rustfs_utils::get_env_usize(rustfs_config::ENV_FSYNC_BLOCKING_THREADS, rustfs_config::DEFAULT_FSYNC_BLOCKING_THREADS);
|
||||||
|
if threads <= 1 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let mut builder = tokio::runtime::Builder::new_multi_thread();
|
||||||
|
builder
|
||||||
|
.worker_threads(num_cpus::get().min(8))
|
||||||
|
.max_blocking_threads(threads)
|
||||||
|
.thread_name("rustfs-fsync")
|
||||||
|
.thread_stack_size(512 * 1024)
|
||||||
|
.enable_all();
|
||||||
|
match builder.build() {
|
||||||
|
Ok(rt) => {
|
||||||
|
tracing::info!(threads, "fsync dedicated blocking pool enabled");
|
||||||
|
Some(rt)
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
tracing::warn!(%err, "failed to build fsync runtime, falling back to main pool");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Spawn a blocking task on the fsync-dedicated runtime if configured,
|
||||||
|
/// otherwise fall back to the main tokio blocking pool.
|
||||||
|
fn fsync_spawn_blocking<T: Send + 'static>(f: impl FnOnce() -> T + Send + 'static) -> tokio::task::JoinHandle<T> {
|
||||||
|
match FSYNC_RUNTIME.as_ref() {
|
||||||
|
Some(rt) => rt.spawn_blocking(f),
|
||||||
|
None => tokio::task::spawn_blocking(f),
|
||||||
|
}
|
||||||
|
}
|
||||||
static DISK_VOLUME_MUTATION_LOCKS: LazyLock<Mutex<HashMap<PathBuf, Weak<RwLock<()>>>>> =
|
static DISK_VOLUME_MUTATION_LOCKS: LazyLock<Mutex<HashMap<PathBuf, Weak<RwLock<()>>>>> =
|
||||||
LazyLock::new(|| Mutex::new(HashMap::new()));
|
LazyLock::new(|| Mutex::new(HashMap::new()));
|
||||||
type NamespaceMutationLock = AsyncMutex<()>;
|
type NamespaceMutationLock = AsyncMutex<()>;
|
||||||
@@ -1217,7 +1255,7 @@ where
|
|||||||
F: FnOnce() -> io::Result<T> + Send + 'static,
|
F: FnOnce() -> io::Result<T> + Send + 'static,
|
||||||
{
|
{
|
||||||
let (disk_permit, global_permit) = acquire_file_sync_permits(disk_permits).await?;
|
let (disk_permit, global_permit) = acquire_file_sync_permits(disk_permits).await?;
|
||||||
let result = tokio::task::spawn_blocking(move || {
|
let result = fsync_spawn_blocking(move || {
|
||||||
let _disk_permit = disk_permit;
|
let _disk_permit = disk_permit;
|
||||||
work()
|
work()
|
||||||
})
|
})
|
||||||
@@ -2146,7 +2184,7 @@ async fn run_blocking_namespace_file_sync_operation_with_global<T: Send + 'stati
|
|||||||
wait_started,
|
wait_started,
|
||||||
);
|
);
|
||||||
let disk_permit = admission.disk_permit.clone();
|
let disk_permit = admission.disk_permit.clone();
|
||||||
let result = tokio::task::spawn_blocking(move || {
|
let result = fsync_spawn_blocking(move || {
|
||||||
let _lease = lease;
|
let _lease = lease;
|
||||||
let _disk_permit = disk_permit;
|
let _disk_permit = disk_permit;
|
||||||
operation()
|
operation()
|
||||||
|
|||||||
@@ -297,10 +297,16 @@ impl ECStore {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::bucket::metadata_sys;
|
||||||
use crate::core::pools::{PoolDecommissionInfo, PoolStatus};
|
use crate::core::pools::{PoolDecommissionInfo, PoolStatus};
|
||||||
use crate::disk::{DiskOption, format::FormatV3, new_disk};
|
use crate::disk::{DeleteOptions, DiskOption, format::FormatV3, new_disk};
|
||||||
use crate::layout::endpoints::{Endpoints, PoolEndpoints};
|
use crate::layout::endpoints::{EndpointServerPools, Endpoints, PoolEndpoints};
|
||||||
|
use crate::runtime::instance::InstanceContext;
|
||||||
|
use crate::storage_api_contracts::bucket::{BucketOperations, MakeBucketOptions};
|
||||||
|
use crate::storage_api_contracts::object::{ObjectIO as _, ObjectOperations};
|
||||||
use crate::store::init_format::{load_format_erasure, save_format_file};
|
use crate::store::init_format::{load_format_erasure, save_format_file};
|
||||||
|
use crate::store::init_local_disks_with_instance_ctx;
|
||||||
|
use tokio_util::sync::CancellationToken;
|
||||||
|
|
||||||
async fn minimal_heal_pool(pool_idx: usize) -> Arc<Sets> {
|
async fn minimal_heal_pool(pool_idx: usize) -> Arc<Sets> {
|
||||||
let format = FormatV3::new(1, 1);
|
let format = FormatV3::new(1, 1);
|
||||||
@@ -347,6 +353,51 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn multi_pool_heal_store() -> (tempfile::TempDir, Arc<ECStore>, CancellationToken) {
|
||||||
|
let temp_dir = tempfile::tempdir().expect("multi-pool heal test directory should be created");
|
||||||
|
let mut pool_endpoints = Vec::new();
|
||||||
|
for pool_index in 0..2 {
|
||||||
|
let mut endpoints = Vec::new();
|
||||||
|
for disk_index in 0..4 {
|
||||||
|
let disk_path = temp_dir.path().join(format!("pool{pool_index}-disk{disk_index}"));
|
||||||
|
tokio::fs::create_dir_all(&disk_path)
|
||||||
|
.await
|
||||||
|
.expect("multi-pool heal test disk should be created");
|
||||||
|
let mut endpoint = Endpoint::try_from(disk_path.to_str().expect("disk path should be utf8"))
|
||||||
|
.expect("test endpoint should parse");
|
||||||
|
endpoint.set_pool_index(pool_index);
|
||||||
|
endpoint.set_set_index(0);
|
||||||
|
endpoint.set_disk_index(disk_index);
|
||||||
|
endpoints.push(endpoint);
|
||||||
|
}
|
||||||
|
pool_endpoints.push(PoolEndpoints {
|
||||||
|
legacy: false,
|
||||||
|
set_count: 1,
|
||||||
|
drives_per_set: 4,
|
||||||
|
endpoints: Endpoints::from(endpoints),
|
||||||
|
cmd_line: format!("heal-owner-pool-{pool_index}"),
|
||||||
|
platform: "test".to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let endpoint_pools = EndpointServerPools::from(pool_endpoints);
|
||||||
|
let instance_ctx = Arc::new(InstanceContext::new());
|
||||||
|
init_local_disks_with_instance_ctx(&instance_ctx, endpoint_pools.clone())
|
||||||
|
.await
|
||||||
|
.expect("multi-pool local disks should initialize");
|
||||||
|
let shutdown = CancellationToken::new();
|
||||||
|
let store = ECStore::new_with_instance_ctx(
|
||||||
|
"127.0.0.1:0".parse().expect("test address should parse"),
|
||||||
|
endpoint_pools,
|
||||||
|
shutdown.clone(),
|
||||||
|
instance_ctx,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("multi-pool test store should initialize");
|
||||||
|
metadata_sys::init_bucket_metadata_sys(store.clone(), Vec::new()).await;
|
||||||
|
(temp_dir, store, shutdown)
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn heal_object_pool_scope_selects_only_requested_pool() {
|
async fn heal_object_pool_scope_selects_only_requested_pool() {
|
||||||
let store = minimal_heal_store().await;
|
let store = minimal_heal_store().await;
|
||||||
@@ -506,6 +557,204 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[serial_test::serial]
|
||||||
|
async fn unscoped_heal_object_suspended_owner_semantics() {
|
||||||
|
let (_temp_dir, store, shutdown) = multi_pool_heal_store().await;
|
||||||
|
let bucket = format!("heal-owner-{}", Uuid::new_v4().simple());
|
||||||
|
let active_object = "active-owner";
|
||||||
|
let suspended_only_object = "suspended-only";
|
||||||
|
let duplicate_object = "duplicate-owner";
|
||||||
|
let marker_object = "marker-owner";
|
||||||
|
let quorum_object = "quorum-owner";
|
||||||
|
store
|
||||||
|
.make_bucket(&bucket, &MakeBucketOptions::default())
|
||||||
|
.await
|
||||||
|
.expect("bucket should be created in all pools");
|
||||||
|
|
||||||
|
let mut active_reader = PutObjReader::from_vec(b"active owner".to_vec());
|
||||||
|
store.pools[0]
|
||||||
|
.put_object(&bucket, active_object, &mut active_reader, &ObjectOptions::default())
|
||||||
|
.await
|
||||||
|
.expect("active owner object should be written");
|
||||||
|
let active_disks = store.pools[0].disk_set[0].disks.read().await.clone();
|
||||||
|
let missing_active_disk = active_disks[0].clone().expect("active disk should be online");
|
||||||
|
missing_active_disk
|
||||||
|
.delete(
|
||||||
|
&bucket,
|
||||||
|
active_object,
|
||||||
|
DeleteOptions {
|
||||||
|
recursive: true,
|
||||||
|
immediate: true,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("active owner shard should be removed for repair");
|
||||||
|
assert!(
|
||||||
|
missing_active_disk.read_xl(&bucket, active_object, false).await.is_err(),
|
||||||
|
"the active owner fixture must start with one missing metadata copy"
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut suspended_reader = PutObjReader::from_vec(b"suspended owner".to_vec());
|
||||||
|
store.pools[1]
|
||||||
|
.put_object(&bucket, suspended_only_object, &mut suspended_reader, &ObjectOptions::default())
|
||||||
|
.await
|
||||||
|
.expect("suspended owner object should be written");
|
||||||
|
for (pool_index, mod_time) in [1_i64, 2_i64].into_iter().enumerate() {
|
||||||
|
let mut duplicate_reader = PutObjReader::from_vec(format!("duplicate-pool-{pool_index}").into_bytes());
|
||||||
|
store.pools[pool_index]
|
||||||
|
.put_object(
|
||||||
|
&bucket,
|
||||||
|
duplicate_object,
|
||||||
|
&mut duplicate_reader,
|
||||||
|
&ObjectOptions {
|
||||||
|
mod_time: Some(OffsetDateTime::UNIX_EPOCH + time::Duration::seconds(mod_time)),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("duplicate owner object should be written");
|
||||||
|
}
|
||||||
|
let history_version = Uuid::new_v4();
|
||||||
|
let mut history_reader = PutObjReader::from_vec(b"marker history".to_vec());
|
||||||
|
store.pools[0]
|
||||||
|
.put_object(
|
||||||
|
&bucket,
|
||||||
|
marker_object,
|
||||||
|
&mut history_reader,
|
||||||
|
&ObjectOptions {
|
||||||
|
versioned: true,
|
||||||
|
version_id: Some(history_version.to_string()),
|
||||||
|
mod_time: Some(OffsetDateTime::UNIX_EPOCH + time::Duration::seconds(1)),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("versioned marker history should be written");
|
||||||
|
store.pools[0]
|
||||||
|
.delete_object(
|
||||||
|
&bucket,
|
||||||
|
marker_object,
|
||||||
|
ObjectOptions {
|
||||||
|
versioned: true,
|
||||||
|
mod_time: Some(OffsetDateTime::UNIX_EPOCH + time::Duration::seconds(2)),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("delete marker should be written");
|
||||||
|
let mut quorum_reader = PutObjReader::from_vec(b"quorum boundary".to_vec());
|
||||||
|
store.pools[0]
|
||||||
|
.put_object(&bucket, quorum_object, &mut quorum_reader, &ObjectOptions::default())
|
||||||
|
.await
|
||||||
|
.expect("quorum boundary object should be written");
|
||||||
|
{
|
||||||
|
let mut pool_meta = store.pool_meta.write().await;
|
||||||
|
let mut next = PoolMeta::new(&store.pools, &pool_meta);
|
||||||
|
next.pools[1].decommission = Some(PoolDecommissionInfo {
|
||||||
|
start_time: Some(OffsetDateTime::UNIX_EPOCH),
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
*pool_meta = next;
|
||||||
|
}
|
||||||
|
|
||||||
|
let (_, duplicate_owner) = store
|
||||||
|
.get_latest_object_info_with_idx(&bucket, duplicate_object, &ObjectOptions::default())
|
||||||
|
.await
|
||||||
|
.expect("duplicate owner should resolve");
|
||||||
|
assert_eq!(duplicate_owner, 1, "latest duplicate must win when all pools are eligible");
|
||||||
|
let (_, active_duplicate_owner) = store
|
||||||
|
.get_latest_object_info_with_idx(
|
||||||
|
&bucket,
|
||||||
|
duplicate_object,
|
||||||
|
&ObjectOptions {
|
||||||
|
skip_decommissioned: true,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("active duplicate owner should resolve");
|
||||||
|
assert_eq!(
|
||||||
|
active_duplicate_owner, 0,
|
||||||
|
"suspended duplicate must be excluded from active owner selection"
|
||||||
|
);
|
||||||
|
let (marker_info, marker_owner) = store
|
||||||
|
.get_latest_object_info_with_idx(
|
||||||
|
&bucket,
|
||||||
|
marker_object,
|
||||||
|
&ObjectOptions {
|
||||||
|
skip_decommissioned: true,
|
||||||
|
versioned: true,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("latest delete marker should resolve");
|
||||||
|
assert_eq!(marker_owner, 0);
|
||||||
|
assert!(marker_info.delete_marker, "latest version must preserve delete-marker semantics");
|
||||||
|
|
||||||
|
let (active_result, active_err) = store
|
||||||
|
.handle_heal_object(&bucket, active_object, "", &HealOpts::default())
|
||||||
|
.await
|
||||||
|
.expect("unscoped active-owner heal should complete");
|
||||||
|
assert_eq!(active_result.object, active_object);
|
||||||
|
assert!(active_err.is_none(), "active owner must be selected even with a suspended pool");
|
||||||
|
assert!(
|
||||||
|
missing_active_disk.read_xl(&bucket, active_object, false).await.is_ok(),
|
||||||
|
"active owner heal must write the missing disk metadata: result={active_result:?}, err={active_err:?}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
store.pools[1]
|
||||||
|
.get_object_info(&bucket, active_object, &ObjectOptions::default())
|
||||||
|
.await
|
||||||
|
.is_err(),
|
||||||
|
"the suspended pool must not be written for an active-owner object"
|
||||||
|
);
|
||||||
|
|
||||||
|
let (suspended_result, suspended_err) = store
|
||||||
|
.handle_heal_object(&bucket, suspended_only_object, "", &HealOpts::default())
|
||||||
|
.await
|
||||||
|
.expect("unscoped suspended-only heal should return a terminal result");
|
||||||
|
assert!(suspended_result.object.is_empty());
|
||||||
|
assert!(matches!(suspended_err, Some(Error::FileNotFound)));
|
||||||
|
assert!(
|
||||||
|
store.pools[1]
|
||||||
|
.get_object_info(&bucket, suspended_only_object, &ObjectOptions::default())
|
||||||
|
.await
|
||||||
|
.is_ok(),
|
||||||
|
"suspended-only data must remain untouched when unscoped heal reports absent"
|
||||||
|
);
|
||||||
|
|
||||||
|
let (_, explicit_err) = store
|
||||||
|
.handle_heal_object(
|
||||||
|
&bucket,
|
||||||
|
suspended_only_object,
|
||||||
|
"",
|
||||||
|
&HealOpts {
|
||||||
|
pool: Some(1),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("explicit suspended-owner heal should return a mapped error");
|
||||||
|
assert!(matches!(explicit_err, Some(Error::SlowDown)));
|
||||||
|
|
||||||
|
let original_quorum_disks = store.pools[0].disk_set[0].disks.read().await.clone();
|
||||||
|
let surviving_quorum_disk = original_quorum_disks[3].clone();
|
||||||
|
*store.pools[0].disk_set[0].disks.write().await = vec![None, None, None, surviving_quorum_disk];
|
||||||
|
let (_, quorum_err) = store
|
||||||
|
.handle_heal_object(&bucket, quorum_object, "", &HealOpts::default())
|
||||||
|
.await
|
||||||
|
.expect("quorum boundary heal should return a mapped result");
|
||||||
|
*store.pools[0].disk_set[0].disks.write().await = original_quorum_disks;
|
||||||
|
assert!(
|
||||||
|
matches!(quorum_err, Some(Error::ErasureReadQuorum)),
|
||||||
|
"quorum-boundary heal must preserve quorum error, got {quorum_err:?}"
|
||||||
|
);
|
||||||
|
shutdown.cancel();
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn handle_heal_format_continues_after_a_pool_error() {
|
async fn handle_heal_format_continues_after_a_pool_error() {
|
||||||
let canonical_format = FormatV3::new(1, 3);
|
let canonical_format = FormatV3::new(1, 3);
|
||||||
|
|||||||
@@ -206,6 +206,13 @@ def check_runner_selection(root: Path) -> list[str]:
|
|||||||
return errors
|
return errors
|
||||||
|
|
||||||
|
|
||||||
|
def check_s3_tests_runner(root: Path) -> list[str]:
|
||||||
|
runner = (root / "scripts/s3-tests/run.sh").read_text()
|
||||||
|
if "--showlocals" in runner:
|
||||||
|
return ["scripts/s3-tests/run.sh: pytest failure diagnostics must not dump local values"]
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
def profile_selection(root: Path, profile: str) -> str:
|
def profile_selection(root: Path, profile: str) -> str:
|
||||||
if not re.fullmatch(r"e2e-[a-z0-9-]+", profile):
|
if not re.fullmatch(r"e2e-[a-z0-9-]+", profile):
|
||||||
raise ValueError(f"invalid e2e profile name: {profile}")
|
raise ValueError(f"invalid e2e profile name: {profile}")
|
||||||
@@ -272,6 +279,7 @@ def validate(root: Path) -> list[str]:
|
|||||||
errors.extend(check_e2e_modules(root))
|
errors.extend(check_e2e_modules(root))
|
||||||
errors.extend(check_fuzz_targets(root))
|
errors.extend(check_fuzz_targets(root))
|
||||||
errors.extend(check_runner_selection(root))
|
errors.extend(check_runner_selection(root))
|
||||||
|
errors.extend(check_s3_tests_runner(root))
|
||||||
errors.extend(check_profile_definitions(root))
|
errors.extend(check_profile_definitions(root))
|
||||||
return errors
|
return errors
|
||||||
|
|
||||||
@@ -341,6 +349,23 @@ class SelfTests(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
self.assertEqual(len(check_fuzz_targets(root)), 1)
|
self.assertEqual(len(check_fuzz_targets(root)), 1)
|
||||||
|
|
||||||
|
def test_s3_runner_rejects_unbounded_failure_locals(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
root = Path(tmp)
|
||||||
|
runner = root / "scripts/s3-tests/run.sh"
|
||||||
|
runner.parent.mkdir(parents=True)
|
||||||
|
runner.write_text("tox -- -vv -ra --tb=long\n")
|
||||||
|
self.assertEqual(check_s3_tests_runner(root), [])
|
||||||
|
runner.write_text("tox -- -vv -ra --showlocals --tb=long\n")
|
||||||
|
self.assertEqual(len(check_s3_tests_runner(root)), 1)
|
||||||
|
with (
|
||||||
|
mock.patch(__name__ + ".check_e2e_modules", return_value=[]),
|
||||||
|
mock.patch(__name__ + ".check_fuzz_targets", return_value=[]),
|
||||||
|
mock.patch(__name__ + ".check_runner_selection", return_value=[]),
|
||||||
|
mock.patch(__name__ + ".check_profile_definitions", return_value=[]),
|
||||||
|
):
|
||||||
|
self.assertEqual(len(validate(root)), 1)
|
||||||
|
|
||||||
def test_profile_listing_enforces_selection(self) -> None:
|
def test_profile_listing_enforces_selection(self) -> None:
|
||||||
with tempfile.TemporaryDirectory() as tmp:
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
root = Path(tmp)
|
root = Path(tmp)
|
||||||
@@ -411,7 +436,7 @@ def main() -> int:
|
|||||||
for error in errors:
|
for error in errors:
|
||||||
print(f"ERROR: {error}", file=sys.stderr)
|
print(f"ERROR: {error}", file=sys.stderr)
|
||||||
return 1
|
return 1
|
||||||
print("OK: e2e modules, runner selection, fuzz matrices, and profile guards are wired")
|
print("OK: e2e modules, runner selection, fuzz matrices, profiles, and bounded diagnostics are wired")
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1028,10 +1028,11 @@ else
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Run tests from s3tests/functional
|
# Run tests from s3tests/functional
|
||||||
|
# Failure locals can contain multi-MiB request bodies; keep tracebacks without expanding local values.
|
||||||
set +e
|
set +e
|
||||||
S3TEST_CONF="${CONF_OUTPUT_PATH}" \
|
S3TEST_CONF="${CONF_OUTPUT_PATH}" \
|
||||||
tox -- \
|
tox -- \
|
||||||
-vv -ra --showlocals --tb=long \
|
-vv -ra --tb=long \
|
||||||
--maxfail="${MAXFAIL}" \
|
--maxfail="${MAXFAIL}" \
|
||||||
--timeout="${TEST_TIMEOUT}" \
|
--timeout="${TEST_TIMEOUT}" \
|
||||||
--junitxml="${ARTIFACTS_DIR}/junit.xml" \
|
--junitxml="${ARTIFACTS_DIR}/junit.xml" \
|
||||||
|
|||||||
Reference in New Issue
Block a user