use std::alloc::{GlobalAlloc, Layout, System};
use std::sync::atomic::{AtomicUsize, Ordering};
use filt_rs::{Filter, FilterValue, Filterable};
static ALLOCATIONS: AtomicUsize = AtomicUsize::new(0);
struct CountingAllocator;
unsafe impl GlobalAlloc for CountingAllocator {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
ALLOCATIONS.fetch_add(1, Ordering::Relaxed);
unsafe { System.alloc(layout) }
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
unsafe { System.dealloc(ptr, layout) }
}
unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
ALLOCATIONS.fetch_add(1, Ordering::Relaxed);
unsafe { System.realloc(ptr, layout, new_size) }
}
}
#[global_allocator]
static ALLOCATOR: CountingAllocator = CountingAllocator;
fn allocations_during(f: impl FnOnce()) -> usize {
let before = ALLOCATIONS.load(Ordering::Relaxed);
f();
ALLOCATIONS.load(Ordering::Relaxed) - before
}
struct Server {
hostname: &'static str,
region: &'static str,
port: u16,
healthy: bool,
tags: [&'static str; 3],
}
impl Filterable for Server {
fn get(&self, key: &str) -> FilterValue<'_> {
match key {
"hostname" => self.hostname.into(),
"region" => self.region.into(),
"tags" => self
.tags
.iter()
.map(|&t| t.into())
.collect::<Vec<FilterValue<'_>>>()
.into(),
"port" => self.port.into(),
"healthy" => self.healthy.into(),
_ => FilterValue::Null,
}
}
}
#[test]
fn evaluation_allocation_counts() {
let server = Server {
hostname: "web-01.example.com",
region: "eu-west-1",
port: 443,
healthy: true,
tags: ["web", "public", "production"],
};
let cases: &[(&str, usize, &str)] = &[
("true", 0, "a literal-only filter"),
(
r#""Hello World" contains "WORLD""#,
0,
"case-insensitive operators on string literals",
),
(
r#""choo" in ["one", "two", "choo"] && "Pre-Release" startswith "pre" && "v1.2.3" endswith ".3""#,
0,
"tuple and string literals with every string operator",
),
(
"port == 443 && healthy && !(port < 100)",
0,
"numeric and boolean properties (their FilterValues are stack-only)",
),
(
"missing == null",
0,
"an unknown property resolving to null",
),
(
r#"hostname startswith "WEB" && hostname contains "example" && !(hostname endswith ".org")"#,
0,
"three string-property resolutions (each borrows from the server, so none allocate)",
),
(
r#"region in ["eu-west-1", "eu-west-2"]"#,
0,
"a string property against a tuple literal (the property borrows, so it doesn't allocate)",
),
(
r#"tags contains "production""#,
1,
"a tuple property (only the backing Vec allocates; its three strings are borrowed)",
),
];
for &(expression, expected, description) in cases {
let filter = Filter::new(expression).expect("the filter should parse");
filter.matches(&server).expect("the filter should evaluate");
let allocations = allocations_during(|| {
assert!(
filter.matches(&server).expect("the filter should evaluate"),
"the '{expression}' filter should match the fixture"
);
});
assert_eq!(
allocations, expected,
"expected {expected} allocation(s) evaluating {description} ('{expression}'), observed {allocations}"
);
}
}