Skip to main content

observer_rust_lib/
lib.rs

1// SPDX-License-Identifier: MIT
2
3use anyhow::{anyhow, Result};
4use base64::engine::general_purpose::STANDARD;
5use base64::Engine;
6use observer_rust::{TelemetryEntry as RustTelemetryEntry, TelemetryValue as RustTelemetryValue};
7use regex::Regex;
8use serde::Serialize;
9use serde_json::Number;
10use std::cell::RefCell;
11use std::collections::{BTreeSet, HashMap};
12use std::fmt::Debug;
13use std::panic::{catch_unwind, panic_any, resume_unwind, AssertUnwindSafe};
14
15pub const TEST_EXIT_PASS: i32 = 0;
16
17pub type TestOutcome = observer_rust::TestOutcome;
18pub type TestFunction = fn(&mut TestContext);
19
20#[derive(Debug, Clone)]
21pub struct TestRegistration {
22    title: String,
23    canonical_name: String,
24    target: String,
25    function: TestFunction,
26    suite_path: Vec<String>,
27}
28
29impl TestRegistration {
30    pub fn title(&self) -> &str {
31        &self.title
32    }
33
34    pub fn canonical_name(&self) -> &str {
35        &self.canonical_name
36    }
37
38    pub fn target(&self) -> &str {
39        &self.target
40    }
41
42    pub fn suite_path(&self) -> &[String] {
43        &self.suite_path
44    }
45}
46
47#[derive(Debug, Clone, Default)]
48pub struct TestOptions<'a> {
49    pub id: Option<&'a str>,
50}
51
52#[derive(Debug, Clone, PartialEq, Eq)]
53pub enum RegistryError {
54    EmptyCanonicalName,
55    EmptyTarget,
56    DuplicateCanonicalName(String),
57    DuplicateTarget(String),
58}
59
60impl std::fmt::Display for RegistryError {
61    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62        match self {
63            Self::EmptyCanonicalName => {
64                write!(f, "registered test canonical name must not be empty")
65            }
66            Self::EmptyTarget => write!(f, "registered test target must not be empty"),
67            Self::DuplicateCanonicalName(value) => {
68                write!(f, "duplicate canonical test name `{value}`")
69            }
70            Self::DuplicateTarget(value) => write!(f, "duplicate test target `{value}`"),
71        }
72    }
73}
74
75impl std::error::Error for RegistryError {}
76
77#[derive(Debug)]
78pub struct ExpectationFailure {
79    message: String,
80}
81
82impl ExpectationFailure {
83    pub fn message(&self) -> &str {
84        &self.message
85    }
86}
87
88pub struct TestContext {
89    inner: observer_rust::TestContext,
90}
91
92impl Default for TestContext {
93    fn default() -> Self {
94        Self::new()
95    }
96}
97
98impl TestContext {
99    pub fn new() -> Self {
100        Self {
101            inner: observer_rust::TestContext::new(),
102        }
103    }
104
105    pub fn stdout<S>(&mut self, value: S)
106    where
107        S: AsRef<[u8]>,
108    {
109        self.inner.write_out(value.as_ref());
110    }
111
112    pub fn stderr<S>(&mut self, value: S)
113    where
114        S: AsRef<[u8]>,
115    {
116        self.inner.write_err(value.as_ref());
117    }
118
119    pub fn set_exit(&mut self, exit: i32) {
120        self.inner.set_exit(exit);
121    }
122
123    pub fn fail(&mut self, message: &str) {
124        self.inner.fail_now(message);
125    }
126
127    pub fn observe(&mut self) -> Observe<'_> {
128        Observe {
129            inner: &mut self.inner,
130        }
131    }
132
133    pub fn as_raw_mut(&mut self) -> &mut observer_rust::TestContext {
134        &mut self.inner
135    }
136
137    pub fn finish(self) -> TestOutcome {
138        self.inner.finish()
139    }
140}
141
142pub struct Observe<'a> {
143    inner: &'a mut observer_rust::TestContext,
144}
145
146impl Observe<'_> {
147    pub fn metric(&mut self, name: &str, value: f64) -> bool {
148        self.inner.emit_metric(name, value)
149    }
150
151    pub fn vector(&mut self, name: &str, values: &[f64]) -> bool {
152        self.inner.emit_vector(name, values)
153    }
154
155    pub fn tag(&mut self, name: &str, value: &str) -> bool {
156        self.inner.emit_tag(name, value)
157    }
158}
159
160pub trait Truthy {
161    fn is_truthy(&self) -> bool;
162}
163
164impl Truthy for bool {
165    fn is_truthy(&self) -> bool {
166        *self
167    }
168}
169
170impl<T> Truthy for Option<T> {
171    fn is_truthy(&self) -> bool {
172        self.is_some()
173    }
174}
175
176impl<T, E> Truthy for Result<T, E> {
177    fn is_truthy(&self) -> bool {
178        self.is_ok()
179    }
180}
181
182pub trait StrPattern {
183    fn matches_input(&self, input: &str) -> bool;
184    fn describe_pattern(&self) -> String;
185}
186
187impl StrPattern for &str {
188    fn matches_input(&self, input: &str) -> bool {
189        input.contains(self)
190    }
191
192    fn describe_pattern(&self) -> String {
193        (*self).to_owned()
194    }
195}
196
197impl StrPattern for String {
198    fn matches_input(&self, input: &str) -> bool {
199        input.contains(self)
200    }
201
202    fn describe_pattern(&self) -> String {
203        self.clone()
204    }
205}
206
207impl StrPattern for Regex {
208    fn matches_input(&self, input: &str) -> bool {
209        self.is_match(input)
210    }
211
212    fn describe_pattern(&self) -> String {
213        self.as_str().to_owned()
214    }
215}
216
217pub struct Expectation<T> {
218    actual: T,
219    inverted: bool,
220}
221
222pub fn expect<T>(actual: T) -> Expectation<T> {
223    Expectation {
224        actual,
225        inverted: false,
226    }
227}
228
229impl<T> Expectation<T> {
230    pub fn not(mut self) -> Self {
231        self.inverted = !self.inverted;
232        self
233    }
234
235    fn apply(&self, condition: bool, message: String) {
236        if self.inverted {
237            if condition {
238                panic_any(ExpectationFailure {
239                    message: format!("not {message}"),
240                });
241            }
242            return;
243        }
244
245        if !condition {
246            panic_any(ExpectationFailure { message });
247        }
248    }
249}
250
251impl<T> Expectation<T>
252where
253    T: PartialEq + Debug,
254{
255    pub fn to_be(self, expected: T) {
256        let message = format!("expected {:?} to be {:?}", &self.actual, &expected);
257        self.apply(self.actual == expected, message);
258    }
259
260    pub fn to_equal(self, expected: T) {
261        let message = format!("expected {:?} to equal {:?}", &self.actual, &expected);
262        self.apply(self.actual == expected, message);
263    }
264}
265
266impl<T> Expectation<T>
267where
268    T: Truthy + Debug,
269{
270    pub fn to_be_truthy(self) {
271        let message = format!("expected {:?} to be truthy", &self.actual);
272        self.apply(self.actual.is_truthy(), message);
273    }
274
275    pub fn to_be_falsy(self) {
276        let message = format!("expected {:?} to be falsy", &self.actual);
277        self.apply(!self.actual.is_truthy(), message);
278    }
279}
280
281impl<T> Expectation<T>
282where
283    T: AsRef<str> + Debug,
284{
285    pub fn to_contain<S>(self, expected: S)
286    where
287        S: AsRef<str> + Debug,
288    {
289        let actual = self.actual.as_ref();
290        let expected_ref = expected.as_ref();
291        let message = format!("expected {:?} to contain {:?}", actual, expected_ref);
292        self.apply(actual.contains(expected_ref), message);
293    }
294
295    pub fn to_match<P>(self, pattern: P)
296    where
297        P: StrPattern,
298    {
299        let actual = self.actual.as_ref();
300        let description = pattern.describe_pattern();
301        let message = format!("expected {:?} to match {:?}", actual, description);
302        self.apply(pattern.matches_input(actual), message);
303    }
304}
305
306#[derive(Default)]
307struct CollectionState {
308    tests: Vec<CollectedTest>,
309    suite_path: Vec<String>,
310}
311
312#[derive(Clone)]
313struct CollectedTest {
314    title: String,
315    id: Option<String>,
316    function: TestFunction,
317    suite_path: Vec<String>,
318}
319
320thread_local! {
321    static ACTIVE_COLLECTION: RefCell<Option<CollectionState>> = RefCell::new(None);
322}
323
324fn with_collection_state<T>(f: impl FnOnce(&mut CollectionState) -> T) -> T {
325    ACTIVE_COLLECTION.with(|cell| {
326        let mut borrowed = cell.borrow_mut();
327        let state = borrowed
328            .as_mut()
329            .expect("test collection is only available inside collect_tests(...)");
330        f(state)
331    })
332}
333
334fn derive_base_id(suite_path: &[String], title: &str) -> String {
335    if suite_path.is_empty() {
336        title.to_owned()
337    } else {
338        format!("{} :: {title}", suite_path.join(" :: "))
339    }
340}
341
342fn materialize_registrations(collected: Vec<CollectedTest>) -> Vec<TestRegistration> {
343    let mut counts = HashMap::<String, usize>::new();
344
345    collected
346        .into_iter()
347        .map(|test| {
348            let base_id = test
349                .id
350                .clone()
351                .unwrap_or_else(|| derive_base_id(&test.suite_path, &test.title));
352            let next_count = counts.get(&base_id).copied().unwrap_or(0) + 1;
353            counts.insert(base_id.clone(), next_count);
354            let resolved_id = if test.id.is_some() || next_count == 1 {
355                base_id
356            } else {
357                format!("{base_id} #{next_count}")
358            };
359
360            TestRegistration {
361                title: test.title,
362                canonical_name: resolved_id.clone(),
363                target: resolved_id,
364                function: test.function,
365                suite_path: test.suite_path,
366            }
367        })
368        .collect()
369}
370
371pub fn define_test(
372    title: &str,
373    function: TestFunction,
374    options: TestOptions<'_>,
375) -> Result<TestRegistration, RegistryError> {
376    let tests = materialize_registrations(vec![CollectedTest {
377        title: title.to_owned(),
378        id: options.id.map(str::to_owned),
379        function,
380        suite_path: Vec::new(),
381    }]);
382    Ok(sorted_validated_tests(&tests)?.remove(0))
383}
384
385pub fn collect_tests(define: impl FnOnce()) -> Result<Vec<TestRegistration>, RegistryError> {
386    let previous = ACTIVE_COLLECTION.with(|cell| cell.replace(Some(CollectionState::default())));
387    let define_result = catch_unwind(AssertUnwindSafe(define));
388    let state = ACTIVE_COLLECTION
389        .with(|cell| cell.replace(previous))
390        .expect("collection state should exist");
391
392    match define_result {
393        Ok(()) => sorted_validated_tests(&materialize_registrations(state.tests)),
394        Err(payload) => resume_unwind(payload),
395    }
396}
397
398pub fn describe(label: &str, define: impl FnOnce()) {
399    with_collection_state(|state| state.suite_path.push(label.to_owned()));
400    let define_result = catch_unwind(AssertUnwindSafe(define));
401    with_collection_state(|state| {
402        state.suite_path.pop();
403    });
404
405    if let Err(payload) = define_result {
406        resume_unwind(payload);
407    }
408}
409
410pub fn register_test(title: &str, id: Option<&str>, function: TestFunction) {
411    with_collection_state(|state| {
412        state.tests.push(CollectedTest {
413            title: title.to_owned(),
414            id: id.map(str::to_owned),
415            function,
416            suite_path: state.suite_path.clone(),
417        });
418    });
419}
420
421pub fn test(title: &str, function: TestFunction) {
422    register_test(title, None, function);
423}
424
425pub fn it(title: &str, function: TestFunction) {
426    register_test(title, None, function);
427}
428
429pub fn sorted_validated_tests(tests: &[TestRegistration]) -> Result<Vec<TestRegistration>, RegistryError> {
430    validate_registry(tests)?;
431    let mut sorted = tests.to_vec();
432    sorted.sort_by(|left, right| {
433        left.canonical_name
434            .as_bytes()
435            .cmp(right.canonical_name.as_bytes())
436            .then_with(|| left.target.as_bytes().cmp(right.target.as_bytes()))
437    });
438    Ok(sorted)
439}
440
441pub fn validate_registry(tests: &[TestRegistration]) -> Result<(), RegistryError> {
442    let mut names = BTreeSet::new();
443    let mut targets = BTreeSet::new();
444
445    for test in tests {
446        if test.canonical_name.trim().is_empty() {
447            return Err(RegistryError::EmptyCanonicalName);
448        }
449        if test.target.trim().is_empty() {
450            return Err(RegistryError::EmptyTarget);
451        }
452        if !names.insert(test.canonical_name.as_str()) {
453            return Err(RegistryError::DuplicateCanonicalName(
454                test.canonical_name.clone(),
455            ));
456        }
457        if !targets.insert(test.target.as_str()) {
458            return Err(RegistryError::DuplicateTarget(test.target.clone()));
459        }
460    }
461
462    Ok(())
463}
464
465pub fn find_target<'a>(tests: &'a [TestRegistration], target: &str) -> Option<&'a TestRegistration> {
466    tests.iter().find(|test| test.target == target)
467}
468
469pub fn run_test(test: &TestRegistration) -> TestOutcome {
470    let mut ctx = TestContext::new();
471    let result = catch_unwind_silent(AssertUnwindSafe(|| {
472        (test.function)(&mut ctx);
473    }));
474
475    match result {
476        Ok(()) => ctx.finish(),
477        Err(payload) => {
478            let message = panic_payload_message(&payload);
479            ctx.fail(&message);
480            ctx.finish()
481        }
482    }
483}
484
485fn catch_unwind_silent<F, R>(f: F) -> std::thread::Result<R>
486where
487    F: FnOnce() -> R + std::panic::UnwindSafe,
488{
489    let hook = std::panic::take_hook();
490    std::panic::set_hook(Box::new(|_| {}));
491    let result = catch_unwind(f);
492    std::panic::set_hook(hook);
493    result
494}
495
496#[derive(Debug, Clone, PartialEq, Eq)]
497pub struct HostRunArgs {
498    pub target: String,
499    pub timeout_ms: u32,
500}
501
502#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
503pub struct ListPayload {
504    pub provider: String,
505    pub tests: Vec<ListEntry>,
506}
507
508#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
509pub struct ListEntry {
510    pub name: String,
511    pub target: String,
512}
513
514#[derive(Debug, Clone, PartialEq, Serialize)]
515pub struct RunPayload {
516    pub provider: String,
517    pub target: String,
518    pub exit: i32,
519    pub out_b64: String,
520    pub err_b64: String,
521    #[serde(skip_serializing_if = "Option::is_none")]
522    pub telemetry: Option<Vec<TelemetryPayloadEntry>>,
523}
524
525#[derive(Debug, Clone, PartialEq, Serialize)]
526pub struct TelemetryPayloadEntry {
527    pub name: String,
528    #[serde(skip_serializing_if = "Option::is_none")]
529    pub unit: Option<String>,
530    #[serde(flatten)]
531    pub value: TelemetryPayloadValue,
532}
533
534#[derive(Debug, Clone, PartialEq, Serialize)]
535#[serde(tag = "kind", rename_all = "snake_case")]
536pub enum TelemetryPayloadValue {
537    Metric { value: Number },
538    Vector { values: Vec<Number> },
539    Tag { value: String },
540}
541
542pub fn list_payload(provider: &str, tests: &[TestRegistration]) -> Result<ListPayload> {
543    validate_provider(provider)?;
544    let sorted = sorted_validated_tests(tests).map_err(|error| anyhow!(error))?;
545    Ok(ListPayload {
546        provider: provider.to_owned(),
547        tests: sorted
548            .into_iter()
549            .map(|test| ListEntry {
550                name: test.canonical_name,
551                target: test.target,
552            })
553            .collect(),
554    })
555}
556
557pub fn run_payload(
558    provider: &str,
559    tests: &[TestRegistration],
560    target: &str,
561    timeout_ms: u32,
562) -> Result<RunPayload> {
563    let _ = timeout_ms;
564    validate_provider(provider)?;
565    let sorted = sorted_validated_tests(tests).map_err(|error| anyhow!(error))?;
566    let test = find_target(&sorted, target).ok_or_else(|| anyhow!("unknown test target `{target}`"))?;
567    let outcome = run_test(test);
568    Ok(RunPayload {
569        provider: provider.to_owned(),
570        target: target.to_owned(),
571        exit: outcome.exit,
572        out_b64: STANDARD.encode(&outcome.out),
573        err_b64: STANDARD.encode(&outcome.err),
574        telemetry: telemetry_payload_from_outcome(outcome),
575    })
576}
577
578pub fn observer_host_handles_command(command: Option<&str>) -> bool {
579    matches!(command, Some("list" | "run" | "observe"))
580}
581
582pub fn parse_run_args(args: &[String]) -> Result<HostRunArgs> {
583    let mut target = None;
584    let mut timeout_ms = 0;
585    let mut index = 0;
586
587    while index < args.len() {
588        match args[index].as_str() {
589            "--target" => {
590                let value = args
591                    .get(index + 1)
592                    .ok_or_else(|| anyhow!("missing --target value"))?;
593                target = Some(value.clone());
594                index += 2;
595            }
596            "--timeout-ms" => {
597                let value = args
598                    .get(index + 1)
599                    .ok_or_else(|| anyhow!("missing --timeout-ms value"))?;
600                timeout_ms = value
601                    .parse::<u32>()
602                    .map_err(|_| anyhow!("invalid --timeout-ms value"))?;
603                index += 2;
604            }
605            other => return Err(anyhow!("unknown argument {other}")),
606        }
607    }
608
609    Ok(HostRunArgs {
610        target: target.ok_or_else(|| anyhow!("missing --target"))?,
611        timeout_ms,
612    })
613}
614
615pub fn observer_host_dispatch(provider: &str, tests: &[TestRegistration], argv: &[String]) -> Result<()> {
616    validate_provider(provider)?;
617
618    let Some(command) = argv.first().map(String::as_str) else {
619        return Err(anyhow!("expected subcommand list|run|observe"));
620    };
621
622    match command {
623        "list" => write_json(&list_payload(provider, tests)?),
624        "run" | "observe" => {
625            let args = parse_run_args(&argv[1..])?;
626            write_json(&run_payload(provider, tests, &args.target, args.timeout_ms)?)
627        }
628        other => Err(anyhow!("unknown subcommand `{other}`")),
629    }
630}
631
632pub fn observer_host_main(provider: &str, tests: &[TestRegistration]) -> Result<()> {
633    observer_host_main_from(provider, tests, std::env::args())
634}
635
636pub fn observer_host_main_from<I, S>(
637    provider: &str,
638    tests: &[TestRegistration],
639    argv: I,
640) -> Result<()>
641where
642    I: IntoIterator<Item = S>,
643    S: Into<String>,
644{
645    let args = argv.into_iter().map(Into::into).collect::<Vec<_>>();
646    observer_host_dispatch(provider, tests, args.get(1..).unwrap_or(&[]))
647}
648
649pub fn observer_host_dispatch_embedded<I, S>(
650    provider: &str,
651    root_command: &str,
652    tests: &[TestRegistration],
653    argv: I,
654) -> Result<()>
655where
656    I: IntoIterator<Item = S>,
657    S: Into<String>,
658{
659    if root_command.trim().is_empty() {
660        return Err(anyhow!("root command must not be empty"));
661    }
662
663    let args = argv.into_iter().map(Into::into).collect::<Vec<_>>();
664    if args.len() < 2 || args[1] != root_command {
665        return Err(anyhow!("expected root command `{root_command}`"));
666    }
667
668    let rest = args.get(2..).unwrap_or(&[]).to_vec();
669    if rest.is_empty() {
670        return Err(anyhow!(
671            "expected `{root_command} list` or `{root_command} --target <target> --timeout-ms <u32>`"
672        ));
673    }
674
675    if rest[0] == "list" {
676        return observer_host_dispatch(provider, tests, &[String::from("list")]);
677    }
678
679    if rest[0] == "run" || rest[0] == "observe" {
680        return observer_host_dispatch(provider, tests, &rest);
681    }
682
683    let mut routed = vec![String::from("observe")];
684    routed.extend(rest);
685    observer_host_dispatch(provider, tests, &routed)
686}
687
688fn validate_provider(provider: &str) -> Result<()> {
689    if provider.trim().is_empty() {
690        return Err(anyhow!("provider id must not be empty"));
691    }
692    Ok(())
693}
694
695fn write_json<T>(value: &T) -> Result<()>
696where
697    T: Serialize,
698{
699    println!("{}", serde_json::to_string(value)?);
700    Ok(())
701}
702
703fn telemetry_payload_from_outcome(outcome: TestOutcome) -> Option<Vec<TelemetryPayloadEntry>> {
704    let mut telemetry = Vec::new();
705    for entry in outcome.telemetry {
706        telemetry.push(convert_telemetry(entry));
707    }
708    if telemetry.is_empty() {
709        None
710    } else {
711        Some(telemetry)
712    }
713}
714
715fn convert_telemetry(entry: RustTelemetryEntry) -> TelemetryPayloadEntry {
716    let value = match entry.value {
717        RustTelemetryValue::Metric(value) => TelemetryPayloadValue::Metric { value },
718        RustTelemetryValue::Vector(values) => TelemetryPayloadValue::Vector { values },
719        RustTelemetryValue::Tag(value) => TelemetryPayloadValue::Tag { value },
720    };
721
722    TelemetryPayloadEntry {
723        name: entry.name,
724        unit: None,
725        value,
726    }
727}
728
729fn panic_payload_message(payload: &Box<dyn std::any::Any + Send>) -> String {
730    if let Some(failure) = payload.downcast_ref::<ExpectationFailure>() {
731        return failure.message().to_owned();
732    }
733    if let Some(message) = payload.downcast_ref::<&str>() {
734        return format!("panic: {message}");
735    }
736    if let Some(message) = payload.downcast_ref::<String>() {
737        return format!("panic: {message}");
738    }
739    String::from("panic: non-string payload")
740}
741
742#[macro_export]
743macro_rules! describe {
744    ($label:expr, $body:block) => {{
745        $crate::describe($label, || $body)
746    }};
747}
748
749#[macro_export]
750macro_rules! test {
751    ($title:expr, $function:expr $(,)?) => {{
752        $crate::register_test($title, None, $function)
753    }};
754    ($title:expr, id = $id:expr, $function:expr $(,)?) => {{
755        $crate::register_test($title, Some($id), $function)
756    }};
757}
758
759#[macro_export]
760macro_rules! it {
761    ($title:expr, $function:expr $(,)?) => {{
762        $crate::register_test($title, None, $function)
763    }};
764    ($title:expr, id = $id:expr, $function:expr $(,)?) => {{
765        $crate::register_test($title, Some($id), $function)
766    }};
767}
768
769#[cfg(test)]
770mod tests {
771    use super::*;
772
773    #[test]
774    fn collect_tests_derives_and_sorts_names() {
775        let tests = collect_tests(|| {
776            describe("pkg", || {
777                test!("beta test", |ctx| {
778                    ctx.stdout("beta\n");
779                });
780                test!("alpha test", id = "pkg/alpha", |ctx| {
781                    ctx.stdout("alpha\n");
782                });
783            });
784        })
785        .expect("collection should validate");
786
787        assert_eq!(tests.len(), 2);
788        assert_eq!(tests[0].canonical_name(), "pkg :: beta test");
789        assert_eq!(tests[1].canonical_name(), "pkg/alpha");
790    }
791
792    #[test]
793    fn duplicate_derived_titles_gain_occurrence_suffixes() {
794        let tests = collect_tests(|| {
795            describe("pkg", || {
796                test!("same", |_ctx| {});
797                test!("same", |_ctx| {});
798            });
799        })
800        .expect("collection should validate");
801
802        assert_eq!(tests[0].canonical_name(), "pkg :: same");
803        assert_eq!(tests[1].canonical_name(), "pkg :: same #2");
804    }
805
806    #[test]
807    fn explicit_duplicate_ids_are_rejected() {
808        let error = collect_tests(|| {
809            describe("pkg", || {
810                test!("one", id = "pkg/dup", |_ctx| {});
811                test!("two", id = "pkg/dup", |_ctx| {});
812            });
813        })
814        .expect_err("duplicate ids should fail");
815
816        assert_eq!(error, RegistryError::DuplicateCanonicalName(String::from("pkg/dup")));
817    }
818
819    #[test]
820    fn run_test_converts_expectation_failure_into_stderr() {
821        let test = define_test(
822            "smoke",
823            |_ctx| {
824                expect(false).to_be_truthy();
825            },
826            TestOptions::default(),
827        )
828        .expect("define_test should validate");
829
830        let outcome = run_test(&test);
831        assert_eq!(outcome.exit, 1);
832        assert_eq!(String::from_utf8(outcome.err).expect("stderr should be utf8"), "expected false to be truthy\n");
833    }
834
835    #[test]
836    fn list_payload_is_sorted() {
837        let tests = vec![
838            define_test("b", |_ctx| {}, TestOptions { id: Some("pkg/b") }).expect("valid"),
839            define_test("a", |_ctx| {}, TestOptions { id: Some("pkg/a") }).expect("valid"),
840        ];
841
842        let payload = list_payload("rust", &tests).expect("list payload should succeed");
843        assert_eq!(payload.tests[0].name, "pkg/a");
844        assert_eq!(payload.tests[1].name, "pkg/b");
845    }
846
847    #[test]
848    fn run_payload_encodes_output_and_telemetry() {
849        let test = define_test(
850            "smoke",
851            |ctx| {
852                ctx.stdout("ok\n");
853                assert!(ctx.observe().metric("wall_time_ns", 42.0));
854            },
855            TestOptions { id: Some("pkg/smoke") },
856        )
857        .expect("valid test");
858
859        let payload = run_payload("rust", &[test], "pkg/smoke", 1000).expect("run payload should succeed");
860        assert_eq!(payload.exit, 0);
861        assert_eq!(payload.out_b64, "b2sK");
862        assert!(payload.telemetry.is_some());
863    }
864}