1use 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}