feat(scanner): expose cycle progress metrics (#3152)

Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
Co-authored-by: houseme <housemecn@gmail.com>
This commit is contained in:
Henry Guo
2026-06-01 15:54:39 +08:00
committed by GitHub
parent cbb6f9c76f
commit f3bd838925
6 changed files with 454 additions and 6 deletions
+136 -5
View File
@@ -22,9 +22,14 @@
use crate::metrics::report::PrometheusMetric;
use crate::metrics::schema::scanner::{
SCANNER_ACTIVE_PATHS_MD, SCANNER_BUCKET_SCANS_FINISHED_MD, SCANNER_BUCKET_SCANS_STARTED_MD, SCANNER_COMPLETED_CYCLES_MD,
SCANNER_CURRENT_CYCLE_AGE_SECONDS_MD, SCANNER_CURRENT_CYCLE_MD, SCANNER_CURRENT_SCAN_MODE_MD, SCANNER_DIRECTORIES_SCANNED_MD,
SCANNER_FAILED_CYCLES_MD, SCANNER_LAST_ACTIVITY_SECONDS_MD, SCANNER_LAST_CYCLE_DURATION_SECONDS_MD,
SCANNER_LAST_CYCLE_RESULT_MD, SCANNER_OBJECTS_SCANNED_MD, SCANNER_VERSIONS_SCANNED_MD,
SCANNER_CURRENT_CYCLE_AGE_SECONDS_MD, SCANNER_CURRENT_CYCLE_BUCKET_DRIVE_SCANS_MD,
SCANNER_CURRENT_CYCLE_BUCKET_DRIVE_SCANS_PER_SECOND_MD, SCANNER_CURRENT_CYCLE_DIRECTORIES_PER_SECOND_MD,
SCANNER_CURRENT_CYCLE_DIRECTORIES_SCANNED_MD, SCANNER_CURRENT_CYCLE_MD, SCANNER_CURRENT_CYCLE_OBJECTS_PER_SECOND_MD,
SCANNER_CURRENT_CYCLE_OBJECTS_SCANNED_MD, SCANNER_CURRENT_SCAN_MODE_MD, SCANNER_DIRECTORIES_SCANNED_MD,
SCANNER_FAILED_CYCLES_MD, SCANNER_LAST_ACTIVITY_SECONDS_MD, SCANNER_LAST_CYCLE_BUCKET_DRIVE_SCANS_MD,
SCANNER_LAST_CYCLE_BUCKET_DRIVE_SCANS_PER_SECOND_MD, SCANNER_LAST_CYCLE_DIRECTORIES_PER_SECOND_MD,
SCANNER_LAST_CYCLE_DIRECTORIES_SCANNED_MD, SCANNER_LAST_CYCLE_DURATION_SECONDS_MD, SCANNER_LAST_CYCLE_OBJECTS_PER_SECOND_MD,
SCANNER_LAST_CYCLE_OBJECTS_SCANNED_MD, SCANNER_LAST_CYCLE_RESULT_MD, SCANNER_OBJECTS_SCANNED_MD, SCANNER_VERSIONS_SCANNED_MD,
};
/// Scanner statistics.
@@ -50,12 +55,36 @@ pub struct ScannerStats {
pub completed_cycles: u64,
/// Seconds elapsed since the current scanner cycle started
pub current_cycle_age_seconds: u64,
/// Number of objects scanned by the currently running scanner cycle
pub current_cycle_objects_scanned: u64,
/// Number of directories scanned by the currently running scanner cycle
pub current_cycle_directories_scanned: u64,
/// Number of bucket-drive scans finished by the currently running scanner cycle
pub current_cycle_bucket_drive_scans: u64,
/// Object scan rate for the currently running scanner cycle
pub current_cycle_objects_per_second: f64,
/// Directory scan rate for the currently running scanner cycle
pub current_cycle_directories_per_second: f64,
/// Bucket-drive scan rate for the currently running scanner cycle
pub current_cycle_bucket_drive_scans_per_second: f64,
/// Current scanner mode: 0 unknown or idle, 1 normal, 2 deep bitrot scan
pub current_scan_mode: u64,
/// Last scanner cycle result: 0 unknown, 1 success, 2 error
pub last_cycle_result: u64,
/// Duration in seconds of the last finished scanner cycle
pub last_cycle_duration_seconds: f64,
/// Number of objects scanned by the last finished scanner cycle
pub last_cycle_objects_scanned: u64,
/// Number of directories scanned by the last finished scanner cycle
pub last_cycle_directories_scanned: u64,
/// Number of bucket-drive scans finished by the last scanner cycle
pub last_cycle_bucket_drive_scans: u64,
/// Object scan rate for the last finished scanner cycle
pub last_cycle_objects_per_second: f64,
/// Directory scan rate for the last finished scanner cycle
pub last_cycle_directories_per_second: f64,
/// Bucket-drive scan rate for the last finished scanner cycle
pub last_cycle_bucket_drive_scans_per_second: f64,
/// Number of scanner cycles that failed since server start
pub failed_cycles: u64,
}
@@ -76,9 +105,39 @@ pub fn collect_scanner_metrics(stats: &ScannerStats) -> Vec<PrometheusMetric> {
PrometheusMetric::from_descriptor(&SCANNER_CURRENT_CYCLE_MD, stats.current_cycle as f64),
PrometheusMetric::from_descriptor(&SCANNER_COMPLETED_CYCLES_MD, stats.completed_cycles as f64),
PrometheusMetric::from_descriptor(&SCANNER_CURRENT_CYCLE_AGE_SECONDS_MD, stats.current_cycle_age_seconds as f64),
PrometheusMetric::from_descriptor(&SCANNER_CURRENT_CYCLE_OBJECTS_SCANNED_MD, stats.current_cycle_objects_scanned as f64),
PrometheusMetric::from_descriptor(
&SCANNER_CURRENT_CYCLE_DIRECTORIES_SCANNED_MD,
stats.current_cycle_directories_scanned as f64,
),
PrometheusMetric::from_descriptor(
&SCANNER_CURRENT_CYCLE_BUCKET_DRIVE_SCANS_MD,
stats.current_cycle_bucket_drive_scans as f64,
),
PrometheusMetric::from_descriptor(&SCANNER_CURRENT_CYCLE_OBJECTS_PER_SECOND_MD, stats.current_cycle_objects_per_second),
PrometheusMetric::from_descriptor(
&SCANNER_CURRENT_CYCLE_DIRECTORIES_PER_SECOND_MD,
stats.current_cycle_directories_per_second,
),
PrometheusMetric::from_descriptor(
&SCANNER_CURRENT_CYCLE_BUCKET_DRIVE_SCANS_PER_SECOND_MD,
stats.current_cycle_bucket_drive_scans_per_second,
),
PrometheusMetric::from_descriptor(&SCANNER_CURRENT_SCAN_MODE_MD, stats.current_scan_mode as f64),
PrometheusMetric::from_descriptor(&SCANNER_LAST_CYCLE_RESULT_MD, stats.last_cycle_result as f64),
PrometheusMetric::from_descriptor(&SCANNER_LAST_CYCLE_DURATION_SECONDS_MD, stats.last_cycle_duration_seconds),
PrometheusMetric::from_descriptor(&SCANNER_LAST_CYCLE_OBJECTS_SCANNED_MD, stats.last_cycle_objects_scanned as f64),
PrometheusMetric::from_descriptor(
&SCANNER_LAST_CYCLE_DIRECTORIES_SCANNED_MD,
stats.last_cycle_directories_scanned as f64,
),
PrometheusMetric::from_descriptor(&SCANNER_LAST_CYCLE_BUCKET_DRIVE_SCANS_MD, stats.last_cycle_bucket_drive_scans as f64),
PrometheusMetric::from_descriptor(&SCANNER_LAST_CYCLE_OBJECTS_PER_SECOND_MD, stats.last_cycle_objects_per_second),
PrometheusMetric::from_descriptor(&SCANNER_LAST_CYCLE_DIRECTORIES_PER_SECOND_MD, stats.last_cycle_directories_per_second),
PrometheusMetric::from_descriptor(
&SCANNER_LAST_CYCLE_BUCKET_DRIVE_SCANS_PER_SECOND_MD,
stats.last_cycle_bucket_drive_scans_per_second,
),
PrometheusMetric::from_descriptor(&SCANNER_FAILED_CYCLES_MD, stats.failed_cycles as f64),
]
}
@@ -101,16 +160,28 @@ mod tests {
current_cycle: 12,
completed_cycles: 11,
current_cycle_age_seconds: 90,
current_cycle_objects_scanned: 250,
current_cycle_directories_scanned: 20,
current_cycle_bucket_drive_scans: 2,
current_cycle_objects_per_second: 12.5,
current_cycle_directories_per_second: 1.0,
current_cycle_bucket_drive_scans_per_second: 0.1,
current_scan_mode: 2,
last_cycle_result: 1,
last_cycle_duration_seconds: 42.5,
last_cycle_objects_scanned: 900,
last_cycle_directories_scanned: 80,
last_cycle_bucket_drive_scans: 6,
last_cycle_objects_per_second: 18.0,
last_cycle_directories_per_second: 1.6,
last_cycle_bucket_drive_scans_per_second: 0.12,
failed_cycles: 3,
};
let metrics = collect_scanner_metrics(&stats);
report_metrics(&metrics);
assert_eq!(metrics.len(), 14);
assert_eq!(metrics.len(), 26);
let objects = metrics.iter().find(|m| m.value == 1000000.0);
assert!(objects.is_some());
@@ -138,6 +209,36 @@ mod tests {
.find(|m| m.name == SCANNER_CURRENT_CYCLE_AGE_SECONDS_MD.get_full_metric_name());
assert_eq!(current_cycle_age.map(|m| m.value), Some(90.0));
let current_cycle_objects = metrics
.iter()
.find(|m| m.name == SCANNER_CURRENT_CYCLE_OBJECTS_SCANNED_MD.get_full_metric_name());
assert_eq!(current_cycle_objects.map(|m| m.value), Some(250.0));
let current_cycle_directories = metrics
.iter()
.find(|m| m.name == SCANNER_CURRENT_CYCLE_DIRECTORIES_SCANNED_MD.get_full_metric_name());
assert_eq!(current_cycle_directories.map(|m| m.value), Some(20.0));
let current_cycle_bucket_drive_scans = metrics
.iter()
.find(|m| m.name == SCANNER_CURRENT_CYCLE_BUCKET_DRIVE_SCANS_MD.get_full_metric_name());
assert_eq!(current_cycle_bucket_drive_scans.map(|m| m.value), Some(2.0));
let current_cycle_objects_rate = metrics
.iter()
.find(|m| m.name == SCANNER_CURRENT_CYCLE_OBJECTS_PER_SECOND_MD.get_full_metric_name());
assert_eq!(current_cycle_objects_rate.map(|m| m.value), Some(12.5));
let current_cycle_directories_rate = metrics
.iter()
.find(|m| m.name == SCANNER_CURRENT_CYCLE_DIRECTORIES_PER_SECOND_MD.get_full_metric_name());
assert_eq!(current_cycle_directories_rate.map(|m| m.value), Some(1.0));
let current_cycle_bucket_drive_scans_rate = metrics
.iter()
.find(|m| m.name == SCANNER_CURRENT_CYCLE_BUCKET_DRIVE_SCANS_PER_SECOND_MD.get_full_metric_name());
assert_eq!(current_cycle_bucket_drive_scans_rate.map(|m| m.value), Some(0.1));
let current_scan_mode = metrics
.iter()
.find(|m| m.name == SCANNER_CURRENT_SCAN_MODE_MD.get_full_metric_name());
@@ -153,6 +254,36 @@ mod tests {
.find(|m| m.name == SCANNER_LAST_CYCLE_DURATION_SECONDS_MD.get_full_metric_name());
assert_eq!(last_cycle_duration.map(|m| m.value), Some(42.5));
let last_cycle_objects = metrics
.iter()
.find(|m| m.name == SCANNER_LAST_CYCLE_OBJECTS_SCANNED_MD.get_full_metric_name());
assert_eq!(last_cycle_objects.map(|m| m.value), Some(900.0));
let last_cycle_directories = metrics
.iter()
.find(|m| m.name == SCANNER_LAST_CYCLE_DIRECTORIES_SCANNED_MD.get_full_metric_name());
assert_eq!(last_cycle_directories.map(|m| m.value), Some(80.0));
let last_cycle_bucket_drive_scans = metrics
.iter()
.find(|m| m.name == SCANNER_LAST_CYCLE_BUCKET_DRIVE_SCANS_MD.get_full_metric_name());
assert_eq!(last_cycle_bucket_drive_scans.map(|m| m.value), Some(6.0));
let last_cycle_objects_rate = metrics
.iter()
.find(|m| m.name == SCANNER_LAST_CYCLE_OBJECTS_PER_SECOND_MD.get_full_metric_name());
assert_eq!(last_cycle_objects_rate.map(|m| m.value), Some(18.0));
let last_cycle_directories_rate = metrics
.iter()
.find(|m| m.name == SCANNER_LAST_CYCLE_DIRECTORIES_PER_SECOND_MD.get_full_metric_name());
assert_eq!(last_cycle_directories_rate.map(|m| m.value), Some(1.6));
let last_cycle_bucket_drive_scans_rate = metrics
.iter()
.find(|m| m.name == SCANNER_LAST_CYCLE_BUCKET_DRIVE_SCANS_PER_SECOND_MD.get_full_metric_name());
assert_eq!(last_cycle_bucket_drive_scans_rate.map(|m| m.value), Some(0.12));
let failed_cycles = metrics
.iter()
.find(|m| m.name == SCANNER_FAILED_CYCLES_MD.get_full_metric_name());
@@ -164,7 +295,7 @@ mod tests {
let stats = ScannerStats::default();
let metrics = collect_scanner_metrics(&stats);
assert_eq!(metrics.len(), 14);
assert_eq!(metrics.len(), 26);
for metric in &metrics {
assert_eq!(metric.value, 0.0);
assert!(metric.labels.is_empty());
@@ -282,9 +282,21 @@ pub enum MetricName {
ScannerCurrentCycle,
ScannerCompletedCycles,
ScannerCurrentCycleAgeSeconds,
ScannerCurrentCycleObjectsScanned,
ScannerCurrentCycleDirectoriesScanned,
ScannerCurrentCycleBucketDriveScans,
ScannerCurrentCycleObjectsPerSecond,
ScannerCurrentCycleDirectoriesPerSecond,
ScannerCurrentCycleBucketDriveScansPerSecond,
ScannerCurrentScanMode,
ScannerLastCycleResult,
ScannerLastCycleDurationSeconds,
ScannerLastCycleObjectsScanned,
ScannerLastCycleDirectoriesScanned,
ScannerLastCycleBucketDriveScans,
ScannerLastCycleObjectsPerSecond,
ScannerLastCycleDirectoriesPerSecond,
ScannerLastCycleBucketDriveScansPerSecond,
ScannerFailedCycles,
// CPU system-related metrics
@@ -630,9 +642,21 @@ impl MetricName {
Self::ScannerCurrentCycle => "current_cycle".to_string(),
Self::ScannerCompletedCycles => "completed_cycles".to_string(),
Self::ScannerCurrentCycleAgeSeconds => "current_cycle_age_seconds".to_string(),
Self::ScannerCurrentCycleObjectsScanned => "current_cycle_objects_scanned".to_string(),
Self::ScannerCurrentCycleDirectoriesScanned => "current_cycle_directories_scanned".to_string(),
Self::ScannerCurrentCycleBucketDriveScans => "current_cycle_bucket_drive_scans".to_string(),
Self::ScannerCurrentCycleObjectsPerSecond => "current_cycle_objects_per_second".to_string(),
Self::ScannerCurrentCycleDirectoriesPerSecond => "current_cycle_directories_per_second".to_string(),
Self::ScannerCurrentCycleBucketDriveScansPerSecond => "current_cycle_bucket_drive_scans_per_second".to_string(),
Self::ScannerCurrentScanMode => "current_scan_mode".to_string(),
Self::ScannerLastCycleResult => "last_cycle_result".to_string(),
Self::ScannerLastCycleDurationSeconds => "last_cycle_duration_seconds".to_string(),
Self::ScannerLastCycleObjectsScanned => "last_cycle_objects_scanned".to_string(),
Self::ScannerLastCycleDirectoriesScanned => "last_cycle_directories_scanned".to_string(),
Self::ScannerLastCycleBucketDriveScans => "last_cycle_bucket_drive_scans".to_string(),
Self::ScannerLastCycleObjectsPerSecond => "last_cycle_objects_per_second".to_string(),
Self::ScannerLastCycleDirectoriesPerSecond => "last_cycle_directories_per_second".to_string(),
Self::ScannerLastCycleBucketDriveScansPerSecond => "last_cycle_bucket_drive_scans_per_second".to_string(),
Self::ScannerFailedCycles => "failed_cycles".to_string(),
// CPU system-related metrics
+108
View File
@@ -107,6 +107,60 @@ pub static SCANNER_CURRENT_CYCLE_AGE_SECONDS_MD: LazyLock<MetricDescriptor> = La
)
});
pub static SCANNER_CURRENT_CYCLE_OBJECTS_SCANNED_MD: LazyLock<MetricDescriptor> = LazyLock::new(|| {
new_gauge_md(
MetricName::ScannerCurrentCycleObjectsScanned,
"Number of objects scanned by the currently running scanner cycle.",
&[],
subsystems::SCANNER,
)
});
pub static SCANNER_CURRENT_CYCLE_DIRECTORIES_SCANNED_MD: LazyLock<MetricDescriptor> = LazyLock::new(|| {
new_gauge_md(
MetricName::ScannerCurrentCycleDirectoriesScanned,
"Number of directories scanned by the currently running scanner cycle.",
&[],
subsystems::SCANNER,
)
});
pub static SCANNER_CURRENT_CYCLE_BUCKET_DRIVE_SCANS_MD: LazyLock<MetricDescriptor> = LazyLock::new(|| {
new_gauge_md(
MetricName::ScannerCurrentCycleBucketDriveScans,
"Number of bucket-drive scans finished by the currently running scanner cycle.",
&[],
subsystems::SCANNER,
)
});
pub static SCANNER_CURRENT_CYCLE_OBJECTS_PER_SECOND_MD: LazyLock<MetricDescriptor> = LazyLock::new(|| {
new_gauge_md(
MetricName::ScannerCurrentCycleObjectsPerSecond,
"Object scan rate for the currently running scanner cycle.",
&[],
subsystems::SCANNER,
)
});
pub static SCANNER_CURRENT_CYCLE_DIRECTORIES_PER_SECOND_MD: LazyLock<MetricDescriptor> = LazyLock::new(|| {
new_gauge_md(
MetricName::ScannerCurrentCycleDirectoriesPerSecond,
"Directory scan rate for the currently running scanner cycle.",
&[],
subsystems::SCANNER,
)
});
pub static SCANNER_CURRENT_CYCLE_BUCKET_DRIVE_SCANS_PER_SECOND_MD: LazyLock<MetricDescriptor> = LazyLock::new(|| {
new_gauge_md(
MetricName::ScannerCurrentCycleBucketDriveScansPerSecond,
"Bucket-drive scan rate for the currently running scanner cycle.",
&[],
subsystems::SCANNER,
)
});
pub static SCANNER_CURRENT_SCAN_MODE_MD: LazyLock<MetricDescriptor> = LazyLock::new(|| {
new_gauge_md(
MetricName::ScannerCurrentScanMode,
@@ -134,6 +188,60 @@ pub static SCANNER_LAST_CYCLE_DURATION_SECONDS_MD: LazyLock<MetricDescriptor> =
)
});
pub static SCANNER_LAST_CYCLE_OBJECTS_SCANNED_MD: LazyLock<MetricDescriptor> = LazyLock::new(|| {
new_gauge_md(
MetricName::ScannerLastCycleObjectsScanned,
"Number of objects scanned by the last finished scanner cycle.",
&[],
subsystems::SCANNER,
)
});
pub static SCANNER_LAST_CYCLE_DIRECTORIES_SCANNED_MD: LazyLock<MetricDescriptor> = LazyLock::new(|| {
new_gauge_md(
MetricName::ScannerLastCycleDirectoriesScanned,
"Number of directories scanned by the last finished scanner cycle.",
&[],
subsystems::SCANNER,
)
});
pub static SCANNER_LAST_CYCLE_BUCKET_DRIVE_SCANS_MD: LazyLock<MetricDescriptor> = LazyLock::new(|| {
new_gauge_md(
MetricName::ScannerLastCycleBucketDriveScans,
"Number of bucket-drive scans finished by the last scanner cycle.",
&[],
subsystems::SCANNER,
)
});
pub static SCANNER_LAST_CYCLE_OBJECTS_PER_SECOND_MD: LazyLock<MetricDescriptor> = LazyLock::new(|| {
new_gauge_md(
MetricName::ScannerLastCycleObjectsPerSecond,
"Object scan rate for the last finished scanner cycle.",
&[],
subsystems::SCANNER,
)
});
pub static SCANNER_LAST_CYCLE_DIRECTORIES_PER_SECOND_MD: LazyLock<MetricDescriptor> = LazyLock::new(|| {
new_gauge_md(
MetricName::ScannerLastCycleDirectoriesPerSecond,
"Directory scan rate for the last finished scanner cycle.",
&[],
subsystems::SCANNER,
)
});
pub static SCANNER_LAST_CYCLE_BUCKET_DRIVE_SCANS_PER_SECOND_MD: LazyLock<MetricDescriptor> = LazyLock::new(|| {
new_gauge_md(
MetricName::ScannerLastCycleBucketDriveScansPerSecond,
"Bucket-drive scan rate for the last finished scanner cycle.",
&[],
subsystems::SCANNER,
)
});
pub static SCANNER_FAILED_CYCLES_MD: LazyLock<MetricDescriptor> = LazyLock::new(|| {
new_counter_md(
MetricName::ScannerFailedCycles,
+46
View File
@@ -65,6 +65,14 @@ fn scanner_scan_mode_code(scan_mode: &str) -> u64 {
}
}
fn scanner_work_rate_per_second(count: u64, seconds: f64) -> f64 {
if seconds > 0.0 && seconds.is_finite() {
count as f64 / seconds
} else {
0.0
}
}
const DRIVE_STATE_OK: &str = "ok";
const DRIVE_STATE_ONLINE: &str = "online";
const DRIVE_STATE_UNFORMATTED: &str = "unformatted";
@@ -901,6 +909,8 @@ pub async fn collect_scanner_metric_stats() -> Option<ScannerStats> {
let active_paths = metrics.active_scan_paths as u64;
let current_cycle_age_seconds = current_scanner_cycle_age_seconds(metrics.current_cycle, metrics.current_started, now);
let current_scan_mode = scanner_scan_mode_code(&metrics.current_scan_mode);
let current_cycle_age = current_cycle_age_seconds as f64;
let last_cycle_duration = metrics.last_cycle_duration_seconds;
Some(ScannerStats {
bucket_scans_finished,
@@ -916,9 +926,33 @@ pub async fn collect_scanner_metric_stats() -> Option<ScannerStats> {
current_cycle: metrics.current_cycle,
completed_cycles,
current_cycle_age_seconds,
current_cycle_objects_scanned: metrics.current_cycle_objects_scanned,
current_cycle_directories_scanned: metrics.current_cycle_directories_scanned,
current_cycle_bucket_drive_scans: metrics.current_cycle_bucket_drive_scans,
current_cycle_objects_per_second: scanner_work_rate_per_second(metrics.current_cycle_objects_scanned, current_cycle_age),
current_cycle_directories_per_second: scanner_work_rate_per_second(
metrics.current_cycle_directories_scanned,
current_cycle_age,
),
current_cycle_bucket_drive_scans_per_second: scanner_work_rate_per_second(
metrics.current_cycle_bucket_drive_scans,
current_cycle_age,
),
current_scan_mode,
last_cycle_result: metrics.last_cycle_result_code,
last_cycle_duration_seconds: metrics.last_cycle_duration_seconds,
last_cycle_objects_scanned: metrics.last_cycle_objects_scanned,
last_cycle_directories_scanned: metrics.last_cycle_directories_scanned,
last_cycle_bucket_drive_scans: metrics.last_cycle_bucket_drive_scans,
last_cycle_objects_per_second: scanner_work_rate_per_second(metrics.last_cycle_objects_scanned, last_cycle_duration),
last_cycle_directories_per_second: scanner_work_rate_per_second(
metrics.last_cycle_directories_scanned,
last_cycle_duration,
),
last_cycle_bucket_drive_scans_per_second: scanner_work_rate_per_second(
metrics.last_cycle_bucket_drive_scans,
last_cycle_duration,
),
failed_cycles: metrics.failed_cycles,
})
}
@@ -1021,4 +1055,16 @@ mod tests {
fn scanner_scan_mode_code_maps_unknown_mode() {
assert_eq!(scanner_scan_mode_code(""), HealScanMode::Unknown as u8 as u64);
}
#[test]
fn scanner_work_rate_per_second_reports_rate() {
assert_eq!(scanner_work_rate_per_second(90, 45.0), 2.0);
}
#[test]
fn scanner_work_rate_per_second_returns_zero_for_invalid_seconds() {
assert_eq!(scanner_work_rate_per_second(90, 0.0), 0.0);
assert_eq!(scanner_work_rate_per_second(90, f64::INFINITY), 0.0);
assert_eq!(scanner_work_rate_per_second(90, f64::NAN), 0.0);
}
}