diff --git a/src/backends/mod.rs b/src/backends/mod.rs index 195d22d..013e72d 100644 --- a/src/backends/mod.rs +++ b/src/backends/mod.rs @@ -409,8 +409,8 @@ mod tests { std::iter::from_fn(|| listener.accept().ok()).count() } - /// Test that discovery identifies a control-socket peer once and does - /// not reconnect on later passes until the socket is rebound. + /// Test that discovery identifies a control-socket peer once and does not + /// reconnect on later passes until the socket is rebound. #[tokio::test] async fn test_discovery_reuses_identified_control_socket_peer() { let socket_dir = tempfile::tempdir().unwrap(); diff --git a/src/backends/qemu/helpers.rs b/src/backends/qemu/helpers.rs index 1b120f6..01e85cf 100644 --- a/src/backends/qemu/helpers.rs +++ b/src/backends/qemu/helpers.rs @@ -325,8 +325,8 @@ mod tests { use super::*; proptest! { - /// Test that vQ round-robin mapping spreads queues across IOThreads - /// as evenly as possible. + /// Test that vQ round-robin mapping spreads queues across IOThreads as + /// evenly as possible. #[test] fn test_qemu_round_robin_partitions_queues( ids in prop::collection::vec("[a-z]{1,4}", 0..8), @@ -373,8 +373,8 @@ mod tests { } } - /// Test that managed `iotN` threads are kept in id order and other ids are - /// dropped. + /// Test that managed `iotN` threads are kept in id order and other ids + /// are dropped. #[test] fn test_qemu_topology_keeps_managed_threads_in_id_order() { let body = r#" diff --git a/src/backends/qemu/libvirt.rs b/src/backends/qemu/libvirt.rs index 99d8c29..2c641ef 100644 --- a/src/backends/qemu/libvirt.rs +++ b/src/backends/qemu/libvirt.rs @@ -443,10 +443,10 @@ fn parse_qmp_envelope(cmd: &str, body: &str) -> Result, QemuError> mod tests { use super::*; - /// Test that domain XML detection accepts virtio-scsi model - /// variants and rejects others. + /// Test that domain XML detection accepts virtio-scsi model variants and + /// rejects others. #[test] - fn xml_matches_virtio_scsi_variants() { + fn test_libvirt_xml_matches_virtio_scsi_variants() { let a = r#""#; let b = r#""#; let c = r#""#; @@ -457,18 +457,17 @@ mod tests { assert!(!xml_has_virtio_scsi(d)); } - /// Test that a QMP success envelope yields the return payload. + /// Test that parsing the qmp envelope does not modify the payload. #[test] - fn parse_envelope_extracts_return() { + fn test_libvirt_qmp_return_envelope_yields_the_payload_unchanged() { let body = r#"{"return":{"foo":42}}"#; let v = parse_qmp_envelope("query", body).unwrap(); assert_eq!(v.get(), r#"{"foo":42}"#); } - /// Test that a QMP error envelope becomes a typed error with - /// class/desc. + /// Test that a QMP error envelope becomes a typed error with class/desc. #[test] - fn parse_envelope_surfaces_qmp_error() { + fn test_libvirt_qmp_error_envelope_preserves_class_and_desc() { let body = r#"{"error":{"class":"GenericError","desc":"boom"}}"#; let err = parse_qmp_envelope("query", body).unwrap_err(); match err { @@ -480,40 +479,25 @@ mod tests { } } - /// Test that blockstats counters are summed across disks. + /// Test that device counters are added, with flush and unmap folded into + /// other I/O. #[test] - fn parse_blockstats_sums_across_devices() { + fn test_libvirt_blockstats_sums_devices() { let body = r#"[ - {"stats": { - "rd_operations": 100, - "wr_operations": 200, - "flush_operations": 3, - "unmap_operations": 1, - "rd_bytes": 4096, - "wr_bytes": 8192 - }}, - {"stats": { - "rd_operations": 50, - "wr_operations": 25, - "flush_operations": 0, - "unmap_operations": 0, - "rd_bytes": 2048, - "wr_bytes": 1024 - }} + {"stats":{"rd_bytes":10,"wr_bytes":20,"rd_operations":1,"wr_operations":2,"flush_operations":3,"unmap_operations":4}}, + {"stats":{"rd_bytes":5,"wr_bytes":6,"rd_operations":7,"wr_operations":8,"flush_operations":9,"unmap_operations":10}} ]"#; - let p = parse_blockstats(body).unwrap().unwrap(); - assert_eq!(p.read_io_count, 150); - assert_eq!(p.write_io_count, 225); - assert_eq!(p.other_io_count, 4); - assert_eq!(p.read_bytes_total, 6144); - assert_eq!(p.write_bytes_total, 9216); - assert_eq!(p.total_io_count(), 150 + 225 + 4); + let perf = parse_blockstats(body).unwrap().unwrap(); + assert_eq!(perf.read_io_count, 8); + assert_eq!(perf.write_io_count, 10); + assert_eq!(perf.other_io_count, 26); + assert_eq!(perf.read_bytes_total, 15); + assert_eq!(perf.write_bytes_total, 26); } - /// Test that empty blockstats parses as unavailable/`None`. + /// Test that parsing an empty blockstats array doesn't return a sample. #[test] - fn parse_blockstats_empty_returns_unavailable() { - let p = parse_blockstats("[]").unwrap(); - assert!(p.is_none()); + fn test_libvirt_blockstats_empty_array_is_none() { + assert!(parse_blockstats("[]").unwrap().is_none()); } } diff --git a/src/config.rs b/src/config.rs index 5843f85..d75e00e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -365,23 +365,19 @@ pub fn dump_default_config() -> String { mod tests { use super::*; - /// Test that default `Config` validates and uses the threshold - /// engine with a 10s poll. + /// Test that the default `Config` validates. #[test] - fn default_config_is_valid() { + fn test_config_default_is_valid() { let mut cfg = Config::default(); - assert_eq!(cfg.engine, "threshold"); - assert_eq!(cfg.scale_poll_secs, 10); validate_config(&mut cfg).unwrap().finish().unwrap(); } - /// Test that `validate_config` rejects an empty engine name. + /// Test that an empty engine does not validate. #[test] - fn validate_rejects_empty_engine() { + fn test_config_rejects_empty_engine() { let mut cfg = Config::default(); cfg.engine.clear(); - let err = validate_config(&mut cfg).err().unwrap().to_string(); - assert!(err.contains("engine must be non-empty")); + assert!(validate_config(&mut cfg).is_err()); } /// Test that `validate_config` rejects a zero `scale_poll_secs`. @@ -475,7 +471,7 @@ mod tests { /// Test that minimal valid JSON loads into `Config` with the /// expected engine, poll, and paths. #[test] - fn load_config_round_trips_required_fields() { + fn test_config_load_round_trips_every_field() { let dir = tempfile::tempdir().unwrap(); let path = dir.path().join("config.json"); std::fs::write( @@ -502,7 +498,7 @@ mod tests { /// Test that loading JSON with an unknown field returns /// `ConfigError::SerdeJson`. #[test] - fn load_config_rejects_unknown_fields() { + fn test_config_load_rejects_unknown_fields() { let dir = tempfile::tempdir().unwrap(); let path = dir.path().join("config.json"); std::fs::write( @@ -511,7 +507,7 @@ mod tests { "engine": "threshold", "engine_config_dir": "/engines", "backend_config_dir": "/backends", - "scale_poll_secs": 1, + "scale_poll_secs": 1.0, "not_a_real_field": true }"#, ) @@ -520,11 +516,10 @@ mod tests { let err = load_or_default::(PrefixedPath::new(path.to_str().unwrap())).unwrap_err(); assert!(matches!(err, ConfigError::SerdeJson(_))); } - /// Test that pretty-printed default JSON includes engine, poll /// interval, and engine config path. #[test] - fn dump_default_config_contains_engine() { + fn test_dump_default_config_contains_engine() { let dumped = dump_default_config(); assert!(dumped.contains(r#""engine": "threshold""#)); assert!(dumped.contains(r#""scale_poll_secs": 10"#)); @@ -534,7 +529,7 @@ mod tests { /// Test that serde defaults for min/max threads, host CPU ceiling, /// and cooldown match `Config::default()`. #[test] - fn controller_policy_defaults_round_trip() { + fn test_controller_policy_defaults_round_trip() { let cfg: Config = serde_json::from_str(r#"{"engine": "foo", "engine_config_dir": "/path/to/engines", "backend_config_dir": "/path/to/backends", "scale_poll_secs": 42, "vm_state_path": "/path/to/vm-state.json"}"#).unwrap(); assert_eq!(cfg.min_thread_count, 1); assert_eq!(cfg.max_thread_count, 8); diff --git a/src/controller.rs b/src/controller.rs index 3077cbe..fc777d7 100644 --- a/src/controller.rs +++ b/src/controller.rs @@ -1142,11 +1142,10 @@ mod tests { async fn close(&self) {} } - /// Test that actuation will not raise the pool above the VM vCPU - /// count. + /// Test that actuation will not raise the pool above the VM vCPU count. #[rstest] #[tokio::test] - async fn actuation_enforces_vcpu_cap(#[from(mockdir)] _mockdir: MockDir) { + async fn test_controller_actuation_enforces_vcpu_cap(#[from(mockdir)] _mockdir: MockDir) { let cfg = Config { vm_state_path: PrefixedPath::new("/ownership.json"), cooldown_secs: 0, @@ -1209,7 +1208,7 @@ mod tests { /// and blocks scale-up when host CPU is above the ceiling. #[rstest] #[tokio::test] - async fn actuation_enforces_controller_bounds_and_host_ceiling( + async fn test_controller_actuation_enforces_controller_bounds_and_host_ceiling( #[from(mockdir)] _mockdir: MockDir, ) { let cfg = Config { @@ -1281,7 +1280,9 @@ mod tests { #[cfg(feature = "threshold-engine")] #[rstest] #[tokio::test] - async fn revert_arms_cooldown_blocking_immediate_retry(#[from(mockdir)] _mockdir: MockDir) { + async fn test_controller_revert_arms_cooldown_blocking_immediate_retry( + #[from(mockdir)] _mockdir: MockDir, + ) { let cfg = Config { vm_state_path: PrefixedPath::new("/ownership.json"), cooldown_secs: 30, @@ -1338,7 +1339,7 @@ mod tests { #[cfg(feature = "threshold-engine")] #[rstest] #[tokio::test] - async fn revert_bypasses_existing_cooldown(#[from(mockdir)] _mockdir: MockDir) { + async fn test_controller_revert_bypasses_existing_cooldown(#[from(mockdir)] _mockdir: MockDir) { let cfg = Config { vm_state_path: PrefixedPath::new("/ownership.json"), cooldown_secs: 60, @@ -1550,8 +1551,8 @@ mod tests { )) } - /// Test that the per-VM status line has a fixed number of - /// space-separated fields whether or not samples are available. + /// Test that the per-VM status line has a fixed number of space-separated + /// fields whether or not samples are available. #[test] fn test_per_vm_status_line_has_fixed_space_separated_fields() { let mut status = InstanceStatus::default(); @@ -1570,11 +1571,13 @@ mod tests { assert!(full.contains(" 1 2 3 4 - - - - "), "{full}"); } - /// Test that discovery sync adds new VMs, retains existing ones, - /// and removes disappeared ones. + /// Test that discovery sync adds new VMs, retains existing ones, and + /// removes disappeared ones. #[rstest] #[tokio::test] - async fn sync_instances_adds_retains_and_removes(#[from(mockdir)] _mockdir: MockDir) { + async fn test_constroller_sync_instances_adds_retains_and_removes( + #[from(mockdir)] _mockdir: MockDir, + ) { let added = Arc::new(AtomicUsize::new(0)); let removed = Arc::new(AtomicUsize::new(0)); let evaluated = Arc::new(AtomicUsize::new(0)); @@ -1635,8 +1638,8 @@ mod tests { (per_vm_log, dir, guard) } - /// Return the records of one VM's per-VM log that carry an `event` - /// field, without timestamps. + /// Return the records of one VM's per-VM log that carry an `event` field, + /// without timestamps. fn per_vm_log_events(per_vm_log: &PerVmLog, vm: &str) -> Vec { std::fs::read_to_string(per_vm_log.path(vm).unwrap()) .unwrap() @@ -1647,11 +1650,11 @@ mod tests { .collect() } - /// Test that one tick refreshes state, runs the engine, and drops - /// instances that fail refresh. + /// Test that one tick refreshes state, runs the engine, and drops instances + /// that fail to refresh. #[rstest] #[tokio::test] - async fn tick_refreshes_evaluates_and_drops_failed_instances( + async fn test_controller_tick_refreshes_evaluates_and_drops_failed_instances( #[from(mockdir)] _mockdir: MockDir, ) { let added = Arc::new(AtomicUsize::new(0)); diff --git a/src/engines/mod.rs b/src/engines/mod.rs index 2535c05..8d2e3f8 100644 --- a/src/engines/mod.rs +++ b/src/engines/mod.rs @@ -299,7 +299,7 @@ mod tests { /// Test that `ScaleAction::target`/`Display` cover None/Up/Down /// variants. #[test] - fn scale_action_target_and_display() { + fn test_scale_action_target_and_display() { assert_eq!(ScaleAction::None.target(), None); assert_eq!(ScaleAction::Up(4).target(), Some(4)); assert_eq!(ScaleAction::Down(2).target(), Some(2)); @@ -314,7 +314,7 @@ mod tests { /// Test that `InstanceDecision::new` stores the VM id and /// `ScaleAction` unchanged. #[test] - fn instance_decision_new_preserves_fields() { + fn test_instance_decision_new_preserves_fields() { let decision = InstanceDecision::new("vm-1", ScaleAction::Up(5)); assert_eq!(decision.instance_id, "vm-1"); assert_eq!(decision.decision, ScaleAction::Up(5)); @@ -323,7 +323,7 @@ mod tests { /// Test that registry loads the threshold engine by name and errors /// on unknown engines. #[test] - fn load_registered_engine_finds_threshold_and_rejects_unknown() { + fn test_load_registered_engine_finds_threshold_and_rejects_unknown() { let dir = PrefixedPath::new("/tmp/missing-engines"); let mut durations = crate::config::Config::default().poll_period().converter(); let engine = load_registered_engine(&dir, "threshold", &mut durations).unwrap(); @@ -399,7 +399,7 @@ mod tests { /// Test that default fleet `evaluate` skips ineligible VMs and only /// plans eligible ones. #[tokio::test] - async fn default_fleet_evaluation_skips_ineligible_instances() { + async fn test_default_fleet_evaluation_skips_ineligible_instances() { let engine = PerInstanceEngine { calls: AtomicUsize::new(0), }; diff --git a/src/engines/threshold.rs b/src/engines/threshold.rs index a7956e9..08ede90 100644 --- a/src/engines/threshold.rs +++ b/src/engines/threshold.rs @@ -1090,7 +1090,7 @@ mod tests { /// Test that an IOPS-rate drop after scale-up triggers revert. #[rstest] #[tokio::test] - async fn scale_up_revert_fires_on_iops_rate_drop( + async fn test_scale_up_revert_fires_on_iops_rate_drop( engine: ThresholdEngine, #[future] instance: Arc, ) { @@ -1125,7 +1125,7 @@ mod tests { /// revert. #[rstest] #[tokio::test] - async fn scale_up_flat_rate_reverts( + async fn test_scale_up_flat_rate_reverts( engine: ThresholdEngine, #[future] instance: Arc, ) { @@ -1159,7 +1159,7 @@ mod tests { /// Test that a sufficient post-scale IOPS-rate gain keeps the scale-up. #[rstest] #[tokio::test] - async fn scale_up_rate_gain_keeps_scale( + async fn test_scale_up_rate_gain_keeps_scale( engine: ThresholdEngine, #[future] instance: Arc, ) { @@ -1323,7 +1323,7 @@ mod tests { /// (demand left; IOPS collapse is expected). #[rstest] #[tokio::test] - async fn scale_down_keeps_shrink_when_util_stays_low(#[future] instance: Arc) { + async fn test_scale_down_keeps_shrink_when_util_stays_low(#[future] instance: Arc) { let instance = instance.await; let engine = new_engine(ThresholdConfig { scale_up_threshold: 0.8, @@ -1361,7 +1361,7 @@ mod tests { /// remains) but IOPS fell beyond the tolerated drop. #[rstest] #[tokio::test] - async fn scale_down_reverts_when_util_stays_high_and_iops_drop( + async fn test_scale_down_reverts_when_util_stays_high_and_iops_drop( #[future] instance: Arc, ) { let instance = instance.await; @@ -1576,10 +1576,10 @@ mod tests { ); } - /// Test that percent fields serde as human percents on the wire and - /// fractions in memory. + /// Test that percent fields serde as human percents in JSON and fractions + /// in memory. #[test] - fn percent_wire_format_round_trips() { + fn test_percent_wire_format_round_trips() { let config: ThresholdConfig = serde_json::from_str(r#"{"scale_up_min_gain_percent":10}"#).unwrap(); assert!((config.scale_up_min_gain - 0.10).abs() < f64::EPSILON); @@ -1608,7 +1608,7 @@ mod tests { /// scale-ups are suppressed. #[rstest] #[test(tokio::test)] - async fn no_scale_up_during_pending_validation( + async fn test_no_scale_up_during_pending_validation( engine: ThresholdEngine, #[future] instance: Arc, ) { @@ -1666,7 +1666,7 @@ mod tests { /// performance doesn't increase much. #[rstest] #[test(tokio::test)] - async fn pending_validation_reverts_regressive_scale( + async fn test_pending_validation_reverts_regressive_scale( _engine: ThresholdEngine, #[future] instance: Arc, ) { @@ -1718,7 +1718,7 @@ mod tests { /// Test that scale-up decision logs current/min IOPS and validation delay. #[rstest] #[tokio::test] - async fn scale_up_log_includes_iops_targets( + async fn test_scale_up_log_includes_iops_targets( engine: ThresholdEngine, #[future] instance: Arc, ) { @@ -1754,7 +1754,7 @@ mod tests { /// Test that a missing `threshold.json` still builds an engine with /// defaults. #[test] - fn missing_config_file_uses_defaults() { + fn test_missing_config_file_uses_defaults() { let dir = tempfile::tempdir().unwrap(); let mut durations = PollPeriod::new(POLL_SECS, false).converter(); let engine = ThresholdEngine::from_config_dir( @@ -1811,7 +1811,7 @@ mod tests { /// Hold/`None`. #[rstest] #[tokio::test] - async fn evaluate_holds_when_util_below_thresholds(#[future] instance: Arc) { + async fn test_evaluate_holds_when_util_below_thresholds(#[future] instance: Arc) { let instance = instance.await; let engine = new_engine(ThresholdConfig::default()); let action = engine diff --git a/src/instance.rs b/src/instance.rs index e8befc5..391c3b6 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -807,10 +807,9 @@ mod tests { } } - /// Test that a successful client snapshot updates - /// alive/thread_count state. + /// Test that a successful client snapshot updates alive/thread_count state. #[tokio::test] - async fn refresh_state_applies_successful_snapshot() { + async fn test_instance_refresh_state_applies_successful_snapshot() { let instance = Instance::new( "vm-ok".to_string(), PrefixedPath::new(""), @@ -823,10 +822,9 @@ mod tests { assert_eq!(status.thread_count, 3); } - /// Test that client errors mark the instance dead and close the - /// client. + /// Test that client errors mark the instance dead and close the client. #[tokio::test] - async fn refresh_state_marks_failed_instances_dead_and_closes() { + async fn test_instance_refresh_marks_failed_instances_dead_and_closes() { let closed = Arc::new(AtomicBool::new(false)); let instance = Instance::new( "vm-bad".to_string(), @@ -844,7 +842,7 @@ mod tests { /// Test that `Display` for an instance prints the bare VM id. #[test] - fn display_trims_instance_id() { + fn test_display_trims_instance_id() { let instance = Instance::new( " vm-1 ".to_string(), PrefixedPath::new(""), @@ -854,10 +852,6 @@ mod tests { assert_eq!(instance.to_string(), "vm-1"); } - use super::{TaskCpuSample, compute_per_worker_util}; - - use super::ThreadNameFilter; - /// Test that exact `IgnoreThreadNames` wins over a matching regex /// allowlist. #[test] @@ -876,28 +870,39 @@ mod tests { } } - /// Test that two workers sharing a name still get independent - /// CPU-delta util samples. + /// Test that utilization is calculated as change in CPU ticks divided by + /// elapsed ticks. #[test] - fn duplicate_worker_names_keep_independent_deltas() { - let previous = vec![task(10, "worker", 100), task(11, "worker", 200)]; - let current = vec![task(11, "worker", 400), task(10, "worker", 200)]; - - let util = compute_per_worker_util(&previous, ¤t, 500.0); - assert_eq!(util.len(), 2); - assert!((util[0].1 - 0.4).abs() < f64::EPSILON); - assert!((util[1].1 - 0.2).abs() < f64::EPSILON); + fn test_util_is_tick_delta_divided_by_wall_ticks() { + let previous = vec![task(10, "a", 100), task(11, "b", 0)]; + let current = vec![task(11, "b", 25), task(10, "a", 150)]; + assert_eq!( + compute_per_worker_util(&previous, ¤t, 100.0), + vec![ + ("b".to_string(), (25.0 - 0.0) / 100.0), + ("a".to_string(), (150.0 - 100.0) / 100.0), + ] + ); } - /// Test that a newly appeared worker does not spike util from - /// lifetime counters. + /// Test that a renamed task, an unknown tid, and a backwards counter are + /// all dropped. #[test] - fn new_worker_starts_without_a_lifetime_spike() { - let previous = vec![task(10, "worker0", 100)]; - let current = vec![task(10, "worker0", 200), task(11, "worker1", 900_000)]; - - let util = compute_per_worker_util(&previous, ¤t, 500.0); - assert_eq!(util.len(), 1); - assert!((util[0].1 - 0.2).abs() < f64::EPSILON); + fn test_util_drops_unmatched_tasks() { + let previous = vec![ + task(1, "old", 100), + task(2, "kept", 50), + task(3, "back", 200), + ]; + let current = vec![ + task(1, "new", 150), + task(9, "unknown", 10), + task(3, "back", 100), + task(2, "kept", 75), + ]; + assert_eq!( + compute_per_worker_util(&previous, ¤t, 100.0), + vec![("kept".to_string(), 0.25)] + ); } } diff --git a/src/path.rs b/src/path.rs index ac8a955..f5f2dae 100644 --- a/src/path.rs +++ b/src/path.rs @@ -151,22 +151,25 @@ mod tests { /// Test that relative `Path` values are stored unchanged. #[test] - fn relative_paths_are_unchanged() { + fn test_relative_paths_are_unchanged() { let path = PrefixedPath::new("relative/sock"); assert_eq!(path.as_os_str(), OsStr::new("relative/sock")); } - /// Test that absolute `Path` values stay absolute under the default - /// root. + /// Test that paths are prefixed with the contents of the env var. #[test] - fn absolute_paths_keep_absolute_form_under_default_root() { + fn test_absolute_paths_keep_absolute_form_under_default_root() { + let env = env::var("IO_THREAD_CONTROLLER_ROOT_PATH").unwrap_or(String::from("")); let path = PrefixedPath::new("/var/run/example.sock"); - assert_eq!(path.as_os_str(), OsStr::new("/var/run/example.sock")); + assert_eq!( + path.as_os_str(), + OsStr::new(&format!("{env}/var/run/example.sock")) + ); } /// Test that `FromStr` builds a `Path` from a string. #[test] - fn from_str_builds_path() { + fn test_from_str_builds_path() { let path: PrefixedPath = "tmp/example".parse().unwrap(); assert_eq!(path.as_os_str(), OsStr::new("tmp/example")); } diff --git a/src/rolling.rs b/src/rolling.rs index e13e3fb..d57f96c 100644 --- a/src/rolling.rs +++ b/src/rolling.rs @@ -240,7 +240,7 @@ mod tests { /// Test that the first sample records a baseline and yields no /// rates yet. #[test] - fn first_sample_only_establishes_baseline() { + fn test_rolling_first_sample_only_establishes_baseline() { let mut metrics = RollingMetrics::new(); let t0 = Instant::now(); metrics.push_from_procfs_delta(t0, 10, 20); @@ -249,8 +249,10 @@ mod tests { /// Test that rolling IOPS/CPU-per-IO rates use real elapsed time /// between samples. + // TODO determine if it's ok to have iops_over with a longer Duration + // than wall_ns. #[test] - fn rates_use_real_elapsed_time() { + fn test_rolling_rates_use_real_elapsed_time() { let mut metrics = RollingMetrics::new(); let t0 = Instant::now(); metrics.push_from_procfs_delta(t0, 10, 20); @@ -265,7 +267,7 @@ mod tests { /// Test that a counter reset drops prior rates and starts a new /// baseline. #[test] - fn counter_reset_replaces_baseline() { + fn test_rolling_counter_reset_replaces_baseline() { let mut metrics = RollingMetrics::new(); let t0 = Instant::now(); metrics.push_from_procfs_delta(t0, 100, 100); diff --git a/src/state.rs b/src/state.rs index f1a187d..b6b0916 100644 --- a/src/state.rs +++ b/src/state.rs @@ -129,14 +129,16 @@ impl VmStateStore { #[cfg(test)] mod tests { + use rstest::rstest; + use super::*; + use crate::test::{MockDir, mockdir::mockdir}; - /// Test that managed/unmanaged sets save and reload from a single - /// JSON file. - #[test] - fn ownership_round_trips_in_one_file() { - let dir = tempfile::tempdir().unwrap(); - let store = VmStateStore::new(PrefixedPath::new(&dir.path().join("ownership.json"))); + /// Test that managed/unmanaged sets save and reload from a single JSON + /// file. + #[rstest] + fn test_state_disjoint_ownership_round_trips_in_one_file(mockdir: MockDir) { + let store = VmStateStore::new(PrefixedPath::new("/ownership.json")); let mut expected = VmOwnership::default(); expected.record("managed", true).unwrap(); expected.record("../unmanaged", false).unwrap(); @@ -144,32 +146,29 @@ mod tests { store.save(&expected).unwrap(); assert_eq!(store.load().unwrap(), expected); - assert_eq!(std::fs::read_dir(dir.path()).unwrap().count(), 1); + assert_eq!(std::fs::read_dir(mockdir.root()).unwrap().count(), 1); } /// Test that a missing ownership file loads as empty/default state. - #[test] - fn missing_state_uses_defaults() { - let dir = tempfile::tempdir().unwrap(); + #[rstest] + fn test_state_uses_defaults(#[from(mockdir)] _md: MockDir) { assert_eq!( - VmStateStore::new(PrefixedPath::new(&dir.path().join("missing.json"))) + VmStateStore::new(PrefixedPath::new("/missing.json")) .load() .unwrap(), VmOwnership::default() ); } - /// Test that a VM listed as both managed and unmanaged is rejected - /// on load. - #[test] - fn overlapping_classifications_are_rejected() { - let dir = tempfile::tempdir().unwrap(); - let path = dir.path().join("ownership.json"); + /// Test that a VM listed as both managed and unmanaged is rejected on load. + #[rstest] + fn test_state_overlapping_ownership_is_rejected(#[from(mockdir)] _md: MockDir) { + let path = PrefixedPath::new("/ownership.json"); std::fs::write( &path, r#"{"managed_vms":["same"],"unmanaged_vms":["same"]}"#, ) .unwrap(); - assert!(VmStateStore::new(PrefixedPath::new(&path)).load().is_err()); + assert!(VmStateStore::new(&path).load().is_err()); } } diff --git a/src/test/mockdir.rs b/src/test/mockdir.rs index 9ae7f5d..286ee3a 100644 --- a/src/test/mockdir.rs +++ b/src/test/mockdir.rs @@ -52,6 +52,10 @@ impl MockDir { _dir: dir, } } + + pub(crate) fn root(&self) -> &std::path::Path { + &self.path + } } /// Create a new `MockDir` as an `rstest` fixture