Skip to main content

observer_rust/
lib.rs

1// SPDX-FileCopyrightText: 2026 Alexander R. Croft
2// SPDX-License-Identifier: GPL-3.0-or-later
3
4use std::collections::BTreeSet;
5
6use serde_json::Number;
7use thiserror::Error;
8
9pub type TestFn = fn(&mut TestContext);
10
11const MAX_TELEMETRY_ENTRIES: usize = 64;
12const MAX_TELEMETRY_NAME_LEN: usize = 128;
13const MAX_TELEMETRY_VECTOR_LEN: usize = 256;
14const MAX_TELEMETRY_TAG_LEN: usize = 1024;
15
16#[derive(Debug, Clone, PartialEq)]
17pub struct TestOutcome {
18    pub exit: i32,
19    pub out: Vec<u8>,
20    pub err: Vec<u8>,
21    pub telemetry: Vec<TelemetryEntry>,
22}
23
24#[derive(Debug, Clone, PartialEq)]
25pub struct TelemetryEntry {
26    pub name: String,
27    pub value: TelemetryValue,
28}
29
30#[derive(Debug, Clone, PartialEq)]
31pub enum TelemetryValue {
32    Metric(Number),
33    Vector(Vec<Number>),
34    Tag(String),
35}
36
37#[derive(Debug, Default, Clone, PartialEq)]
38pub struct TestContext {
39    out: Vec<u8>,
40    err: Vec<u8>,
41    exit: i32,
42    telemetry: Vec<TelemetryEntry>,
43}
44
45impl TestContext {
46    pub fn new() -> Self {
47        Self::default()
48    }
49
50    pub fn write_out(&mut self, bytes: &[u8]) {
51        self.out.extend_from_slice(bytes);
52    }
53
54    pub fn write_err(&mut self, bytes: &[u8]) {
55        self.err.extend_from_slice(bytes);
56    }
57
58    pub fn set_exit(&mut self, exit: i32) {
59        self.exit = exit;
60    }
61
62    pub fn emit_metric(&mut self, name: &str, value: f64) -> bool {
63        let Some(value) = finite_number(value) else {
64            return false;
65        };
66        self.push_telemetry(name, TelemetryValue::Metric(value))
67    }
68
69    pub fn emit_vector(&mut self, name: &str, values: &[f64]) -> bool {
70        if values.len() > MAX_TELEMETRY_VECTOR_LEN {
71            return false;
72        }
73
74        let mut numbers = Vec::with_capacity(values.len());
75        for value in values {
76            let Some(number) = finite_number(*value) else {
77                return false;
78            };
79            numbers.push(number);
80        }
81
82        self.push_telemetry(name, TelemetryValue::Vector(numbers))
83    }
84
85    pub fn emit_tag(&mut self, name: &str, value: &str) -> bool {
86        if value.len() > MAX_TELEMETRY_TAG_LEN {
87            return false;
88        }
89        self.push_telemetry(name, TelemetryValue::Tag(value.to_owned()))
90    }
91
92    pub fn fail_now(&mut self, msg: &str) {
93        if self.exit == 0 {
94            self.exit = 1;
95        }
96        self.write_err(msg.as_bytes());
97        if !msg.ends_with('\n') {
98            self.write_err(b"\n");
99        }
100    }
101
102    pub fn finish(self) -> TestOutcome {
103        TestOutcome {
104            exit: self.exit,
105            out: self.out,
106            err: self.err,
107            telemetry: self.telemetry,
108        }
109    }
110
111    fn push_telemetry(&mut self, name: &str, value: TelemetryValue) -> bool {
112        let name = name.trim();
113        if name.is_empty() || name.len() > MAX_TELEMETRY_NAME_LEN {
114            return false;
115        }
116        if self.telemetry.len() >= MAX_TELEMETRY_ENTRIES {
117            return false;
118        }
119        self.telemetry.push(TelemetryEntry {
120            name: name.to_owned(),
121            value,
122        });
123        true
124    }
125}
126
127fn finite_number(value: f64) -> Option<Number> {
128    if !value.is_finite() {
129        return None;
130    }
131    if value.fract() == 0.0 && value >= i64::MIN as f64 && value <= i64::MAX as f64 {
132        return Some(Number::from(value as i64));
133    }
134    Number::from_f64(value)
135}
136
137#[derive(Debug, Clone, Copy)]
138pub struct TestRegistration {
139    pub canonical_name: &'static str,
140    pub target: &'static str,
141    pub function: TestFn,
142    pub file: &'static str,
143    pub line: u32,
144    pub module_path: &'static str,
145}
146
147pub trait Registry {
148    fn tests() -> &'static [TestRegistration];
149}
150
151#[derive(Debug, Error, PartialEq, Eq)]
152pub enum RegistryError {
153    #[error("registered test canonical name must not be empty")]
154    EmptyCanonicalName,
155    #[error("registered test target must not be empty")]
156    EmptyTarget,
157    #[error("duplicate canonical test name `{0}`")]
158    DuplicateCanonicalName(String),
159    #[error("duplicate test target `{0}`")]
160    DuplicateTarget(String),
161}
162
163pub fn sorted_validated_tests<R: Registry>() -> Result<Vec<&'static TestRegistration>, RegistryError> {
164    let tests = R::tests();
165    let mut names = BTreeSet::new();
166    let mut targets = BTreeSet::new();
167
168    for test in tests {
169        if test.canonical_name.trim().is_empty() {
170            return Err(RegistryError::EmptyCanonicalName);
171        }
172        if test.target.trim().is_empty() {
173            return Err(RegistryError::EmptyTarget);
174        }
175        if !names.insert(test.canonical_name) {
176            return Err(RegistryError::DuplicateCanonicalName(
177                test.canonical_name.to_owned(),
178            ));
179        }
180        if !targets.insert(test.target) {
181            return Err(RegistryError::DuplicateTarget(test.target.to_owned()));
182        }
183    }
184
185    let mut sorted = tests.iter().collect::<Vec<_>>();
186    sorted.sort_by(|left, right| {
187        left.canonical_name
188            .as_bytes()
189            .cmp(right.canonical_name.as_bytes())
190            .then_with(|| left.target.as_bytes().cmp(right.target.as_bytes()))
191    });
192    Ok(sorted)
193}
194
195#[cfg(test)]
196mod tests {
197    use super::*;
198
199    fn noop(_ctx: &mut TestContext) {}
200
201    struct SortedRegistry;
202
203    impl Registry for SortedRegistry {
204        fn tests() -> &'static [TestRegistration] {
205            static TESTS: [TestRegistration; 2] = [
206                TestRegistration {
207                    canonical_name: "B::Second",
208                    target: "b::second",
209                    function: noop,
210                    file: file!(),
211                    line: line!(),
212                    module_path: module_path!(),
213                },
214                TestRegistration {
215                    canonical_name: "A::First",
216                    target: "a::first",
217                    function: noop,
218                    file: file!(),
219                    line: line!(),
220                    module_path: module_path!(),
221                },
222            ];
223            &TESTS
224        }
225    }
226
227    struct DuplicateNameRegistry;
228
229    impl Registry for DuplicateNameRegistry {
230        fn tests() -> &'static [TestRegistration] {
231            static TESTS: [TestRegistration; 2] = [
232                TestRegistration {
233                    canonical_name: "A::First",
234                    target: "a::first",
235                    function: noop,
236                    file: file!(),
237                    line: line!(),
238                    module_path: module_path!(),
239                },
240                TestRegistration {
241                    canonical_name: "A::First",
242                    target: "a::second",
243                    function: noop,
244                    file: file!(),
245                    line: line!(),
246                    module_path: module_path!(),
247                },
248            ];
249            &TESTS
250        }
251    }
252
253    #[test]
254    fn fail_now_sets_nonzero_exit_and_stderr() {
255        let mut ctx = TestContext::new();
256        ctx.fail_now("expected failure");
257
258        let outcome = ctx.finish();
259        assert_eq!(outcome.exit, 1);
260        assert_eq!(outcome.err, b"expected failure\n");
261    }
262
263    #[test]
264    fn emit_telemetry_records_bounded_typed_entries() {
265        let mut ctx = TestContext::new();
266
267        assert!(ctx.emit_metric("wall_time_ns", 12.0));
268        assert!(ctx.emit_vector("latency_ns", &[1.0, 2.5, 3.0]));
269        assert!(ctx.emit_tag("resource_path", "fixtures/config.json"));
270        assert!(!ctx.emit_metric("", 1.0));
271        assert!(!ctx.emit_metric("bad", f64::NAN));
272
273        let outcome = ctx.finish();
274        assert_eq!(outcome.telemetry.len(), 3);
275        assert_eq!(outcome.telemetry[0].name, "wall_time_ns");
276        assert_eq!(outcome.telemetry[2].name, "resource_path");
277    }
278
279    #[test]
280    fn sorted_validated_tests_orders_by_canonical_name() {
281        let tests = sorted_validated_tests::<SortedRegistry>().expect("registry should validate");
282        assert_eq!(tests[0].canonical_name, "A::First");
283        assert_eq!(tests[1].canonical_name, "B::Second");
284    }
285
286    #[test]
287    fn sorted_validated_tests_rejects_duplicate_names() {
288        let error = sorted_validated_tests::<DuplicateNameRegistry>()
289            .expect_err("duplicate names should fail");
290        assert_eq!(
291            error,
292            RegistryError::DuplicateCanonicalName("A::First".to_owned())
293        );
294    }
295}