mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-20 03:22:18 +00:00
test(ecstore): assert the error conversions, and stop the census over-reporting (#6241)
This commit is contained in:
@@ -853,13 +853,32 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_error_conversions() {
|
fn test_error_conversions() {
|
||||||
// Test From implementations
|
// A plain io::Error carries no typed payload to recover, so it lands in
|
||||||
|
// `Io` rather than being guessed at from its kind — `NotFound` here must
|
||||||
|
// not silently become `FileNotFound`, which quorum aggregation counts as
|
||||||
|
// a different error (rustfs/backlog#1836).
|
||||||
let io_error = std::io::Error::new(std::io::ErrorKind::NotFound, "test");
|
let io_error = std::io::Error::new(std::io::ErrorKind::NotFound, "test");
|
||||||
let _disk_error: DiskError = io_error.into();
|
let disk_error: DiskError = io_error.into();
|
||||||
|
match &disk_error {
|
||||||
|
DiskError::Io(inner) => assert_eq!(inner.kind(), std::io::ErrorKind::NotFound),
|
||||||
|
other => panic!("a plain io::Error must stay typed as Io, got {other:?}"),
|
||||||
|
}
|
||||||
|
|
||||||
let json_str = r#"{"invalid": json}"#; // Invalid JSON
|
// A typed DiskError boxed through io::Error round-trips back to itself
|
||||||
|
// instead of degrading to `Io`.
|
||||||
|
let boxed: std::io::Error = std::io::Error::other(DiskError::VolumeNotFound);
|
||||||
|
assert_eq!(DiskError::from(boxed), DiskError::VolumeNotFound);
|
||||||
|
|
||||||
|
// serde_json errors have no dedicated variant and fold into `other`,
|
||||||
|
// keeping the original message.
|
||||||
|
let json_str = r#"{"invalid": json}"#;
|
||||||
let json_error = serde_json::from_str::<serde_json::Value>(json_str).unwrap_err();
|
let json_error = serde_json::from_str::<serde_json::Value>(json_str).unwrap_err();
|
||||||
let _disk_error: DiskError = json_error.into();
|
let json_message = json_error.to_string();
|
||||||
|
let disk_error: DiskError = json_error.into();
|
||||||
|
assert!(
|
||||||
|
disk_error.to_string().contains(&json_message),
|
||||||
|
"the json error message must survive the conversion: {disk_error}"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -436,30 +436,45 @@ mod tests {
|
|||||||
assert_eq!(unknown_profile.sequential_boost_multiplier, 1.0);
|
assert_eq!(unknown_profile.sequential_boost_multiplier, 1.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
// What platform probing returns depends on the machine, so these pin the two
|
||||||
|
// rules that do not: the override wins over probing, and probing that is
|
||||||
|
// switched off reports Unknown rather than guessing (rustfs/backlog#1836).
|
||||||
#[test]
|
#[test]
|
||||||
fn test_linux_storage_detection_exists() {
|
fn storage_media_override_wins_over_platform_detection() {
|
||||||
// This test just verifies the detection function exists and doesn't panic
|
for (override_value, expected) in [
|
||||||
// The actual result depends on the system it's running on
|
("nvme", StorageMedia::Nvme),
|
||||||
let result = detect_storage_media(true, "");
|
("ssd", StorageMedia::Ssd),
|
||||||
// We should get some result (not panic)
|
("hdd", StorageMedia::Hdd),
|
||||||
match result {
|
] {
|
||||||
StorageMedia::Nvme | StorageMedia::Ssd | StorageMedia::Hdd | StorageMedia::Unknown => {
|
assert_eq!(detect_storage_media(true, override_value), expected);
|
||||||
// All valid results
|
assert_eq!(
|
||||||
}
|
detect_storage_media(false, override_value),
|
||||||
|
expected,
|
||||||
|
"an override must be honoured even with detection disabled"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_macos_storage_detection_exists() {
|
fn disabled_detection_reports_unknown_instead_of_guessing() {
|
||||||
// This test just verifies the detection function exists and doesn't panic
|
assert_eq!(detect_storage_media(false, ""), StorageMedia::Unknown);
|
||||||
let result = detect_storage_media(true, "");
|
assert_eq!(
|
||||||
// We should get some result (not panic)
|
detect_storage_media(false, "not-a-medium"),
|
||||||
match result {
|
StorageMedia::Unknown,
|
||||||
StorageMedia::Nvme | StorageMedia::Ssd | StorageMedia::Hdd | StorageMedia::Unknown => {
|
"an unparseable override falls through to the disabled path"
|
||||||
// All valid results
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
#[test]
|
||||||
|
fn enabled_detection_returns_a_medium_for_this_platform() {
|
||||||
|
// Whatever this machine reports, it must be one of the known variants and
|
||||||
|
// it must be stable across calls — a probe that flapped would make the
|
||||||
|
// scheduler's profile depend on when it asked.
|
||||||
|
let first = detect_storage_media(true, "");
|
||||||
|
assert!(matches!(
|
||||||
|
first,
|
||||||
|
StorageMedia::Nvme | StorageMedia::Ssd | StorageMedia::Hdd | StorageMedia::Unknown
|
||||||
|
));
|
||||||
|
assert_eq!(detect_storage_media(true, ""), first);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -527,9 +527,19 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn runtime_facade_stops_empty_replay_workers() {
|
async fn stopping_replay_workers_is_a_no_op_when_there_are_none() {
|
||||||
let (facade, _, _) = build_facade();
|
let (facade, _, _) = build_facade();
|
||||||
|
|
||||||
facade.stop_replay_workers().await;
|
facade.stop_replay_workers().await;
|
||||||
|
|
||||||
|
// The stop path takes the worker list and hands it to the adapter, so an
|
||||||
|
// empty facade must come back with the list still empty and dispatch
|
||||||
|
// released rather than left paused (rustfs/backlog#1836).
|
||||||
|
assert!(facade.replay_workers.read().await.is_empty());
|
||||||
|
|
||||||
|
// Calling it twice must stay harmless: shutdown paths do exactly that.
|
||||||
|
facade.stop_replay_workers().await;
|
||||||
|
assert!(facade.replay_workers.read().await.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|||||||
@@ -873,9 +873,16 @@ mod tests {
|
|||||||
/// now return a finite, non-panicking mask.
|
/// now return a finite, non-panicking mask.
|
||||||
#[test]
|
#[test]
|
||||||
fn test_mask_never_recurses_for_any_variant() {
|
fn test_mask_never_recurses_for_any_variant() {
|
||||||
for ev in ALL_EVENT_NAMES {
|
// Terminating is the point — a regression here overflows the stack rather
|
||||||
// Must terminate (no infinite recursion / stack overflow).
|
// than failing an assertion — but the masks are collected and checked so
|
||||||
let _ = ev.mask();
|
// the loop cannot be optimised into nothing and so a variant that starts
|
||||||
|
// returning an empty mask is caught too (rustfs/backlog#1836).
|
||||||
|
let masks: Vec<u64> = ALL_EVENT_NAMES.iter().map(|ev| ev.mask()).collect();
|
||||||
|
|
||||||
|
assert_eq!(masks.len(), ALL_EVENT_NAMES.len());
|
||||||
|
for (ev, mask) in ALL_EVENT_NAMES.iter().zip(&masks) {
|
||||||
|
assert_ne!(*mask, 0, "{ev:?} must carry at least one bit");
|
||||||
|
assert_eq!(ev.mask(), *mask, "{ev:?} must return the same mask every call");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -49,19 +49,47 @@ import sys
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
VERIFY_SIGNALS = re.compile(
|
VERIFY_SIGNALS = re.compile(
|
||||||
r"assert!|assert_eq!|assert_ne!|debug_assert|panic!\(|\.expect\(|\.unwrap\(|"
|
r"assert[a-z0-9_]*!|debug_assert|panic!\(|\.expect\(|\.unwrap\(|"
|
||||||
r"unreachable!|matches!\(|insta::|proptest!|\.await\?|\)\?|\?;|should_panic"
|
r"unreachable!|matches!\(|insta::|proptest!|\.await\?|\)\?|\?;|should_panic"
|
||||||
)
|
)
|
||||||
DELEGATION = re.compile(
|
DELEGATION = re.compile(
|
||||||
r"\b(?:assert|verify|check|expect|ensure|run)_[a-z0-9_]*\s*\(|"
|
r"\b(?:assert|verify|check|expect|ensure|run)_[a-z0-9_]*(?:::<[^>]*>)?\s*\(|"
|
||||||
r"\b[a-z0-9_]+_(?:case|cases|harness|roundtrip|round_trip)\s*\("
|
r"\b[a-z0-9_]+_(?:case|cases|harness|roundtrip|round_trip)(?:::<[^>]*>)?\s*\("
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# A body whose whole content is one call delegates by construction, whatever the
|
||||||
|
# callee is named: `run(DurabilityMode::Strict).await` and
|
||||||
|
# `aborting_encode_drops_blocked_producer(EncodePipeline::Vec).await` both hand
|
||||||
|
# every assertion to a shared harness.
|
||||||
|
SINGLE_CALL_BODY = re.compile(
|
||||||
|
r"\A\s*[a-zA-Z_][a-zA-Z0-9_:]*(?:::<[^>]*>)?\s*\([^;]*\)\s*(?:\.await\s*)?;?\s*\Z",
|
||||||
|
re.S,
|
||||||
|
)
|
||||||
|
|
||||||
|
# A nested `fn` that is only bound and discarded is a signature guard: the type
|
||||||
|
# system is the assertion, exactly like the `fn _name()` form below.
|
||||||
|
SIGNATURE_GUARD = re.compile(r"\bfn\s+[a-zA-Z0-9_]+\s*(?:<[^>]*>)?\s*\([^;]*\)[^;]*\{", re.S)
|
||||||
|
DISCARDED_BINDING = re.compile(r"\blet\s+_\s*=\s*[a-zA-Z_][a-zA-Z0-9_]*\s*;")
|
||||||
|
# `let _ = Type::<T>::method;` — a path item referenced but never called can only
|
||||||
|
# be a signature guard; the call form (`let _ = x.foo();`) is excluded by the
|
||||||
|
# absence of parens before the semicolon.
|
||||||
|
DISCARDED_PATH_ITEM = re.compile(r"\blet\s+_\s*=\s*[a-zA-Z_][a-zA-Z0-9_]*(?:::(?:<[^>]*>|[a-zA-Z_][a-zA-Z0-9_]*))+\s*;")
|
||||||
COMPILE_TIME_CHECK = re.compile(r"\bfn\s+_[a-zA-Z0-9_]*\s*(?:<[^>]*>)?\s*\(")
|
COMPILE_TIME_CHECK = re.compile(r"\bfn\s+_[a-zA-Z0-9_]*\s*(?:<[^>]*>)?\s*\(")
|
||||||
TEST_ATTR = re.compile(r"#\[(?:tokio::)?test[\](]")
|
TEST_ATTR = re.compile(r"#\[(?:tokio::)?test[\](]")
|
||||||
TEST_CASE_ATTR = re.compile(r"#\[test_case")
|
TEST_CASE_ATTR = re.compile(r"#\[test_case")
|
||||||
FN_LINE = re.compile(r"^\s*(?:pub\s+)?(?:async\s+)?fn\s+([a-zA-Z0-9_]+)")
|
FN_LINE = re.compile(r"^\s*(?:pub\s+)?(?:async\s+)?fn\s+([a-zA-Z0-9_]+)")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def extract_body(text: str) -> str:
|
||||||
|
"""Return what is between the outermost braces of a scanned function."""
|
||||||
|
start = text.find("{")
|
||||||
|
end = text.rfind("}")
|
||||||
|
if start == -1 or end <= start:
|
||||||
|
return text
|
||||||
|
return text[start + 1 : end]
|
||||||
|
|
||||||
|
|
||||||
def scan_file(path: Path):
|
def scan_file(path: Path):
|
||||||
try:
|
try:
|
||||||
lines = path.read_text(encoding="utf-8").split("\n")
|
lines = path.read_text(encoding="utf-8").split("\n")
|
||||||
@@ -105,7 +133,17 @@ def scan_file(path: Path):
|
|||||||
break
|
break
|
||||||
k += 1
|
k += 1
|
||||||
text = "\n".join(body)
|
text = "\n".join(body)
|
||||||
if not VERIFY_SIGNALS.search(text) and not DELEGATION.search(text) and not COMPILE_TIME_CHECK.search(text):
|
# The attribute block carries verification too: `#[should_panic(expected
|
||||||
|
# = "...")]` makes the panic message the assertion.
|
||||||
|
attr_text = "\n".join(attrs)
|
||||||
|
inner = extract_body(text)
|
||||||
|
delegates = (
|
||||||
|
DELEGATION.search(text)
|
||||||
|
or SINGLE_CALL_BODY.match(inner)
|
||||||
|
or (SIGNATURE_GUARD.search(inner) and DISCARDED_BINDING.search(inner))
|
||||||
|
or DISCARDED_PATH_ITEM.search(inner)
|
||||||
|
)
|
||||||
|
if not VERIFY_SIGNALS.search(text) and not VERIFY_SIGNALS.search(attr_text) and not delegates and not COMPILE_TIME_CHECK.search(text):
|
||||||
print(f"{path}:{j + 1}: {name}")
|
print(f"{path}:{j + 1}: {name}")
|
||||||
i = k + 1
|
i = k + 1
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user