Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 17 additions & 32 deletions src/backends/qemu/libvirt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -446,7 +446,7 @@ mod tests {
/// 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#"<controller type='scsi' index='0' model='virtio-scsi'>"#;
let b = r#"<controller type="scsi" index="0" model="virtio-scsi"/>"#;
let c = r#"<controller type='scsi' model='lsi'/>"#;
Expand All @@ -459,7 +459,7 @@ mod tests {

/// Test that a QMP success envelope yields the return 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}"#);
Expand All @@ -468,7 +468,7 @@ mod tests {
/// 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 {
Expand All @@ -480,40 +480,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 an empty blockstats array yields no 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());
}
}
53 changes: 18 additions & 35 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -255,45 +255,29 @@ 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 cfg = Config::default();
assert_eq!(cfg.engine, "threshold");
assert!((cfg.scale_poll_secs - 10.0).abs() < f64::EPSILON);
validate_config(&cfg).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(&cfg).unwrap_err().to_string();
assert!(err.contains("engine must be non-empty"));
}

/// Test that `validate_config` rejects non-positive
/// `scale_poll_secs`.
#[test]
fn validate_rejects_non_positive_poll() {
let mut cfg = Config {
scale_poll_secs: 0.0,
..Default::default()
};
let err = validate_config(&cfg).unwrap_err().to_string();
assert!(err.contains("scale_poll_secs must be > 0"));

cfg.scale_poll_secs = f64::NAN;
let err = validate_config(&cfg).unwrap_err().to_string();
assert!(err.contains("scale_poll_secs must be > 0"));
assert!(
validate_config(&cfg)
.is_err_and(|err| err.to_string().contains("engine must be non-empty"))
);
}

/// 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(
Expand All @@ -320,29 +304,28 @@ 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(
&path,
r#"{
"engine": "threshold",
"engine_config_dir": "/engines",
"backend_config_dir": "/backends",
"scale_poll_secs": 1.0,
"not_a_real_field": true
}"#,
"engine": "threshold",
"engine_config_dir": "/engines",
"backend_config_dir": "/backends",
"scale_poll_secs": 1.0,
"not_a_real_field": true
}"#,
)
.unwrap();

let err = load_config_or_default::<Config>(Path::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.0"#));
Expand All @@ -352,7 +335,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);
Expand Down
26 changes: 15 additions & 11 deletions src/controller.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1191,7 +1191,7 @@ mod tests {
};

use async_trait::async_trait;
use test_log::test;
use rstest::rstest;

use super::*;
use crate::{
Expand Down Expand Up @@ -1233,7 +1233,7 @@ mod tests {
/// 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: Path::new("/ownership.json"),
cooldown_secs: 0.0,
Expand Down Expand Up @@ -1301,7 +1301,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 {
Expand Down Expand Up @@ -1378,7 +1378,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: Path::new("/ownership.json"),
cooldown_secs: 30.0,
Expand Down Expand Up @@ -1440,7 +1442,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: Path::new("/ownership.json"),
cooldown_secs: 60.0,
Expand Down Expand Up @@ -1648,11 +1650,13 @@ mod tests {
))
}

/// 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));
Expand Down Expand Up @@ -1693,11 +1697,11 @@ mod tests {
assert_eq!(evaluated.load(Ordering::Relaxed), 0);
}

/// 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 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));
Expand Down
8 changes: 4 additions & 4 deletions src/engines/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,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));
Expand All @@ -304,7 +304,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));
Expand All @@ -313,7 +313,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 = Path::new("/tmp/missing-engines");
let engine = load_registered_engine(&dir, "threshold").unwrap();
assert_eq!(engine.name(), "threshold");
Expand Down Expand Up @@ -388,7 +388,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),
};
Expand Down
22 changes: 11 additions & 11 deletions src/engines/threshold.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1000,7 +1000,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<Instance>,
) {
Expand Down Expand Up @@ -1035,7 +1035,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<Instance>,
) {
Expand Down Expand Up @@ -1069,7 +1069,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<Instance>,
) {
Expand Down Expand Up @@ -1233,7 +1233,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<Instance>) {
async fn test_scale_down_keeps_shrink_when_util_stays_low(#[future] instance: Arc<Instance>) {
let instance = instance.await;
let engine = ThresholdEngine::new(ThresholdConfig {
scale_up_threshold: 0.8,
Expand Down Expand Up @@ -1271,7 +1271,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<Instance>,
) {
let instance = instance.await;
Expand Down Expand Up @@ -1489,7 +1489,7 @@ mod tests {
/// Test that percent fields serde as human percents on the wire 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);
Expand Down Expand Up @@ -1518,7 +1518,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<Instance>,
) {
Expand Down Expand Up @@ -1575,7 +1575,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<Instance>,
) {
Expand Down Expand Up @@ -1627,7 +1627,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<Instance>,
) {
Expand Down Expand Up @@ -1663,7 +1663,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 engine =
ThresholdEngine::from_config_dir(&Path::new(dir.path().to_str().unwrap())).unwrap();
Expand All @@ -1675,7 +1675,7 @@ mod tests {
/// Hold/`None`.
#[rstest]
#[tokio::test]
async fn evaluate_holds_when_util_below_thresholds(#[future] instance: Arc<Instance>) {
async fn test_evaluate_holds_when_util_below_thresholds(#[future] instance: Arc<Instance>) {
let instance = instance.await;
let engine = ThresholdEngine::new(ThresholdConfig::default());
let action = engine
Expand Down
Loading
Loading