#![cfg(test)]
use super::*;
#[test]
fn default_threadstate_state_is_sentinel_tilde() {
let t = ThreadState::default();
assert_eq!(
t.state, '~',
"ThreadState::default().state must be '~' (the \
absent-value sentinel chosen to lex-sort AFTER \
every real kernel state letter), not '\\0' (the \
bare char Default); see field doc on \
ThreadState::state"
);
}
#[test]
fn mode_tiebreak_against_default_state_picks_real_letter() {
use crate::ctprof_compare::{AggRule, Aggregated, aggregate};
let default_thread = ThreadState::default();
let real_thread = ThreadState {
state: 'R',
..ThreadState::default()
};
let agg = aggregate(
AggRule::ModeChar(|t| t.state),
&[&default_thread, &real_thread],
);
match &agg {
Aggregated::Mode { .. } => assert_eq!(
agg.mode_value(),
"R",
"Mode tiebreak between '~' (default sentinel) \
and 'R' (real kernel state) must elect 'R'; \
got {:?}",
agg.mode_value(),
),
other => panic!("expected Mode, got {other:?}"),
}
}
#[test]
fn wire_format_identity_raw_primitives_deserialize_into_wrapped_thread_state() {
let json = r#"{
"tid": 1234,
"tgid": 1234,
"pcomm": "demo",
"comm": "demo-w",
"cgroup": "/app",
"start_time_clock_ticks": 555000,
"policy": "SCHED_OTHER",
"nice": -5,
"cpu_affinity": [0, 1, 2, 3],
"processor": 7,
"state": "R",
"ext_enabled": false,
"run_time_ns": 1000000,
"wait_time_ns": 0,
"timeslices": 50,
"voluntary_csw": 100,
"nonvoluntary_csw": 25,
"nr_wakeups": 200,
"nr_wakeups_local": 80,
"nr_wakeups_remote": 30,
"nr_wakeups_sync": 10,
"nr_wakeups_migrate": 5,
"nr_wakeups_affine": 60,
"nr_wakeups_affine_attempts": 100,
"nr_migrations": 8,
"nr_forced_migrations": 1,
"nr_failed_migrations_affine": 0,
"nr_failed_migrations_running": 0,
"nr_failed_migrations_hot": 0,
"wait_sum": 5000000,
"wait_count": 15,
"wait_max": 250000,
"voluntary_sleep_ns": 3200000,
"sleep_max": 180000,
"block_sum": 1100000,
"block_max": 60000,
"iowait_sum": 77000,
"iowait_count": 18,
"exec_max": 90000,
"slice_max": 400000,
"allocated_bytes": 16777216,
"deallocated_bytes": 8388608,
"minflt": 7777,
"majflt": 8888,
"utime_clock_ticks": 10,
"stime_clock_ticks": 11,
"priority": 25,
"rt_priority": 99,
"core_forceidle_sum": 0,
"fair_slice_ns": 250000,
"nr_threads": 4,
"smaps_rollup_kb": {},
"rchar": 100,
"wchar": 200,
"syscr": 10,
"syscw": 20,
"read_bytes": 4096,
"write_bytes": 8192,
"cancelled_write_bytes": 1024,
"cpu_delay_count": 0,
"cpu_delay_total_ns": 0,
"cpu_delay_max_ns": 0,
"cpu_delay_min_ns": 0,
"blkio_delay_count": 0,
"blkio_delay_total_ns": 0,
"blkio_delay_max_ns": 0,
"blkio_delay_min_ns": 0,
"swapin_delay_count": 0,
"swapin_delay_total_ns": 0,
"swapin_delay_max_ns": 0,
"swapin_delay_min_ns": 0,
"freepages_delay_count": 0,
"freepages_delay_total_ns": 0,
"freepages_delay_max_ns": 0,
"freepages_delay_min_ns": 0,
"thrashing_delay_count": 0,
"thrashing_delay_total_ns": 0,
"thrashing_delay_max_ns": 0,
"thrashing_delay_min_ns": 0,
"compact_delay_count": 0,
"compact_delay_total_ns": 0,
"compact_delay_max_ns": 0,
"compact_delay_min_ns": 0,
"wpcopy_delay_count": 0,
"wpcopy_delay_total_ns": 0,
"wpcopy_delay_max_ns": 0,
"wpcopy_delay_min_ns": 0,
"irq_delay_count": 0,
"irq_delay_total_ns": 0,
"irq_delay_max_ns": 0,
"irq_delay_min_ns": 0,
"hiwater_rss_bytes": 0,
"hiwater_vm_bytes": 0
}"#;
let t: ThreadState = serde_json::from_str(json).expect("deserialize");
assert_eq!(t.run_time_ns, crate::metric_types::MonotonicNs(1_000_000));
assert_eq!(t.timeslices, crate::metric_types::MonotonicCount(50));
assert_eq!(t.utime_clock_ticks, crate::metric_types::ClockTicks(10));
assert_eq!(t.allocated_bytes, crate::metric_types::Bytes(16_777_216));
assert_eq!(
t.cancelled_write_bytes,
crate::metric_types::Bytes(1024),
"cancelled_write_bytes round-trips through the JSON \
wire format alongside the other Bytes-typed fields",
);
assert_eq!(t.wait_max, crate::metric_types::PeakNs(250_000));
assert_eq!(t.fair_slice_ns, crate::metric_types::GaugeNs(250_000));
assert_eq!(t.nr_threads, crate::metric_types::GaugeCount(4));
assert_eq!(t.nice, crate::metric_types::OrdinalI32(-5));
assert_eq!(t.rt_priority, crate::metric_types::OrdinalU32(99));
assert_eq!(
t.policy,
crate::metric_types::CategoricalString::from("SCHED_OTHER")
);
assert_eq!(
t.cpu_affinity,
crate::metric_types::CpuSet(vec![0, 1, 2, 3])
);
}
#[test]
fn nr_threads_field_pinned_to_gauge_count() {
let t = ThreadState::default();
let _: crate::metric_types::GaugeCount = t.nr_threads;
}
#[test]
fn cancelled_write_bytes_field_pinned_to_bytes() {
let t = ThreadState::default();
let _: crate::metric_types::Bytes = t.cancelled_write_bytes;
}
#[test]
fn psi_half_avg10_percent_converts_centi_percent_to_percent() {
let zero = PsiHalf {
avg10: 0,
avg60: 0,
avg300: 0,
total_usec: 0,
};
assert_eq!(zero.avg10_percent(), 0.0);
let one = PsiHalf {
avg10: 1,
..PsiHalf::default()
};
assert!(
(one.avg10_percent() - 0.01).abs() < f64::EPSILON,
"avg10=1 must convert to 0.01 %, got {}",
one.avg10_percent(),
);
let one_pct = PsiHalf {
avg10: 100,
..PsiHalf::default()
};
assert!(
(one_pct.avg10_percent() - 1.0).abs() < f64::EPSILON,
"avg10=100 must convert to 1.0 %, got {}",
one_pct.avg10_percent(),
);
let mid = PsiHalf {
avg10: 5050,
..PsiHalf::default()
};
assert!(
(mid.avg10_percent() - 50.50).abs() < 1e-9,
"avg10=5050 must convert to 50.50 %, got {}",
mid.avg10_percent(),
);
let max = PsiHalf {
avg10: 10000,
..PsiHalf::default()
};
assert!(
(max.avg10_percent() - 100.0).abs() < f64::EPSILON,
"avg10=10000 must convert to 100.0 %, got {}",
max.avg10_percent(),
);
let over = PsiHalf {
avg10: 10099,
..PsiHalf::default()
};
assert!(
(over.avg10_percent() - 100.99).abs() < 1e-9,
"avg10=10099 must convert to 100.99 %, got {}",
over.avg10_percent(),
);
let umax = PsiHalf {
avg10: u16::MAX,
..PsiHalf::default()
};
assert!(
(umax.avg10_percent() - (u16::MAX as f64 / 100.0)).abs() < 1e-6,
"avg10=u16::MAX must convert to {} %, got {}",
u16::MAX as f64 / 100.0,
umax.avg10_percent(),
);
}
#[test]
fn psi_half_avg60_percent_uses_avg60_field() {
let p = PsiHalf {
avg10: 1234,
avg60: 5678,
avg300: 9012,
total_usec: 0,
};
assert!(
(p.avg60_percent() - 56.78).abs() < 1e-9,
"avg60=5678 must convert to 56.78 % (NOT avg10's 12.34); got {}",
p.avg60_percent(),
);
let zero = PsiHalf::default();
assert_eq!(zero.avg60_percent(), 0.0);
let max = PsiHalf {
avg60: 10000,
..PsiHalf::default()
};
assert!(
(max.avg60_percent() - 100.0).abs() < f64::EPSILON,
"avg60=10000 must convert to 100.0 %, got {}",
max.avg60_percent(),
);
}
#[test]
fn psi_half_avg300_percent_uses_avg300_field() {
let p = PsiHalf {
avg10: 1234,
avg60: 5678,
avg300: 9012,
total_usec: 0,
};
assert!(
(p.avg300_percent() - 90.12).abs() < 1e-9,
"avg300=9012 must convert to 90.12 % (NOT avg10/avg60's values); got {}",
p.avg300_percent(),
);
let zero = PsiHalf::default();
assert_eq!(zero.avg300_percent(), 0.0);
let max = PsiHalf {
avg300: 10000,
..PsiHalf::default()
};
assert!(
(max.avg300_percent() - 100.0).abs() < f64::EPSILON,
"avg300=10000 must convert to 100.0 %, got {}",
max.avg300_percent(),
);
}
#[test]
fn psi_half_percent_methods_preserve_kernel_two_decimal_precision() {
let cases: &[(u16, u16, u16, f64)] = &[
(3, 25, 325, 3.25),
(42, 7, 4207, 42.07),
(87, 50, 8750, 87.50),
(99, 99, 9999, 99.99),
];
for (int_part, frac_part, stored, expected) in cases {
assert_eq!(
(*int_part as u32) * 100 + (*frac_part as u32),
*stored as u32,
"fixture: int*100+frac must equal stored",
);
let p = PsiHalf {
avg10: *stored,
avg60: *stored,
avg300: *stored,
total_usec: 0,
};
assert!(
(p.avg10_percent() - *expected).abs() < 1e-9,
"stored={stored} → expected {expected}, got avg10={}",
p.avg10_percent(),
);
assert!(
(p.avg60_percent() - *expected).abs() < 1e-9,
"stored={stored} → expected {expected}, got avg60={}",
p.avg60_percent(),
);
assert!(
(p.avg300_percent() - *expected).abs() < 1e-9,
"stored={stored} → expected {expected}, got avg300={}",
p.avg300_percent(),
);
}
}