use super::*;
pub(crate) fn normalize_process_info(
info: &mut serde_json::Value,
home: &Path,
name: &str,
samples: Samples,
) {
let pid = info["pid"]
.as_i64()
.unwrap_or_else(|| panic!("pid must be a real positive OS pid: {info}"));
assert!(pid > 0, "pid must be a real positive OS pid: {info}");
info["uptime_ms"]
.as_u64()
.unwrap_or_else(|| panic!("uptime_ms must be present: {info}"));
let home_str = home.to_str().unwrap();
for (key, stream) in [("out_file", "out"), ("err_file", "err")] {
let path = info[key]
.as_str()
.unwrap_or_else(|| panic!("{key} must be a string: {info}"));
assert!(
path.starts_with(home_str),
"{key} must be rooted under $SHEP_HOME: {path}"
);
assert!(
path.ends_with(&format!("{name}-0-{stream}.log")),
"{key} must name this sheep's own log file: {path}"
);
}
match samples {
Samples::Live => {
let bytes = info["memory_bytes"].as_u64().unwrap_or_else(|| {
panic!("memory_bytes must be a live reading off the host: {info}")
});
assert!(
bytes > 0,
"a running sheep's tree cannot be 0 bytes: {info}"
);
info["cpu_ms"]
.as_u64()
.unwrap_or_else(|| panic!("cpu_ms must be a live reading off the host: {info}"));
}
Samples::None => {
assert!(
info["memory_bytes"].is_null(),
"a verb that takes no live sample must report no memory: {info}"
);
assert!(
info["cpu_ms"].is_null(),
"a verb that takes no live sample must report no CPU counter: {info}"
);
}
}
info["pid"] = serde_json::Value::Null;
info["uptime_ms"] = serde_json::Value::Null;
info["out_file"] = serde_json::Value::Null;
info["err_file"] = serde_json::Value::Null;
info["cpu_percent"] = serde_json::Value::Null;
info["memory_bytes"] = serde_json::Value::Null;
info["cpu_ms"] = serde_json::Value::Null;
if let Some(lambs) = info["lambs"].as_array_mut() {
for lamb in lambs {
lamb["pid"] = serde_json::Value::Null;
}
}
}
pub(crate) fn normalize_host(envelope: &mut serde_json::Value) {
let Some(host) = envelope.get_mut("host") else {
return;
};
let Some(host) = host.as_object_mut() else {
panic!("host must be an object on a machine sysinfo can read: {host}");
};
for key in ["memory_used_bytes", "memory_total_bytes"] {
let bytes = host[key]
.as_u64()
.unwrap_or_else(|| panic!("{key} is not a rate and is never absent: {host:?}"));
assert!(bytes > 0, "{key} must be a live reading off the host");
}
for value in host.values_mut() {
*value = serde_json::Value::Null;
}
}
#[derive(Debug, Clone, Copy)]
pub(crate) enum Samples {
Live,
None,
}
pub(crate) fn assert_envelope_matches_fixture(
output: &Output,
home: &Path,
command: &str,
sheep_name: &str,
samples: Samples,
) {
let mut envelope: serde_json::Value =
serde_json::from_slice(&output.stdout).unwrap_or_else(|e| {
panic!(
"{command}: stdout was not JSON: {e}: {}",
String::from_utf8_lossy(&output.stdout)
)
});
envelope["data"] = serde_json::Value::Array(sheep_rows(&envelope["data"]));
{
let data = envelope["data"]
.as_array()
.unwrap_or_else(|| panic!("{command}: data must be an array"));
assert_eq!(data.len(), 1, "{command}: exactly one sheep is expected");
}
normalize_process_info(&mut envelope["data"][0], home, sheep_name, samples);
normalize_host(&mut envelope);
assert_eq!(
envelope,
load_fixture(command),
"{command} envelope drifted from its committed fixture"
);
}
pub(crate) fn combined_output(output: &Output) -> String {
let mut text = String::from_utf8_lossy(&output.stdout).into_owned();
text.push_str(&String::from_utf8_lossy(&output.stderr));
text
}
pub(crate) fn assert_json_error(output: &Output, expected_status: i32, expected_error_code: &str) {
assert_eq!(
output.status.code(),
Some(expected_status),
"stderr={}",
String::from_utf8_lossy(&output.stderr)
);
assert!(
output.stdout.is_empty(),
"stdout must stay empty on failure: {}",
String::from_utf8_lossy(&output.stdout)
);
let err: serde_json::Value = serde_json::from_slice(&output.stderr).unwrap_or_else(|e| {
panic!(
"stderr was not JSON: {e}: {}",
String::from_utf8_lossy(&output.stderr)
)
});
assert_eq!(err["error"]["code"], expected_error_code, "{err}");
}
pub(crate) fn as_shep_spells_it(path: &Path) -> String {
let canonical = std::fs::canonicalize(path).expect("canonicalize the recorded binary");
shep_core::paths::strip_verbatim_prefix(&canonical)
.display()
.to_string()
}
pub(crate) fn unstamped_file(path: &Path) -> String {
let text = std::fs::read_to_string(path).unwrap();
let mut out = String::new();
for line in text.lines() {
out.push_str(shep_core::logstamp::strip(line));
out.push('\n');
}
out
}