use crate::test_support::{Metric, MetricSource, MetricStream, OutputFormat, Polarity};
pub fn extract_metrics(
output: &str,
format: &OutputFormat,
stream: MetricStream,
) -> Result<Vec<Metric>, String> {
match format {
OutputFormat::ExitCode => Ok(Vec::new()),
OutputFormat::Json => Ok(find_and_parse_json(output)
.map(|v| walk_json_leaves(&v, MetricSource::Json, stream))
.unwrap_or_default()),
OutputFormat::LlmExtract(_) => Ok(Vec::new()),
}
}
pub(crate) fn find_and_parse_json(output: &str) -> Option<serde_json::Value> {
if let Ok(v) = serde_json::from_str::<serde_json::Value>(output.trim()) {
return Some(v);
}
let region = extract_json_region(output)?;
serde_json::from_str::<serde_json::Value>(region).ok()
}
fn extract_json_region(s: &str) -> Option<&str> {
let bytes = s.as_bytes();
let start = bytes.iter().position(|&c| c == b'{' || c == b'[')?;
let opener = bytes[start];
let closer = if opener == b'{' { b'}' } else { b']' };
let mut depth = 0i32;
let mut in_string = false;
let mut escape = false;
for (i, &c) in bytes.iter().enumerate().skip(start) {
if escape {
escape = false;
continue;
}
if in_string {
match c {
b'\\' => escape = true,
b'"' => in_string = false,
_ => {}
}
continue;
}
match c {
b'"' => in_string = true,
x if x == opener => depth += 1,
x if x == closer => {
depth -= 1;
if depth == 0 {
return Some(&s[start..=i]);
}
}
_ => {}
}
}
None
}
pub fn walk_json_leaves(
value: &serde_json::Value,
source: MetricSource,
stream: MetricStream,
) -> Vec<Metric> {
let mut out = Vec::new();
let mut path = String::new();
walk(value, &mut path, 0, source, stream, &mut out);
out
}
pub const MAX_WALK_DEPTH: usize = 64;
pub const WALK_TRUNCATION_SENTINEL_NAME: &str = "__walk_json_leaves_truncated";
pub fn is_truncation_sentinel_name(name: &str) -> bool {
name == WALK_TRUNCATION_SENTINEL_NAME
}
fn walk(
value: &serde_json::Value,
path: &mut String,
depth: usize,
source: MetricSource,
stream: MetricStream,
out: &mut Vec<Metric>,
) {
if depth > MAX_WALK_DEPTH {
tracing::warn!(
depth,
max = MAX_WALK_DEPTH,
path = %path,
"walk_json_leaves: depth cap hit, subtree skipped",
);
out.push(Metric {
name: WALK_TRUNCATION_SENTINEL_NAME.to_string(),
value: depth as f64,
polarity: Polarity::Unknown,
unit: String::new(),
source,
stream,
});
return;
}
match value {
serde_json::Value::Object(map) => {
for (k, v) in map {
let saved_len = path.len();
if !path.is_empty() {
path.push('.');
}
path.push_str(k);
walk(v, path, depth + 1, source, stream, out);
path.truncate(saved_len);
}
}
serde_json::Value::Array(items) => {
for (i, v) in items.iter().enumerate() {
let saved_len = path.len();
if !path.is_empty() {
path.push('.');
}
use std::fmt::Write;
let _ = write!(path, "{i}");
walk(v, path, depth + 1, source, stream, out);
path.truncate(saved_len);
}
}
serde_json::Value::Number(n) => {
if let Some(f) = n.as_f64()
&& f.is_finite()
{
out.push(Metric {
name: path.clone(),
value: f,
polarity: Polarity::Unknown,
unit: String::new(),
source,
stream,
});
}
}
_ => {}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn is_truncation_sentinel_name_matches_the_constant() {
assert!(is_truncation_sentinel_name(WALK_TRUNCATION_SENTINEL_NAME));
}
#[test]
fn is_truncation_sentinel_name_rejects_other_names() {
assert!(!is_truncation_sentinel_name("foo"));
assert!(!is_truncation_sentinel_name(""));
assert!(!is_truncation_sentinel_name("__walk_json_leaves"));
}
#[test]
fn exit_code_returns_empty() {
let m = extract_metrics("whatever", &OutputFormat::ExitCode, MetricStream::Stdout).unwrap();
assert!(m.is_empty());
}
#[test]
fn json_full_document_extracts_numeric_leaves() {
let s = r#"{"iops": 10000, "lat_ns": 500}"#;
let m = extract_metrics(s, &OutputFormat::Json, MetricStream::Stdout).unwrap();
assert_eq!(m.len(), 2);
let names: Vec<_> = m.iter().map(|x| x.name.as_str()).collect();
assert!(names.contains(&"iops"));
assert!(names.contains(&"lat_ns"));
for metric in &m {
assert_eq!(metric.source, MetricSource::Json);
assert_eq!(metric.polarity, Polarity::Unknown);
assert_eq!(metric.unit, "");
}
}
#[test]
fn json_with_banner_prefix_extracts_region() {
let s = "fio-3.36 starting up\n{\"iops\": 500}";
let m = extract_metrics(s, &OutputFormat::Json, MetricStream::Stdout).unwrap();
assert_eq!(m.len(), 1);
assert_eq!(m[0].name, "iops");
assert_eq!(m[0].value, 500.0);
}
#[test]
fn json_nested_objects_use_dotted_paths() {
let s = r#"{"jobs": {"0": {"read": {"iops": 123}}}}"#;
let m = extract_metrics(s, &OutputFormat::Json, MetricStream::Stdout).unwrap();
assert_eq!(m.len(), 1);
assert_eq!(m[0].name, "jobs.0.read.iops");
assert_eq!(m[0].value, 123.0);
}
#[test]
fn json_arrays_use_numeric_index_paths() {
let s = r#"{"samples": [100, 200, 300]}"#;
let m = extract_metrics(s, &OutputFormat::Json, MetricStream::Stdout).unwrap();
assert_eq!(m.len(), 3);
let mut actual: Vec<(&str, f64)> = m.iter().map(|x| (x.name.as_str(), x.value)).collect();
actual.sort_by_key(|(n, _)| n.to_string());
assert_eq!(
actual,
vec![
("samples.0", 100.0),
("samples.1", 200.0),
("samples.2", 300.0),
]
);
}
#[test]
fn json_malformed_returns_empty() {
let m = extract_metrics(
"garbage not json",
&OutputFormat::Json,
MetricStream::Stdout,
)
.unwrap();
assert!(m.is_empty());
}
#[test]
fn json_empty_stdout_returns_empty() {
let m = extract_metrics("", &OutputFormat::Json, MetricStream::Stdout).unwrap();
assert!(m.is_empty());
}
#[test]
fn json_skips_string_and_bool_leaves() {
let s = r#"{"name": "fio", "ok": true, "iops": 42}"#;
let m = extract_metrics(s, &OutputFormat::Json, MetricStream::Stdout).unwrap();
assert_eq!(m.len(), 1);
assert_eq!(m[0].name, "iops");
}
#[test]
fn json_top_level_array_extracts_entries() {
let s = "[1, 2, 3]";
let m = extract_metrics(s, &OutputFormat::Json, MetricStream::Stdout).unwrap();
assert_eq!(m.len(), 3);
}
#[test]
fn llm_extract_returns_empty_unconditionally() {
for (label, format) in [
("no-hint", OutputFormat::LlmExtract(None)),
(
"with-hint",
OutputFormat::LlmExtract(Some("focus on latency")),
),
] {
for input in [
"",
"anything",
r#"{"latency_us": 42}"#,
"schbench-shaped percentile table\n50.0%: 100ns\n",
] {
let m = extract_metrics(input, &format, MetricStream::Stdout)
.unwrap_or_else(|e| panic!("{label}: extract_metrics returned Err: {e}"));
assert!(
m.is_empty(),
"{label}: LlmExtract arm must short-circuit to empty Vec; \
got {} metric(s) for input {input:?}",
m.len(),
);
}
}
}
#[test]
fn extract_json_region_finds_braced_region() {
let r = extract_json_region("prefix {\"a\": 1} suffix").unwrap();
assert_eq!(r, "{\"a\": 1}");
}
#[test]
fn extract_json_region_handles_nested_braces() {
let r = extract_json_region("log: {\"a\": {\"b\": 1}} done").unwrap();
assert_eq!(r, "{\"a\": {\"b\": 1}}");
}
#[test]
fn extract_json_region_skips_braces_in_strings() {
let r = extract_json_region(r#"{"text": "not a }"}"#).unwrap();
assert_eq!(r, r#"{"text": "not a }"}"#);
}
#[test]
fn extract_json_region_handles_escaped_quotes() {
let r = extract_json_region(r#"{"text": "has \"escaped\" quotes"}"#).unwrap();
assert_eq!(r, r#"{"text": "has \"escaped\" quotes"}"#);
}
#[test]
fn extract_json_region_returns_none_for_no_brace() {
assert!(extract_json_region("no braces here").is_none());
}
#[test]
fn extract_json_region_returns_none_for_unbalanced() {
assert!(extract_json_region("incomplete {").is_none());
}
#[test]
fn extract_json_region_dmesg_timestamp_prefix_wins_over_stdout_json() {
let input = "[ 0.001234] kernel boot banner\n{\"iops\": 100}";
let first_region = extract_json_region(input)
.expect("dmesg-style prefix starts with `[` â finder must return SOME region");
assert_eq!(
first_region, "[ 0.001234]",
"left-to-right scan picks the FIRST balanced region; \
the dmesg prefix is self-balancing and wins over the \
stdout JSON that follows",
);
let m = extract_metrics(input, &OutputFormat::Json, MetricStream::Stdout).unwrap();
let names: Vec<&str> = m.iter().map(|x| x.name.as_str()).collect();
assert!(
!names.contains(&"iops"),
"regression check: the finder picking up the dmesg \
timestamp array means the real stdout `iops` metric \
is NOT extracted; if this ever starts containing \
`iops`, the finder must have gained smarter \
noise-skipping (update this test and the \
documented contract); got: {names:?}",
);
}
#[test]
fn extract_json_region_unbalanced_stderr_prefix_returns_none() {
let input = "[FAIL stress-ng: worker timed out\n{\"iops\": 200}";
let r = extract_json_region(input);
assert!(
r.is_none(),
"unbalanced leading `[` makes the finder return \
None even though valid stdout JSON follows â the \
finder commits to the first opener and does not \
retry past its failure point. Known limitation; \
callers pre-strip stderr noise. Got: {r:?}",
);
}
#[test]
fn extract_json_region_balanced_unparseable_stderr_wins_first_region() {
let input = "[stderr bracket message]\n{\"iops\": 300}";
let first_region = extract_json_region(input).expect(
"leading `[stderr bracket message]` is a balanced region \
at the byte level (ignoring JSON validity); finder must \
return it",
);
assert_eq!(
first_region, "[stderr bracket message]",
"balanced-but-invalid-JSON regions still win the \
first-region scan; the following valid `{{..}}` is \
never inspected by the finder",
);
let m = extract_metrics(input, &OutputFormat::Json, MetricStream::Stdout).unwrap();
assert!(
m.is_empty(),
"region parses unsuccessfully as JSON â fallback \
yields no metrics. The stdout `iops` metric is lost. \
Documents the known limitation: mixed-stream \
captures lose valid JSON when a preceding balanced \
region fails to parse; got: {m:?}",
);
let m_clean = extract_metrics(
r#"{"iops": 400}"#,
&OutputFormat::Json,
MetricStream::Stdout,
)
.unwrap();
let clean_names: Vec<&str> = m_clean.iter().map(|x| x.name.as_str()).collect();
assert!(
clean_names.contains(&"iops"),
"control: stdout JSON in isolation must extract the \
`iops` metric so the preceding assertion is \
isolating the noise-interaction behaviour, not \
hiding a broken extractor; got: {clean_names:?}",
);
}
#[test]
fn walk_json_leaves_polarity_is_unknown_before_hint_resolution() {
let v: serde_json::Value = serde_json::from_str(r#"{"a": 1}"#).unwrap();
let m = walk_json_leaves(&v, MetricSource::Json, MetricStream::Stdout);
assert_eq!(m[0].polarity, Polarity::Unknown);
}
#[test]
fn walk_json_leaves_tags_source() {
let v: serde_json::Value = serde_json::from_str(r#"{"a": 1}"#).unwrap();
let json_tagged = walk_json_leaves(&v, MetricSource::Json, MetricStream::Stdout);
assert_eq!(json_tagged[0].source, MetricSource::Json);
let llm_tagged = walk_json_leaves(&v, MetricSource::LlmExtract, MetricStream::Stdout);
assert_eq!(llm_tagged[0].source, MetricSource::LlmExtract);
}
#[test]
fn walk_json_leaves_tags_stream_orthogonally_to_source() {
let obj: serde_json::Value = serde_json::from_str(r#"{"a": 1}"#).unwrap();
let arr: serde_json::Value = serde_json::from_str(r#"[{"a": 1}, {"b": 2}]"#).unwrap();
for (fixture_label, value, expected_len) in
[("object", &obj, 1_usize), ("array", &arr, 2_usize)]
{
for (src, stream) in [
(MetricSource::Json, MetricStream::Stdout),
(MetricSource::Json, MetricStream::Stderr),
(MetricSource::LlmExtract, MetricStream::Stdout),
(MetricSource::LlmExtract, MetricStream::Stderr),
] {
let tagged = walk_json_leaves(value, src, stream);
assert_eq!(
tagged.len(),
expected_len,
"walker on {fixture_label} fixture must produce \
exactly {expected_len} leaf(s); got {}",
tagged.len(),
);
for (i, m) in tagged.iter().enumerate() {
assert_eq!(
m.source, src,
"{fixture_label} leaf {i}: source tag must \
match the argument ({src:?}); got {:?}",
m.source,
);
assert_eq!(
m.stream, stream,
"{fixture_label} leaf {i}: stream tag must \
match the argument ({stream:?}); got {:?}. \
A regression that stamped the stream only \
on the object-recurse branch would pass \
the object fixture but fail here.",
m.stream,
);
}
}
}
}
#[test]
fn extract_metrics_threads_stream_from_argument_to_emitted_metric() {
let json = r#"{"iops": 100}"#;
for stream in [MetricStream::Stdout, MetricStream::Stderr] {
let metrics = extract_metrics(json, &OutputFormat::Json, stream).unwrap();
assert_eq!(metrics.len(), 1, "one leaf expected from {{\"iops\": 100}}",);
assert_eq!(
metrics[0].stream, stream,
"extract_metrics must thread stream={stream:?} to the \
emitted Metric; got {:?}",
metrics[0].stream,
);
}
}
#[test]
fn json_deeply_nested_array_of_objects() {
let s = r#"{"samples": [{"iops": 100}, {"iops": 200}, {"iops": 300}]}"#;
let m = extract_metrics(s, &OutputFormat::Json, MetricStream::Stdout).unwrap();
assert_eq!(m.len(), 3);
let names: Vec<&str> = m.iter().map(|x| x.name.as_str()).collect();
assert!(names.contains(&"samples.0.iops"));
assert!(names.contains(&"samples.1.iops"));
assert!(names.contains(&"samples.2.iops"));
}
#[test]
fn json_large_integer_round_trip_via_f64() {
let s = r#"{"big_iops": 1000000000000}"#; let m = extract_metrics(s, &OutputFormat::Json, MetricStream::Stdout).unwrap();
assert_eq!(m.len(), 1);
assert_eq!(m[0].value, 1_000_000_000_000.0);
}
#[test]
fn json_fio_style_full_output_with_multiline_banner() {
let s = "fio-3.36 starting up\n\
Running fio with 4 jobs\n\
test: (g=0): rw=randread, bs=4k, ioengine=libaio\n\
\n\
{\"jobs\": [{\"jobname\": \"test\", \"read\": {\"iops\": 12345, \"bw_bytes\": 50593792}}], \
\"disk_util\": [{\"util\": 99.5}]}";
let m = extract_metrics(s, &OutputFormat::Json, MetricStream::Stdout).unwrap();
assert_eq!(m.len(), 3);
let by_name: std::collections::BTreeMap<&str, f64> =
m.iter().map(|x| (x.name.as_str(), x.value)).collect();
assert_eq!(by_name.get("jobs.0.read.iops"), Some(&12345.0));
assert_eq!(by_name.get("jobs.0.read.bw_bytes"), Some(&50593792.0));
assert_eq!(by_name.get("disk_util.0.util"), Some(&99.5));
}
#[test]
fn walk_json_leaves_skips_nonfinite_defensively() {
assert!(serde_json::Number::from_f64(f64::NAN).is_none());
assert!(serde_json::Number::from_f64(f64::INFINITY).is_none());
assert!(serde_json::Number::from_f64(f64::NEG_INFINITY).is_none());
assert!(serde_json::Number::from_f64(2.78).is_some());
}
#[test]
fn json_large_integer_above_2_pow_53_loses_precision() {
let s = r#"{"huge": 9007199254740993}"#;
let m = extract_metrics(s, &OutputFormat::Json, MetricStream::Stdout).unwrap();
assert_eq!(m.len(), 1);
assert_eq!(m[0].value, 9_007_199_254_740_992.0_f64);
let rounded: f64 = 9_007_199_254_740_993_u64 as f64;
assert_eq!(m[0].value, rounded);
assert_eq!(m[0].value as u64, 9_007_199_254_740_992_u64);
assert_ne!(m[0].value as u64, 9_007_199_254_740_993_u64);
}
#[test]
fn json_integer_at_2_pow_53_is_exact() {
let s = r#"{"exact": 9007199254740992}"#;
let m = extract_metrics(s, &OutputFormat::Json, MetricStream::Stdout).unwrap();
assert_eq!(m.len(), 1);
assert_eq!(m[0].value, 9_007_199_254_740_992.0_f64);
}
#[test]
fn find_and_parse_json_recovers_object_with_trailing_garbage() {
let s = r#"{"a": 1, "b": 2} --- trailing prose from banner"#;
let v = find_and_parse_json(s).expect("trailing garbage must not block parse");
assert_eq!(v["a"], serde_json::json!(1));
assert_eq!(v["b"], serde_json::json!(2));
}
#[test]
fn find_and_parse_json_recovers_array_with_trailing_garbage() {
let s = "[1, 2, 3]\nextra: banner line\n";
let v = find_and_parse_json(s).expect("array with trailing garbage must parse");
assert_eq!(v, serde_json::json!([1, 2, 3]));
}
#[test]
fn find_and_parse_json_with_banner_and_trailer() {
let s = "fio-3.36 starting up\n{\"iops\": 100}\nfio done }";
let v = find_and_parse_json(s).expect("banner + trailer must resolve to body");
assert_eq!(v["iops"], serde_json::json!(100));
}
#[test]
fn find_and_parse_json_returns_first_region_when_trailer_also_balanced() {
let s = r#"{"first": 1} unrelated {"second": 2}"#;
let v = find_and_parse_json(s).expect("first balanced region parses");
assert_eq!(v["first"], serde_json::json!(1));
assert!(v.get("second").is_none(), "second region must not merge in");
}
#[test]
fn find_and_parse_json_ignores_braces_inside_string_literals() {
let s = "fio-3.36 starting up\n\
{\"msg\": \"look at {nested} in text\", \"ok\": 1}\n\
trailing banner";
let v = find_and_parse_json(s).expect("embedded braces in string must not break scan");
assert_eq!(v["msg"], serde_json::json!("look at {nested} in text"));
assert_eq!(v["ok"], serde_json::json!(1));
}
#[test]
fn json_negative_numbers_extract_preserving_sign() {
let s = r#"{"delta_ns": -500.5, "underflow": -1000000}"#;
let m = extract_metrics(s, &OutputFormat::Json, MetricStream::Stdout).unwrap();
let by_name: std::collections::BTreeMap<&str, f64> =
m.iter().map(|x| (x.name.as_str(), x.value)).collect();
assert_eq!(by_name.get("delta_ns"), Some(&-500.5));
assert_eq!(by_name.get("underflow"), Some(&-1_000_000.0));
}
#[test]
fn json_zero_values_are_emitted_not_filtered() {
let s = r#"{"errors": 0, "cpu_idle_pct": 0.0, "count": -0.0}"#;
let m = extract_metrics(s, &OutputFormat::Json, MetricStream::Stdout).unwrap();
let by_name: std::collections::BTreeMap<&str, f64> =
m.iter().map(|x| (x.name.as_str(), x.value)).collect();
assert_eq!(by_name.len(), 3, "all three zeros must extract: {m:?}");
assert_eq!(by_name.get("errors"), Some(&0.0));
assert_eq!(by_name.get("cpu_idle_pct"), Some(&0.0));
assert_eq!(by_name.get("count"), Some(&0.0));
}
#[test]
fn json_mixed_signs_and_zero_all_extract() {
let s = r#"{"pos": 10.0, "neg": -10.0, "zero": 0.0}"#;
let m = extract_metrics(s, &OutputFormat::Json, MetricStream::Stdout).unwrap();
assert_eq!(m.len(), 3);
}
#[test]
fn json_empty_object_yields_no_metrics() {
let m = extract_metrics("{}", &OutputFormat::Json, MetricStream::Stdout).unwrap();
assert!(m.is_empty(), "empty object has no leaves: {m:?}");
}
#[test]
fn json_empty_array_yields_no_metrics() {
let m = extract_metrics("[]", &OutputFormat::Json, MetricStream::Stdout).unwrap();
assert!(m.is_empty(), "empty array has no leaves: {m:?}");
}
#[test]
fn json_nested_empty_containers_yield_no_metrics() {
let s = r#"{"outer": {"inner": {}, "also": []}}"#;
let m = extract_metrics(s, &OutputFormat::Json, MetricStream::Stdout).unwrap();
assert!(m.is_empty(), "nested empties emit nothing: {m:?}");
}
#[test]
fn json_empty_container_mixed_with_real_metrics() {
let s = r#"{"iops": 100.0, "meta": {}, "samples": []}"#;
let m = extract_metrics(s, &OutputFormat::Json, MetricStream::Stdout).unwrap();
assert_eq!(m.len(), 1);
assert_eq!(m[0].name, "iops");
assert_eq!(m[0].value, 100.0);
}
#[test]
fn walk_json_leaves_deep_nesting_paths_are_correct() {
let s = r#"{"a":{"b":{"c":{"d":{"e":{"f": 42.0}}}}}}"#;
let m = extract_metrics(s, &OutputFormat::Json, MetricStream::Stdout).unwrap();
assert_eq!(m.len(), 1);
assert_eq!(m[0].name, "a.b.c.d.e.f");
assert_eq!(m[0].value, 42.0);
}
#[test]
fn walk_json_leaves_siblings_do_not_accumulate_path() {
let s = r#"{"root":{"a": 1, "b": 2, "c": 3}}"#;
let m = extract_metrics(s, &OutputFormat::Json, MetricStream::Stdout).unwrap();
assert_eq!(m.len(), 3);
let names: std::collections::BTreeSet<&str> = m.iter().map(|x| x.name.as_str()).collect();
let expected: std::collections::BTreeSet<&str> =
["root.a", "root.b", "root.c"].into_iter().collect();
assert_eq!(names, expected, "path must truncate between siblings");
}
#[test]
fn walk_json_leaves_deep_array_object_interleaving() {
let s = r#"{"data":[{"vals":[10.0, 20.0]},{"vals":[30.0]}]}"#;
let m = extract_metrics(s, &OutputFormat::Json, MetricStream::Stdout).unwrap();
let by_name: std::collections::BTreeMap<&str, f64> =
m.iter().map(|x| (x.name.as_str(), x.value)).collect();
assert_eq!(by_name.get("data.0.vals.0"), Some(&10.0));
assert_eq!(by_name.get("data.0.vals.1"), Some(&20.0));
assert_eq!(by_name.get("data.1.vals.0"), Some(&30.0));
assert_eq!(by_name.len(), 3);
}
#[test]
fn walk_json_leaves_depth_cap_skips_deeply_nested_subtree() {
let mut value = serde_json::json!({"leaf": 42.0});
for _ in 0..100 {
let mut m = serde_json::Map::new();
m.insert("x".to_string(), value);
value = serde_json::Value::Object(m);
}
let metrics = walk_json_leaves(&value, MetricSource::Json, MetricStream::Stdout);
let real_leaves: Vec<_> = metrics
.iter()
.filter(|m| m.name != WALK_TRUNCATION_SENTINEL_NAME)
.collect();
assert!(
real_leaves.is_empty(),
"leaf beyond MAX_WALK_DEPTH cap must not be emitted, got {real_leaves:?}"
);
let sentinel = metrics
.iter()
.find(|m| m.name == WALK_TRUNCATION_SENTINEL_NAME)
.expect("truncation sentinel must be present on cap hit");
assert!(
sentinel.value > MAX_WALK_DEPTH as f64,
"sentinel value must carry the depth at which truncation fired, got {}",
sentinel.value,
);
}
#[test]
fn walk_json_leaves_depth_cap_boundary_leaf_preserved() {
let mut value = serde_json::Value::Number(serde_json::Number::from_f64(42.0).unwrap());
for _ in 0..MAX_WALK_DEPTH {
let mut m = serde_json::Map::new();
m.insert("x".to_string(), value);
value = serde_json::Value::Object(m);
}
let metrics = walk_json_leaves(&value, MetricSource::Json, MetricStream::Stdout);
assert_eq!(metrics.len(), 1, "boundary leaf must be preserved");
assert_eq!(metrics[0].value, 42.0);
}
#[test]
fn walk_json_leaves_mixed_depth_leaves_all_emitted() {
let value = serde_json::json!({
"shallow": 1.0,
"mid": {
"leaf": 2.0,
"deeper": {
"still": {
"further": 3.0
}
}
},
"also_shallow": 4.0,
"deeper_sibling": {
"only_child": 5.0
}
});
let metrics = walk_json_leaves(&value, MetricSource::Json, MetricStream::Stdout);
let by_name: std::collections::BTreeMap<&str, f64> =
metrics.iter().map(|m| (m.name.as_str(), m.value)).collect();
assert_eq!(by_name.get("shallow"), Some(&1.0));
assert_eq!(by_name.get("mid.leaf"), Some(&2.0));
assert_eq!(by_name.get("mid.deeper.still.further"), Some(&3.0));
assert_eq!(by_name.get("also_shallow"), Some(&4.0));
assert_eq!(by_name.get("deeper_sibling.only_child"), Some(&5.0));
assert_eq!(metrics.len(), 5, "exactly five numeric leaves expected");
}
#[test]
fn walk_json_leaves_array_chain_paths_correct() {
let value = serde_json::json!({
"a": [
[[1.0, 2.0], [3.0, 4.0]],
[[5.0, 6.0], [7.0, 8.0]]
]
});
let metrics = walk_json_leaves(&value, MetricSource::Json, MetricStream::Stdout);
let names: Vec<&str> = metrics.iter().map(|m| m.name.as_str()).collect();
assert_eq!(names.len(), 8);
assert_eq!(names[0], "a.0.0.0");
assert_eq!(names[1], "a.0.0.1");
assert_eq!(names[2], "a.0.1.0");
assert_eq!(names[3], "a.0.1.1");
assert_eq!(names[4], "a.1.0.0");
assert_eq!(names[5], "a.1.0.1");
assert_eq!(names[6], "a.1.1.0");
assert_eq!(names[7], "a.1.1.1");
assert_eq!(metrics[0].value, 1.0);
assert_eq!(metrics[7].value, 8.0);
}
#[test]
fn walk_json_leaves_null_at_boundary_produces_no_metric() {
let mut value = serde_json::Value::Null;
for _ in 0..MAX_WALK_DEPTH {
let mut m = serde_json::Map::new();
m.insert("a".to_string(), value);
value = serde_json::Value::Object(m);
}
let metrics = walk_json_leaves(&value, MetricSource::Json, MetricStream::Stdout);
assert!(
metrics.is_empty(),
"Null leaves must produce no metrics (and no truncation sentinel), \
got {metrics:?}",
);
}
#[test]
fn module_level_example_usage() {
const EXAMPLE_PAYLOAD: crate::test_support::Payload = crate::test_support::Payload {
name: "example",
kind: crate::test_support::PayloadKind::Binary("example"),
output: OutputFormat::Json,
default_args: &[],
default_checks: &[],
metrics: &[],
include_files: &[],
uses_parent_pgrp: false,
known_flags: None,
metric_bounds: None,
};
let stdout = r#"{"throughput": 42.5}"#;
let m = extract_metrics(stdout, &EXAMPLE_PAYLOAD.output, MetricStream::Stdout).unwrap();
assert_eq!(m.len(), 1);
assert_eq!(m[0].name, "throughput");
assert_eq!(m[0].value, 42.5);
assert_eq!(m[0].source, MetricSource::Json);
}
}