use std::hash::Hasher;
use sha2::{Digest, Sha256};
use siphasher::sip128::{Hasher128, SipHasher24};
const FUN_PEPPER: &[u8] = b"ktstr-fun-mode/v1";
#[derive(Clone, Debug)]
pub struct Funifier {
key: [u8; 16],
}
impl Funifier {
pub fn ephemeral() -> Self {
let pid = std::process::id() as u64;
let ns = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos() as u64)
.unwrap_or(0);
let mut h = Sha256::new();
h.update(FUN_PEPPER);
h.update([0u8]);
h.update(b"ephemeral");
h.update([0u8]);
h.update(pid.to_le_bytes());
h.update(ns.to_le_bytes());
let digest = h.finalize();
let mut key = [0u8; 16];
key.copy_from_slice(&digest[..16]);
Self { key }
}
pub fn with_seed(seed: &str) -> Self {
let mut h = Sha256::new();
h.update(FUN_PEPPER);
h.update([0u8]);
h.update(seed.as_bytes());
let digest = h.finalize();
let mut key = [0u8; 16];
key.copy_from_slice(&digest[..16]);
Self { key }
}
fn keyed_hash(&self, category: &[u8], payload: &[u8]) -> u128 {
let mut buf = Vec::with_capacity(category.len() + 1 + payload.len());
buf.extend_from_slice(category);
buf.push(0u8);
buf.extend_from_slice(payload);
let mut h = SipHasher24::new_with_key(&self.key);
h.write(&buf);
h.finish128().as_u128()
}
pub fn petname_for(&self, category: &str, payload: &str) -> String {
let h = self.keyed_hash(category.as_bytes(), payload.as_bytes());
let adj_idx = (h & 0xff) as usize;
let ani_idx = ((h >> 8) & 0xff) as usize;
let adj = ADJECTIVES[adj_idx % ADJECTIVES.len()];
let ani = ANIMALS[ani_idx % ANIMALS.len()];
format!("{adj}-{ani}")
}
pub fn numeric_id(&self, category: &str, n: u64) -> u64 {
let h = self.keyed_hash(category.as_bytes(), &n.to_le_bytes());
h as u64
}
pub fn numeric_id_i64(&self, category: &str, n: i64) -> i64 {
if n == 0 {
return 0;
}
let abs = n.unsigned_abs();
let funified = (self.numeric_id(category, abs) & ((1u64 << 63) - 1)) as i64;
if n < 0 { -funified } else { funified }
}
pub fn numeric_id_i32(&self, category: &str, n: i32) -> i32 {
if Self::is_sentinel_i32(n) {
return n;
}
let abs = n.unsigned_abs() as u64;
let funified = (self.numeric_id(category, abs) & ((1u32 << 31) - 1) as u64) as i32;
if n < 0 { -funified } else { funified }
}
pub fn is_sentinel_i32(n: i32) -> bool {
n == 0 || n == i32::MIN || n == i32::MAX
}
pub fn numeric_id_u32(&self, category: &str, n: u32) -> u32 {
if Self::is_sentinel_u32(n) {
return n;
}
(self.numeric_id(category, n as u64) & u32::MAX as u64) as u32
}
pub fn is_sentinel_u64(n: u64) -> bool {
n == 0 || n == u64::MAX
}
pub fn is_sentinel_u32(n: u32) -> bool {
n == 0 || n == u32::MAX
}
pub fn is_u32_category(key: &str) -> bool {
let lc = key.to_ascii_lowercase();
if matches!(
lc.as_str(),
"cpu_id"
| "uid" | "euid" | "ruid" | "suid" | "fsuid"
| "gid" | "egid" | "rgid" | "sgid" | "fsgid"
| "kuid" | "kgid"
) {
return true;
}
const U32_SUFFIXES: &[&str] = &["_u32", "_u32_id"];
for suffix in U32_SUFFIXES {
if lc.ends_with(suffix) {
return true;
}
}
false
}
pub fn is_metric_passthrough(key: &str) -> bool {
let lc = key.to_ascii_lowercase();
if matches!(
lc.as_str(),
"schema"
| "version"
| "type"
| "kind"
| "status"
| "state"
| "result"
| "verdict"
| "outcome"
| "phase"
| "policy"
| "priority"
| "nice"
| "weight"
| "capacity"
| "size"
| "len"
| "length"
| "depth"
| "index"
| "idx"
| "level"
| "tier"
| "rank"
| "slot"
| "epoch"
| "generation"
| "nr_running"
| "nr_queued"
| "nr_failed"
| "nr_switches"
| "runqueue_depth"
| "numa_hit" | "numa_miss" | "numa_foreign" | "numa_interleave_hit" | "numa_local" | "numa_other"
| "select_cpu_fallback" | "dispatch_local_dsq_offline" | "dispatch_keep_last" | "enq_skip_exiting" | "enq_skip_migration_disabled" | "reenq_immed" | "reenq_local_repeat" | "refill_slice_dfl" | "bypass_duration" | "bypass_dispatch" | "bypass_activate" | "insert_not_owned" | "sub_bypass_dispatch"
| "cnt" | "nsecs" | "misses" | "verified_insns"
| "cycles" | "instructions" | "cache_misses" | "branch_misses"
| "flags" | "ops_qseq" | "kick_sync" | "nr_immed" | "rq_clock"
| "nr" | "seq"
| "nr_threads" | "prio" | "static_prio" | "normal_prio" | "nvcsw" | "nivcsw" | "signal_nvcsw" | "signal_nivcsw"
| "bytes_read" | "bytes_written" | "io_errors"
| "cpus" | "cpuset_cpus"
) {
return true;
}
const METRIC_SUFFIXES: &[&str] = &[
"_count",
"_total",
"_completed",
"_dropped",
"_failed",
"_skipped",
"_throttled",
"_read",
"_written",
"_errors",
"_per_sec",
"_per_ms",
"_rate",
"_hz",
"_ratio",
"_fraction",
"_pct",
"_percent",
"_ns",
"_us",
"_ms",
"_sec",
"_seconds",
"_bytes",
"_kb",
"_mb",
"_gb",
"_pages",
"_min",
"_max",
"_mean",
"_avg",
"_stddev",
"_p50",
"_p90",
"_p95",
"_p99",
"_capacity",
"_size",
"_depth",
"_len",
"_length",
"_weight",
"_nice",
"_priority",
"_index",
"_idx",
"_offset",
"_generation",
"_epoch",
"_version",
"_status",
"_state",
"_kind",
"_type",
"_phase",
"_verdict",
"_outcome",
];
for suffix in METRIC_SUFFIXES {
if lc.ends_with(suffix) {
return true;
}
}
false
}
}
pub fn funify_json(value: serde_json::Value, f: &Funifier) -> serde_json::Value {
funify_json_with_context(value, f, None)
}
fn funify_json_with_context(
value: serde_json::Value,
f: &Funifier,
category: Option<&str>,
) -> serde_json::Value {
use serde_json::Value;
match value {
Value::Object(map) => {
let mut out = serde_json::Map::with_capacity(map.len());
for (k, v) in map {
let child_cat: Option<&str> = if Funifier::is_metric_passthrough(&k) {
None
} else {
Some(k.as_str())
};
let funified = funify_json_with_context(v, f, child_cat);
out.insert(k, funified);
}
Value::Object(out)
}
Value::Array(items) => {
let out: Vec<Value> = items
.into_iter()
.map(|v| funify_json_with_context(v, f, category))
.collect();
Value::Array(out)
}
Value::String(s) => {
if let Some(cat) = category {
Value::String(f.petname_for(cat, &s))
} else {
Value::String(s)
}
}
Value::Number(num) => {
if num.is_f64() {
return Value::Number(num);
}
if let Some(cat) = category {
if let Some(u) = num.as_u64() {
if Funifier::is_sentinel_u64(u) {
return Value::Number(num);
}
if Funifier::is_u32_category(cat) {
let narrow = if u > u32::MAX as u64 {
u32::MAX
} else {
u as u32
};
let funified = f.numeric_id_u32(cat, narrow);
Value::Number(serde_json::Number::from(funified))
} else {
Value::Number(serde_json::Number::from(f.numeric_id(cat, u)))
}
} else if let Some(i) = num.as_i64() {
if Funifier::is_u32_category(cat) {
let narrow = if i < i32::MIN as i64 {
i32::MIN
} else if i > i32::MAX as i64 {
i32::MAX
} else {
i as i32
};
let funified = f.numeric_id_i32(cat, narrow);
Value::Number(serde_json::Number::from(funified))
} else {
Value::Number(serde_json::Number::from(f.numeric_id_i64(cat, i)))
}
} else {
Value::Number(num)
}
} else {
Value::Number(num)
}
}
other => other,
}
}
const ADJECTIVES: &[&str] = &[
"able",
"agile",
"airy",
"amber",
"ample",
"amused",
"ancient",
"angry",
"antsy",
"apt",
"ardent",
"arid",
"ashen",
"auburn",
"aware",
"awesome",
"balmy",
"bashful",
"beaded",
"beamy",
"bendy",
"best",
"big",
"bitter",
"black",
"blameless",
"blazing",
"bleached",
"blissful",
"blithe",
"blocky",
"bloomy",
"blue",
"blunt",
"bold",
"bony",
"bouncy",
"brainy",
"brassy",
"brave",
"breezy",
"bright",
"brisk",
"bristly",
"brittle",
"broad",
"bronze",
"brown",
"bubbly",
"burly",
"busy",
"buttery",
"calm",
"candid",
"casual",
"cheery",
"chilly",
"chipper",
"chubby",
"chummy",
"civic",
"classy",
"clean",
"clear",
"clever",
"cloudy",
"clumsy",
"coiled",
"cold",
"comfy",
"cool",
"copper",
"cosmic",
"cozy",
"crafty",
"crimson",
"crisp",
"crystal",
"curious",
"dainty",
"damp",
"dapper",
"daring",
"dark",
"dashing",
"dazed",
"deep",
"deft",
"delft",
"dewy",
"dim",
"dimpled",
"dingy",
"dippy",
"distant",
"dizzy",
"dopey",
"dotted",
"drafty",
"dreamy",
"dressy",
"drowsy",
"dry",
"dual",
"dulcet",
"dusty",
"eager",
"early",
"easy",
"eclectic",
"edgy",
"eerie",
"elastic",
"elated",
"elder",
"electric",
"elfin",
"emerald",
"empty",
"endless",
"ethereal",
"even",
"exact",
"fabled",
"faint",
"fancy",
"fawn",
"fearless",
"feisty",
"ferny",
"festive",
"fey",
"fierce",
"fiery",
"filmy",
"fine",
"fizzy",
"flat",
"fleet",
"fleeting",
"flighty",
"flinty",
"floaty",
"floral",
"flowy",
"fluffy",
"fluted",
"foamy",
"fond",
"foppish",
"frank",
"fresh",
"fretful",
"frilly",
"frisky",
"frosty",
"frugal",
"fudgy",
"funky",
"furry",
"fuzzy",
"gallant",
"game",
"gawky",
"gentle",
"genuine",
"ghostly",
"giddy",
"giggly",
"glad",
"glassy",
"gleaming",
"glib",
"global",
"glossy",
"glowing",
"glum",
"golden",
"good",
"goopy",
"gossamer",
"graceful",
"grainy",
"grand",
"grassy",
"great",
"green",
"grim",
"groovy",
"grown",
"grumpy",
"gummy",
"gusty",
"hale",
"halting",
"handy",
"happy",
"hardy",
"harmless",
"hasty",
"hazy",
"heady",
"hearty",
"heavy",
"helpful",
"high",
"hilly",
"hippy",
"hoarse",
"hollow",
"holy",
"homely",
"honest",
"hooked",
"hopeful",
"hot",
"humble",
"hungry",
"icy",
"ideal",
"iffy",
"immense",
"indigo",
"inland",
"inner",
"ironic",
"itchy",
"ivory",
"jade",
"jaunty",
"jazzy",
"jelly",
"jiffy",
"jiggly",
"jolly",
"jovial",
"joyful",
"jumpy",
"kelpy",
"keen",
"kind",
"kindly",
"kinetic",
"knotty",
"lacy",
"ladylike",
"lambent",
"lanky",
"lapis",
"large",
"late",
"lavish",
"lawful",
"lazy",
"leafy",
"lean",
"lemony",
"lenient",
"level",
"lifelong",
"light",
"lily",
"linen",
"linked",
"lithe",
"little",
"lively",
"loamy",
"lofty",
"long",
"loud",
"lovely",
];
const ANIMALS: &[&str] = &[
"aardvark",
"albatross",
"alligator",
"alpaca",
"ant",
"antelope",
"ape",
"armadillo",
"ass",
"auk",
"axolotl",
"baboon",
"badger",
"bandicoot",
"barnacle",
"barracuda",
"basilisk",
"bat",
"bear",
"beaver",
"bee",
"beetle",
"bison",
"blackbird",
"boar",
"bobcat",
"bonobo",
"boomslang",
"buffalo",
"bulldog",
"bullfrog",
"bumblebee",
"bushbaby",
"butterfly",
"buzzard",
"camel",
"canary",
"capybara",
"caracal",
"cardinal",
"caribou",
"carp",
"cat",
"caterpillar",
"catfish",
"centaur",
"centipede",
"chameleon",
"cheetah",
"chickadee",
"chicken",
"chihuahua",
"chinchilla",
"chipmunk",
"civet",
"clam",
"cobra",
"cockatoo",
"cod",
"coral",
"cougar",
"cow",
"coyote",
"crab",
"crane",
"crayfish",
"cricket",
"crocodile",
"crow",
"cuckoo",
"curlew",
"cuttlefish",
"dachshund",
"dalmatian",
"deer",
"dingo",
"dodo",
"dog",
"dolphin",
"donkey",
"dormouse",
"dove",
"dragon",
"dragonfly",
"drake",
"duck",
"dugong",
"eagle",
"eel",
"egret",
"elephant",
"elk",
"emu",
"ermine",
"falcon",
"fawn",
"ferret",
"finch",
"firefly",
"fish",
"flamingo",
"flatfish",
"flounder",
"fly",
"flycatcher",
"fowl",
"fox",
"frog",
"fulmar",
"gannet",
"gar",
"gazelle",
"gecko",
"gerbil",
"gibbon",
"giraffe",
"gnat",
"gnu",
"goat",
"goldfish",
"goose",
"gopher",
"gorilla",
"goshawk",
"grasshopper",
"greyhound",
"grouse",
"guanaco",
"gull",
"guppy",
"haddock",
"hagfish",
"halibut",
"hamster",
"hare",
"harrier",
"hawk",
"hedgehog",
"hen",
"heron",
"herring",
"hippo",
"hognose",
"hornet",
"horse",
"hound",
"hyena",
"ibex",
"ibis",
"iguana",
"impala",
"jackal",
"jackrabbit",
"jaguar",
"javelina",
"jay",
"jellyfish",
"kangaroo",
"katydid",
"kestrel",
"kingfisher",
"kite",
"kiwi",
"koala",
"kookaburra",
"krill",
"lamb",
"lamprey",
"langur",
"lark",
"lemming",
"lemur",
"leopard",
"lion",
"lizard",
"llama",
"lobster",
"locust",
"loon",
"louse",
"lynx",
"macaque",
"macaw",
"mackerel",
"magpie",
"mallard",
"mammoth",
"manatee",
"mandrill",
"marlin",
"marmoset",
"marmot",
"marten",
"meerkat",
"mink",
"minnow",
"mole",
"molly",
"mongoose",
"monkey",
"moose",
"mosquito",
"moth",
"mouse",
"mule",
"muskrat",
"narwhal",
"newt",
"nightingale",
"ocelot",
"octopus",
"okapi",
"opossum",
"orangutan",
"orca",
"oriole",
"ostrich",
"otter",
"owl",
"ox",
"oyster",
"panda",
"pangolin",
"panther",
"parakeet",
"parrot",
"partridge",
"peacock",
"pelican",
"penguin",
"perch",
"petrel",
"pheasant",
"pig",
"pigeon",
"piglet",
"pika",
"pike",
"pinscher",
"piranha",
"platypus",
"polecat",
"pony",
"poodle",
"porcupine",
"porpoise",
"possum",
"prawn",
"puffin",
"puma",
"puppy",
"python",
"quagga",
"quail",
"quetzal",
"quokka",
"rabbit",
"raccoon",
"ram",
"rat",
"raven",
"reindeer",
"rhino",
"robin",
];
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn petname_deterministic_per_seed() {
let a = Funifier::with_seed("demo-seed");
let b = Funifier::with_seed("demo-seed");
assert_eq!(
a.petname_for("comm", "ktstr_test"),
b.petname_for("comm", "ktstr_test"),
);
}
#[test]
fn petname_namespaced_by_category() {
let f = Funifier::with_seed("demo");
let pid_name = f.petname_for("pid", "42");
let cg_name = f.petname_for("cgroup", "42");
assert_ne!(
pid_name, cg_name,
"category bytes must namespace the keyed hash"
);
}
#[test]
fn petname_format_is_adjective_dash_animal() {
let f = Funifier::with_seed("demo");
let name = f.petname_for("comm", "anything");
let parts: Vec<&str> = name.split('-').collect();
assert_eq!(parts.len(), 2, "expected exactly two segments: {name}");
assert!(!parts[0].is_empty());
assert!(!parts[1].is_empty());
assert!(parts[0].chars().all(|c| c.is_ascii_lowercase()));
assert!(parts[1].chars().all(|c| c.is_ascii_lowercase()));
}
#[test]
fn numeric_id_deterministic() {
let f = Funifier::with_seed("demo");
assert_eq!(f.numeric_id("pid", 42), f.numeric_id("pid", 42));
assert_ne!(f.numeric_id("pid", 42), f.numeric_id("pid", 43));
assert_ne!(f.numeric_id("pid", 42), f.numeric_id("cgroup", 42));
}
#[test]
fn numeric_id_i64_preserves_zero_and_sign() {
let f = Funifier::with_seed("demo");
assert_eq!(f.numeric_id_i64("pid", 0), 0);
let pos = f.numeric_id_i64("pid", 42);
let neg = f.numeric_id_i64("pid", -42);
assert!(pos > 0);
assert!(neg < 0);
assert_eq!(pos, -neg, "abs value must match across signs");
}
#[test]
fn is_sentinel_u64_table() {
assert!(Funifier::is_sentinel_u64(0));
assert!(Funifier::is_sentinel_u64(u64::MAX));
assert!(!Funifier::is_sentinel_u64(1));
assert!(!Funifier::is_sentinel_u64(42));
}
#[test]
fn is_sentinel_u32_table() {
assert!(Funifier::is_sentinel_u32(0));
assert!(Funifier::is_sentinel_u32(u32::MAX));
assert!(!Funifier::is_sentinel_u32(1));
assert!(!Funifier::is_sentinel_u32(42));
}
#[test]
fn numeric_id_u32_masks_to_u32_range() {
let f = Funifier::with_seed("demo");
assert_eq!(f.numeric_id_u32("cpu_id", 0), 0);
assert_eq!(f.numeric_id_u32("cpu_id", u32::MAX), u32::MAX);
let id_42 = f.numeric_id_u32("cpu_id", 42);
assert_ne!(id_42, 42, "non-sentinel input must be funified");
assert_ne!(id_42, 0, "non-sentinel input must not collapse to 0");
assert_eq!(f.numeric_id_u32("cpu_id", 42), id_42);
assert_ne!(
f.numeric_id_u32("cpu_id", 42),
f.numeric_id_u32("uid", 42),
"category must namespace the funified output",
);
}
#[test]
fn is_u32_category_allowlist_hits() {
assert!(Funifier::is_u32_category("cpu_id"));
assert!(Funifier::is_u32_category("uid"));
assert!(Funifier::is_u32_category("euid"));
assert!(Funifier::is_u32_category("ruid"));
assert!(Funifier::is_u32_category("gid"));
assert!(Funifier::is_u32_category("egid"));
assert!(Funifier::is_u32_category("kuid"));
assert!(Funifier::is_u32_category("kgid"));
assert!(Funifier::is_u32_category("worker_u32"));
assert!(Funifier::is_u32_category("alien_id_u32_id"));
assert!(!Funifier::is_u32_category("pid"));
assert!(!Funifier::is_u32_category("worker_id"));
assert!(!Funifier::is_u32_category("cgroup"));
assert!(!Funifier::is_u32_category("comm"));
}
#[test]
fn funify_json_u32_category_stays_in_u32_range() {
let f = Funifier::with_seed("demo");
for n in [1u32, 7, 42, 100, 1024, 65535, 1_000_000, 0x7FFF_FFFF] {
let input = json!({ "cpu_id": n });
let out = funify_json(input, &f);
let funified = out["cpu_id"]
.as_u64()
.expect("u32 category must remain a Number");
assert!(
funified <= u32::MAX as u64,
"funified `cpu_id`={n} produced {funified}, exceeds u32::MAX. \
The u32-narrow dispatch is broken or the category fell through \
to numeric_id (full u64).",
);
}
}
#[test]
fn funify_json_u32_category_preserves_sentinels() {
let f = Funifier::with_seed("demo");
let input = json!({
"cpu_id_zero": { "cpu_id": 0 },
"cpu_id_max": { "cpu_id": u32::MAX },
"uid_zero": { "uid": 0 },
"uid_max": { "uid": u32::MAX },
});
let out = funify_json(input, &f);
assert_eq!(out["cpu_id_zero"]["cpu_id"], json!(0));
assert_eq!(out["cpu_id_max"]["cpu_id"], json!(u32::MAX));
assert_eq!(out["uid_zero"]["uid"], json!(0));
assert_eq!(out["uid_max"]["uid"], json!(u32::MAX));
}
#[test]
fn numeric_id_i32_masks_to_i32_range_and_preserves_sentinels() {
let f = Funifier::with_seed("demo");
assert_eq!(f.numeric_id_i32("kuid", 0), 0);
assert_eq!(f.numeric_id_i32("kuid", i32::MIN), i32::MIN);
assert_eq!(f.numeric_id_i32("kuid", i32::MAX), i32::MAX);
let pos = f.numeric_id_i32("kuid", 42);
let neg = f.numeric_id_i32("kuid", -42);
assert!(pos > 0, "positive input must funify to positive output");
assert!(neg < 0, "negative input must funify to negative output");
assert_eq!(pos, -neg, "abs value must match across signs");
assert_eq!(f.numeric_id_i32("kuid", 42), pos);
assert_ne!(
f.numeric_id_i32("kuid", 42),
f.numeric_id_i32("cpu_id", 42),
"category must namespace the funified output",
);
}
#[test]
fn is_sentinel_i32_table() {
assert!(Funifier::is_sentinel_i32(0));
assert!(Funifier::is_sentinel_i32(i32::MIN));
assert!(Funifier::is_sentinel_i32(i32::MAX));
assert!(!Funifier::is_sentinel_i32(1));
assert!(!Funifier::is_sentinel_i32(-1));
assert!(!Funifier::is_sentinel_i32(42));
}
#[test]
fn funify_json_u32_category_negative_stays_in_i32_range() {
let f = Funifier::with_seed("demo");
for n in [
-1i64,
-7,
-42,
-100,
-1024,
-65535,
-1_000_000,
i32::MIN as i64,
i32::MIN as i64 - 1,
i64::MIN,
] {
let input = json!({ "kuid": n });
let out = funify_json(input, &f);
let funified = out["kuid"]
.as_i64()
.expect("u32 category negative must remain a signed Number");
assert!(
(i32::MIN as i64..=i32::MAX as i64).contains(&funified),
"funified `kuid`={n} produced {funified}, exceeds i32 range. \
The i32-narrow dispatch in the i64 branch is broken or the \
category fell through to numeric_id_i64 (full i64).",
);
}
}
#[test]
fn is_metric_passthrough_allowlist_hits() {
assert!(Funifier::is_metric_passthrough("schema"));
assert!(Funifier::is_metric_passthrough("version"));
assert!(Funifier::is_metric_passthrough("type"));
assert!(Funifier::is_metric_passthrough("kind"));
assert!(Funifier::is_metric_passthrough("status"));
assert!(Funifier::is_metric_passthrough("nr_running"));
assert!(Funifier::is_metric_passthrough("nr_queued"));
assert!(Funifier::is_metric_passthrough("runqueue_depth"));
assert!(Funifier::is_metric_passthrough("nice"));
assert!(Funifier::is_metric_passthrough("weight"));
assert!(Funifier::is_metric_passthrough("priority"));
assert!(Funifier::is_metric_passthrough("reads_completed"));
assert!(Funifier::is_metric_passthrough("io_errors_total"));
assert!(Funifier::is_metric_passthrough("wakeups_per_sec"));
assert!(Funifier::is_metric_passthrough("memory_max_bytes"));
assert!(Funifier::is_metric_passthrough("cpu_max_quota_us"));
assert!(Funifier::is_metric_passthrough("page_locality_ratio"));
assert!(Funifier::is_metric_passthrough("cpu_time_fraction"));
assert!(Funifier::is_metric_passthrough("idle_pct"));
assert!(Funifier::is_metric_passthrough("queue_depth"));
assert!(Funifier::is_metric_passthrough("buffer_size"));
assert!(Funifier::is_metric_passthrough("thread_count"));
}
#[test]
fn is_metric_passthrough_allowlist_misses() {
assert!(!Funifier::is_metric_passthrough("pid"));
assert!(!Funifier::is_metric_passthrough("tid"));
assert!(!Funifier::is_metric_passthrough("tgid"));
assert!(!Funifier::is_metric_passthrough("ppid"));
assert!(!Funifier::is_metric_passthrough("comm"));
assert!(!Funifier::is_metric_passthrough("cpu"));
assert!(!Funifier::is_metric_passthrough("cgroup"));
assert!(!Funifier::is_metric_passthrough("dest_cpu"));
assert!(!Funifier::is_metric_passthrough("running_pid"));
assert!(!Funifier::is_metric_passthrough("scheduler"));
assert!(!Funifier::is_metric_passthrough("cgroup_path"));
assert!(!Funifier::is_metric_passthrough("path"));
assert!(!Funifier::is_metric_passthrough("hostname"));
assert!(!Funifier::is_metric_passthrough("xyz"));
}
#[test]
fn virtio_blk_counter_names_are_metric_passthrough() {
for name in [
"reads_completed",
"writes_completed",
"flushes_completed",
"bytes_read",
"bytes_written",
"throttled_count",
"io_errors",
] {
assert!(
Funifier::is_metric_passthrough(name),
"{name} must be metric",
);
}
}
#[test]
fn funify_json_funifies_non_metric_keys_and_preserves_metrics() {
let f = Funifier::with_seed("demo");
let input = json!({
"schema": "single",
"version": "1.2.3",
"comm": "ktstr_test",
"pid": 42,
"nr_running": 7,
"scheduler": "scx_simple",
"wakeups_per_sec": 500.0,
"thread_count": 4,
"cpus": [
{ "cpu": 1, "comm": "swapper" },
{ "cpu": 3, "comm": "ktstr_worker" }
]
});
let out = funify_json(input.clone(), &f);
assert_eq!(out["schema"], json!("single"));
assert_eq!(out["version"], json!("1.2.3"));
assert_eq!(out["nr_running"], json!(7));
assert_eq!(out["wakeups_per_sec"], json!(500.0));
assert_eq!(out["thread_count"], json!(4));
assert_ne!(out["comm"], input["comm"]);
assert_ne!(out["pid"], input["pid"]);
assert_ne!(out["scheduler"], input["scheduler"]);
let comm = out["comm"].as_str().unwrap();
assert!(
comm.contains('-'),
"expected adjective-animal token, got {comm:?}",
);
assert_ne!(out["cpus"][0]["comm"], input["cpus"][0]["comm"]);
assert_ne!(out["cpus"][1]["comm"], input["cpus"][1]["comm"]);
assert_ne!(out["cpus"][0]["cpu"], input["cpus"][0]["cpu"]);
assert_ne!(out["cpus"][1]["cpu"], input["cpus"][1]["cpu"]);
let s = serde_json::to_string(&out).expect("serialize");
assert!(!s.is_empty());
}
#[test]
fn funify_json_preserves_numeric_sentinels() {
let f = Funifier::with_seed("demo");
let input = json!({
"cpu": 0,
"pid": u64::MAX,
"tid": 1,
});
let out = funify_json(input.clone(), &f);
assert_eq!(out["cpu"], json!(0));
assert_eq!(out["pid"], json!(u64::MAX));
assert_ne!(out["tid"], json!(1));
}
#[test]
fn funify_json_floats_pass_through_unconditionally() {
let f = Funifier::with_seed("demo");
let input = json!({
"wakeups_per_sec": 500.5,
"fairness_score": 0.75,
"anonymous_float": 4.25,
});
let out = funify_json(input.clone(), &f);
assert_eq!(out["wakeups_per_sec"], json!(500.5));
assert_eq!(out["fairness_score"], json!(0.75));
assert_eq!(out["anonymous_float"], json!(4.25));
}
#[test]
fn funify_json_cross_reference_within_dump() {
let f = Funifier::with_seed("demo");
let input = json!({
"running": [
{ "pid": 100 },
{ "pid": 100 },
{ "pid": 200 }
]
});
let out = funify_json(input, &f);
let p0 = &out["running"][0]["pid"];
let p1 = &out["running"][1]["pid"];
let p2 = &out["running"][2]["pid"];
assert_eq!(p0, p1, "same key + same value must funify identically");
assert_ne!(p0, p2, "same key + different value must differ");
}
#[test]
fn funify_json_array_inherits_parent_category() {
let f = Funifier::with_seed("demo");
let input = json!({
"pids": [1, 2, 3],
"completed_per_sec": [10.0, 20.0, 30.0],
});
let out = funify_json(input.clone(), &f);
for i in 0..3 {
assert_ne!(out["pids"][i], input["pids"][i]);
}
assert_eq!(out["completed_per_sec"], input["completed_per_sec"]);
}
#[test]
fn distinct_seeds_produce_distinct_mappings() {
let a = Funifier::with_seed("seed-a");
let b = Funifier::with_seed("seed-b");
let na = a.petname_for("comm", "x");
let nb = b.petname_for("comm", "x");
let na2 = a.numeric_id("pid", 42);
let nb2 = b.numeric_id("pid", 42);
assert!(
na != nb || na2 != nb2,
"two seeds must differ on at least one mapping"
);
}
#[test]
fn ephemeral_within_instance_stable_across_instances_random() {
let a = Funifier::ephemeral();
let n1 = a.petname_for("comm", "same");
let n2 = a.petname_for("comm", "same");
assert_eq!(n1, n2);
let b = Funifier::ephemeral();
let a_bundle = (
a.petname_for("comm", "same"),
a.numeric_id("pid", 42),
a.numeric_id("cgroup", 7),
);
let b_bundle = (
b.petname_for("comm", "same"),
b.numeric_id("pid", 42),
b.numeric_id("cgroup", 7),
);
assert_ne!(a_bundle, b_bundle, "two ephemeral instances must differ");
}
#[test]
fn dictionary_sizes_pinned() {
assert_eq!(ADJECTIVES.len(), 272, "adjective list must be 272 entries");
assert_eq!(ANIMALS.len(), 264, "animal list must be 264 entries");
}
#[test]
fn dictionary_entries_are_lowercase_ascii_words() {
for w in ADJECTIVES.iter().chain(ANIMALS.iter()) {
assert!(!w.is_empty(), "empty word in dictionary");
assert!(
w.chars().all(|c| c.is_ascii_lowercase()),
"non-lowercase-ASCII word in dictionary: {w:?}",
);
}
}
}