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