1use crate::logging::{ConformanceTestLogger, TestEvent, with_test_logger};
8use crate::{ConformanceTest, RuntimeInterface, TestCategory, TestResult};
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11use std::time::{Duration, Instant};
12
13#[derive(Debug, Clone)]
15pub struct RunConfig {
16 pub categories: Vec<TestCategory>,
18 pub tags: Vec<String>,
20 pub test_ids: Vec<String>,
22 pub timeout: Duration,
24 pub fail_fast: bool,
26}
27
28impl Default for RunConfig {
29 fn default() -> Self {
30 Self {
31 categories: Vec::new(),
32 tags: Vec::new(),
33 test_ids: Vec::new(),
34 timeout: Duration::from_secs(30),
35 fail_fast: false,
36 }
37 }
38}
39
40impl RunConfig {
41 pub fn new() -> Self {
43 Self::default()
44 }
45
46 pub fn with_categories(mut self, categories: Vec<TestCategory>) -> Self {
48 self.categories = categories;
49 self
50 }
51
52 pub fn with_tags(mut self, tags: Vec<String>) -> Self {
54 self.tags = tags;
55 self
56 }
57
58 pub fn with_test_ids(mut self, test_ids: Vec<String>) -> Self {
60 self.test_ids = test_ids;
61 self
62 }
63
64 pub fn with_timeout(mut self, timeout: Duration) -> Self {
66 self.timeout = timeout;
67 self
68 }
69
70 pub fn with_fail_fast(mut self, fail_fast: bool) -> Self {
72 self.fail_fast = fail_fast;
73 self
74 }
75}
76
77#[derive(Debug, Clone, Default, Serialize, Deserialize)]
79pub struct RunSummary {
80 pub total: usize,
82 pub passed: usize,
84 pub failed: usize,
86 pub skipped: usize,
88 pub duration_ms: u64,
90 pub results: Vec<SingleRunResult>,
92}
93
94impl RunSummary {
95 pub fn new() -> Self {
97 Self {
98 total: 0,
99 passed: 0,
100 failed: 0,
101 skipped: 0,
102 duration_ms: 0,
103 results: Vec::new(),
104 }
105 }
106
107 pub fn all_passed(&self) -> bool {
109 self.failed == 0
110 }
111}
112
113#[derive(Debug, Clone, Serialize, Deserialize)]
115pub struct SingleRunResult {
116 pub test_id: String,
118 pub test_name: String,
120 pub category: TestCategory,
122 pub result: TestResult,
124}
125
126#[derive(Debug, Clone, Serialize, Deserialize)]
128pub struct SuiteTestResult {
129 pub test_id: String,
131 pub test_name: String,
133 pub category: TestCategory,
135 pub expected: String,
137 pub result: TestResult,
139 pub events: Vec<TestEvent>,
141}
142
143#[derive(Debug, Clone, Serialize, Deserialize)]
145pub struct SuiteResult {
146 pub runtime_name: String,
148 pub total: usize,
150 pub passed: usize,
152 pub failed: usize,
154 pub skipped: usize,
156 pub duration_ms: u64,
158 pub results: Vec<SuiteTestResult>,
160}
161
162impl SuiteResult {
163 pub fn new(runtime_name: impl Into<String>) -> Self {
165 Self {
166 runtime_name: runtime_name.into(),
167 total: 0,
168 passed: 0,
169 failed: 0,
170 skipped: 0,
171 duration_ms: 0,
172 results: Vec::new(),
173 }
174 }
175
176 fn push<RT: RuntimeInterface>(
177 &mut self,
178 test: &ConformanceTest<RT>,
179 result: TestResult,
180 events: Vec<TestEvent>,
181 ) {
182 if result.passed {
183 self.passed += 1;
184 } else {
185 self.failed += 1;
186 }
187
188 self.results.push(SuiteTestResult {
189 test_id: test.meta.id.clone(),
190 test_name: test.meta.name.clone(),
191 category: test.meta.category,
192 expected: test.meta.expected.clone(),
193 result,
194 events,
195 });
196 }
197}
198
199#[derive(Debug, Clone, Serialize, Deserialize)]
201pub struct ComparisonResult {
202 pub test_id: String,
204 pub test_name: String,
206 pub category: TestCategory,
208 pub runtime_a_result: TestResult,
210 pub runtime_b_result: TestResult,
212 pub runtime_a_name: String,
214 pub runtime_b_name: String,
216 pub status: ComparisonStatus,
218}
219
220#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
222pub enum ComparisonStatus {
223 BothPassedEquivalent,
225 BothPassedDifferent {
227 difference: String,
229 },
230 BothFailedSame,
232 BothFailedDifferent {
234 error_a: String,
236 error_b: String,
238 },
239 OnlyAPassed {
241 error_b: String,
243 },
244 OnlyBPassed {
246 error_a: String,
248 },
249}
250
251impl ComparisonStatus {
252 pub fn is_success(&self) -> bool {
254 matches!(
255 self,
256 ComparisonStatus::BothPassedEquivalent | ComparisonStatus::BothPassedDifferent { .. }
257 )
258 }
259
260 pub fn runtime_a_failed(&self) -> bool {
262 matches!(
263 self,
264 ComparisonStatus::OnlyBPassed { .. }
265 | ComparisonStatus::BothFailedSame
266 | ComparisonStatus::BothFailedDifferent { .. }
267 )
268 }
269}
270
271#[derive(Debug, Clone, Default, Serialize, Deserialize)]
273pub struct ComparisonSummary {
274 pub total: usize,
276 pub both_passed_equivalent: usize,
278 pub both_passed_different: usize,
280 pub both_failed_same: usize,
282 pub both_failed_different: usize,
284 pub only_a_passed: usize,
286 pub only_b_passed: usize,
288 pub duration_ms: u64,
290 pub results: Vec<ComparisonResult>,
292}
293
294impl ComparisonSummary {
295 pub fn new() -> Self {
297 Self {
298 total: 0,
299 both_passed_equivalent: 0,
300 both_passed_different: 0,
301 both_failed_same: 0,
302 both_failed_different: 0,
303 only_a_passed: 0,
304 only_b_passed: 0,
305 duration_ms: 0,
306 results: Vec::new(),
307 }
308 }
309
310 pub fn all_acceptable(&self) -> bool {
312 self.only_a_passed == 0 && self.only_b_passed == 0 && self.both_failed_different == 0
313 }
314
315 pub fn add_result(&mut self, result: ComparisonResult) {
317 match &result.status {
318 ComparisonStatus::BothPassedEquivalent => self.both_passed_equivalent += 1,
319 ComparisonStatus::BothPassedDifferent { .. } => self.both_passed_different += 1,
320 ComparisonStatus::BothFailedSame => self.both_failed_same += 1,
321 ComparisonStatus::BothFailedDifferent { .. } => self.both_failed_different += 1,
322 ComparisonStatus::OnlyAPassed { .. } => self.only_a_passed += 1,
323 ComparisonStatus::OnlyBPassed { .. } => self.only_b_passed += 1,
324 }
325 self.total += 1;
326 self.results.push(result);
327 }
328}
329
330pub struct TestRunner<'a, RT: RuntimeInterface> {
332 runtime: &'a RT,
334 runtime_name: &'a str,
336 config: RunConfig,
338}
339
340impl<'a, RT: RuntimeInterface> TestRunner<'a, RT> {
341 pub fn new(runtime: &'a RT, runtime_name: &'a str, config: RunConfig) -> Self {
343 Self {
344 runtime,
345 runtime_name,
346 config,
347 }
348 }
349
350 pub fn name(&self) -> &str {
352 self.runtime_name
353 }
354
355 pub fn run_all(&self, tests: &[ConformanceTest<RT>]) -> RunSummary {
357 let start = Instant::now();
358 let filtered = self.filter_tests(tests);
359
360 let mut summary = RunSummary::new();
361
362 for test in filtered {
363 let result = self.run_single(test);
364
365 if result.passed {
366 summary.passed += 1;
367 } else {
368 summary.failed += 1;
369 if self.config.fail_fast {
370 summary.results.push(SingleRunResult {
371 test_id: test.meta.id.clone(),
372 test_name: test.meta.name.clone(),
373 category: test.meta.category,
374 result,
375 });
376 break;
377 }
378 }
379
380 summary.results.push(SingleRunResult {
381 test_id: test.meta.id.clone(),
382 test_name: test.meta.name.clone(),
383 category: test.meta.category,
384 result,
385 });
386 }
387
388 summary.total = summary.results.len();
389 summary.duration_ms = start.elapsed().as_millis().min(u128::from(u64::MAX)) as u64;
390
391 summary
392 }
393
394 pub fn run_all_with_logs(&self, tests: &[ConformanceTest<RT>]) -> SuiteResult {
396 let start = Instant::now();
397 let filtered = self.filter_tests(tests);
398
399 let mut summary = SuiteResult::new(self.runtime_name);
400
401 for test in filtered {
402 let (result, events) = self.run_single_with_logger(test);
403 let passed = result.passed;
404 summary.push(test, result, events);
405
406 if !passed && self.config.fail_fast {
407 break;
408 }
409 }
410
411 summary.total = summary.results.len();
412 summary.duration_ms = start.elapsed().as_millis().min(u128::from(u64::MAX)) as u64;
413 summary
414 }
415
416 pub fn run_single(&self, test: &ConformanceTest<RT>) -> TestResult {
418 let start = Instant::now();
419
420 let result =
422 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| test.run(self.runtime)));
423
424 let duration = start.elapsed();
425
426 match result {
427 Ok(mut test_result) => {
428 test_result.duration_ms =
429 Some(duration.as_millis().min(u128::from(u64::MAX)) as u64);
430 test_result
431 }
432 Err(panic) => {
433 let message = if let Some(s) = panic.downcast_ref::<&str>() {
434 s.to_string()
435 } else if let Some(s) = panic.downcast_ref::<String>() {
436 s.clone()
437 } else {
438 "Unknown panic".to_string()
439 };
440
441 TestResult::failed(format!("Test panicked: {message}"))
442 .with_duration(duration.as_millis().min(u128::from(u64::MAX)) as u64)
443 }
444 }
445 }
446
447 pub fn run_single_with_logger(
449 &self,
450 test: &ConformanceTest<RT>,
451 ) -> (TestResult, Vec<TestEvent>) {
452 let logger = ConformanceTestLogger::new(&test.meta.name, &test.meta.expected);
453 let start = Instant::now();
454
455 let result = with_test_logger(&logger, || {
456 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| test.run(self.runtime)))
457 });
458
459 let duration = start.elapsed();
460
461 let mut test_result = match result {
462 Ok(mut test_result) => {
463 test_result.duration_ms =
464 Some(duration.as_millis().min(u128::from(u64::MAX)) as u64);
465 test_result
466 }
467 Err(panic) => {
468 let message = if let Some(s) = panic.downcast_ref::<&str>() {
469 s.to_string()
470 } else if let Some(s) = panic.downcast_ref::<String>() {
471 s.clone()
472 } else {
473 "Unknown panic".to_string()
474 };
475
476 TestResult::failed(format!("Test panicked: {message}"))
477 .with_duration(duration.as_millis().min(u128::from(u64::MAX)) as u64)
478 }
479 };
480
481 if test_result.duration_ms.is_none() {
483 test_result.duration_ms = Some(duration.as_millis().min(u128::from(u64::MAX)) as u64);
484 }
485
486 let events = logger.events();
487 (test_result, events)
488 }
489
490 fn filter_tests<'b>(&self, tests: &'b [ConformanceTest<RT>]) -> Vec<&'b ConformanceTest<RT>> {
492 tests
493 .iter()
494 .filter(|test| {
495 if !self.config.categories.is_empty()
497 && !self.config.categories.contains(&test.meta.category)
498 {
499 return false;
500 }
501
502 if !self.config.test_ids.is_empty() && !self.config.test_ids.contains(&test.meta.id)
504 {
505 return false;
506 }
507
508 if !self.config.tags.is_empty() {
510 let has_tag = self
511 .config
512 .tags
513 .iter()
514 .any(|tag| test.meta.tags.contains(tag));
515 if !has_tag {
516 return false;
517 }
518 }
519
520 true
521 })
522 .collect()
523 }
524}
525
526pub fn run_conformance_suite<RT: RuntimeInterface + Sync>(
528 runtime: &RT,
529 runtime_name: &str,
530 config: RunConfig,
531) -> SuiteResult {
532 let tests = crate::tests::all_tests::<RT>();
533 let runner = TestRunner::new(runtime, runtime_name, config);
534 runner.run_all_with_logs(&tests)
535}
536
537fn failure_message(result: &TestResult) -> String {
538 result
539 .message
540 .clone()
541 .unwrap_or_else(|| "Unknown error".to_string())
542}
543
544pub fn compare_results(
546 runtime_a_name: &str,
547 runtime_b_name: &str,
548 result_a: &TestResult,
549 result_b: &TestResult,
550) -> ComparisonStatus {
551 match (result_a.passed, result_b.passed) {
552 (true, true) => {
553 if result_a.checkpoints == result_b.checkpoints {
555 ComparisonStatus::BothPassedEquivalent
556 } else {
557 ComparisonStatus::BothPassedDifferent {
558 difference: format!(
559 "{} had {} checkpoints, {} had {}",
560 runtime_a_name,
561 result_a.checkpoints.len(),
562 runtime_b_name,
563 result_b.checkpoints.len()
564 ),
565 }
566 }
567 }
568 (false, false) => {
569 let error_a = failure_message(result_a);
571 let error_b = failure_message(result_b);
572
573 if error_a == error_b {
574 ComparisonStatus::BothFailedSame
575 } else {
576 ComparisonStatus::BothFailedDifferent { error_a, error_b }
577 }
578 }
579 (true, false) => ComparisonStatus::OnlyAPassed {
580 error_b: failure_message(result_b),
581 },
582 (false, true) => ComparisonStatus::OnlyBPassed {
583 error_a: failure_message(result_a),
584 },
585 }
586}
587
588pub fn run_comparison<RTA: RuntimeInterface, RTB: RuntimeInterface>(
590 runtime_a: &RTA,
591 runtime_a_name: &str,
592 runtime_b: &RTB,
593 runtime_b_name: &str,
594 tests_a: &[ConformanceTest<RTA>],
595 tests_b: &[ConformanceTest<RTB>],
596 config: RunConfig,
597) -> ComparisonSummary {
598 let start = Instant::now();
599 let mut summary = ComparisonSummary::new();
600
601 let tests_a_map: HashMap<&str, &ConformanceTest<RTA>> =
603 tests_a.iter().map(|t| (t.meta.id.as_str(), t)).collect();
604 let tests_b_map: HashMap<&str, &ConformanceTest<RTB>> =
605 tests_b.iter().map(|t| (t.meta.id.as_str(), t)).collect();
606
607 let common_ids: Vec<&str> = tests_a_map
609 .keys()
610 .filter(|id| tests_b_map.contains_key(*id))
611 .copied()
612 .collect();
613
614 let runner_a = TestRunner::new(runtime_a, runtime_a_name, config.clone());
615 let runner_b = TestRunner::new(runtime_b, runtime_b_name, config.clone());
616
617 for id in common_ids {
618 let test_a = tests_a_map[id];
619 let test_b = tests_b_map[id];
620
621 if !config.categories.is_empty() && !config.categories.contains(&test_a.meta.category) {
623 continue;
624 }
625 if !config.test_ids.is_empty() && !config.test_ids.contains(&test_a.meta.id) {
626 continue;
627 }
628 if !config.tags.is_empty() {
629 let has_tag = config.tags.iter().any(|tag| test_a.meta.tags.contains(tag));
630 if !has_tag {
631 continue;
632 }
633 }
634
635 let result_a = runner_a.run_single(test_a);
637 let result_b = runner_b.run_single(test_b);
638
639 let status = compare_results(runtime_a_name, runtime_b_name, &result_a, &result_b);
641
642 summary.add_result(ComparisonResult {
643 test_id: test_a.meta.id.clone(),
644 test_name: test_a.meta.name.clone(),
645 category: test_a.meta.category,
646 runtime_a_result: result_a,
647 runtime_b_result: result_b,
648 runtime_a_name: runtime_a_name.to_string(),
649 runtime_b_name: runtime_b_name.to_string(),
650 status,
651 });
652
653 if config.fail_fast && !summary.results.last().is_none_or(|r| r.status.is_success()) {
654 break;
655 }
656 }
657
658 summary.duration_ms = start.elapsed().as_millis().min(u128::from(u64::MAX)) as u64;
659 summary
660}
661
662#[cfg(test)]
663mod tests {
664 use super::*;
665 use crate::logging::TestEventKind;
666 use crate::{
667 AsyncFile, BroadcastReceiver, BroadcastRecvError, BroadcastSender, MpscReceiver,
668 MpscSender, OneshotRecvError, OneshotSender, TcpListener, TcpStream, TestMeta, UdpSocket,
669 WatchReceiver, WatchRecvError, WatchSender,
670 };
671 use std::collections::VecDeque;
672 use std::future::Future;
673 use std::io;
674 use std::net::SocketAddr;
675 use std::path::Path;
676 use std::pin::Pin;
677 use std::sync::{Arc, Mutex};
678 use std::task::{Context, Poll};
679
680 #[test]
681 fn run_config_default() {
682 let config = RunConfig::default();
683 assert!(config.categories.is_empty());
684 assert!(config.tags.is_empty());
685 assert!(!config.fail_fast);
686 }
687
688 #[test]
689 fn run_config_builder() {
690 let config = RunConfig::new()
691 .with_categories(vec![TestCategory::IO])
692 .with_tags(vec!["tcp".to_string()])
693 .with_timeout(Duration::from_secs(60))
694 .with_fail_fast(true);
695
696 assert_eq!(config.categories, vec![TestCategory::IO]);
697 assert_eq!(config.tags, vec!["tcp".to_string()]);
698 assert_eq!(config.timeout, Duration::from_secs(60));
699 assert!(config.fail_fast);
700 }
701
702 #[test]
703 fn run_summary_all_passed() {
704 let mut summary = RunSummary::new();
705 summary.passed = 5;
706 summary.failed = 0;
707 assert!(summary.all_passed());
708
709 summary.failed = 1;
710 assert!(!summary.all_passed());
711 }
712
713 #[test]
714 fn comparison_status_is_success() {
715 assert!(ComparisonStatus::BothPassedEquivalent.is_success());
716 assert!(
717 ComparisonStatus::BothPassedDifferent {
718 difference: "test".to_string()
719 }
720 .is_success()
721 );
722 assert!(!ComparisonStatus::BothFailedSame.is_success());
723 assert!(
724 !ComparisonStatus::OnlyAPassed {
725 error_b: "err".to_string()
726 }
727 .is_success()
728 );
729 assert!(
730 !ComparisonStatus::OnlyBPassed {
731 error_a: "err".to_string()
732 }
733 .is_success()
734 );
735 }
736
737 #[test]
738 fn compare_results_both_passed() {
739 let result_a = TestResult::passed();
740 let result_b = TestResult::passed();
741
742 let status = compare_results("A", "B", &result_a, &result_b);
743 assert!(matches!(status, ComparisonStatus::BothPassedEquivalent));
744 }
745
746 #[test]
747 fn compare_results_both_failed_same() {
748 let result_a = TestResult::failed("error");
749 let result_b = TestResult::failed("error");
750
751 let status = compare_results("A", "B", &result_a, &result_b);
752 assert!(matches!(status, ComparisonStatus::BothFailedSame));
753 }
754
755 #[test]
756 fn compare_results_both_failed_different() {
757 let result_a = TestResult::failed("error A");
758 let result_b = TestResult::failed("error B");
759
760 let status = compare_results("A", "B", &result_a, &result_b);
761 assert!(matches!(
762 status,
763 ComparisonStatus::BothFailedDifferent { .. }
764 ));
765 }
766
767 #[test]
768 fn compare_results_only_a_passed() {
769 let result_a = TestResult::passed();
770 let result_b = TestResult::failed("error B");
771
772 let status = compare_results("A", "B", &result_a, &result_b);
773 assert!(matches!(status, ComparisonStatus::OnlyAPassed { .. }));
774 }
775
776 #[test]
777 fn compare_results_only_b_passed() {
778 let result_a = TestResult::failed("error A");
779 let result_b = TestResult::passed();
780
781 let status = compare_results("A", "B", &result_a, &result_b);
782 assert!(matches!(status, ComparisonStatus::OnlyBPassed { .. }));
783 }
784
785 #[test]
786 fn comparison_summary_add_result() {
787 let mut summary = ComparisonSummary::new();
788
789 summary.add_result(ComparisonResult {
790 test_id: "test-1".to_string(),
791 test_name: "Test 1".to_string(),
792 category: TestCategory::IO,
793 runtime_a_result: TestResult::passed(),
794 runtime_b_result: TestResult::passed(),
795 runtime_a_name: "A".to_string(),
796 runtime_b_name: "B".to_string(),
797 status: ComparisonStatus::BothPassedEquivalent,
798 });
799
800 assert_eq!(summary.total, 1);
801 assert_eq!(summary.both_passed_equivalent, 1);
802 assert!(summary.all_acceptable());
803
804 summary.add_result(ComparisonResult {
805 test_id: "test-2".to_string(),
806 test_name: "Test 2".to_string(),
807 category: TestCategory::IO,
808 runtime_a_result: TestResult::failed("error"),
809 runtime_b_result: TestResult::passed(),
810 runtime_a_name: "A".to_string(),
811 runtime_b_name: "B".to_string(),
812 status: ComparisonStatus::OnlyBPassed {
813 error_a: "error".to_string(),
814 },
815 });
816
817 assert_eq!(summary.total, 2);
818 assert_eq!(summary.only_b_passed, 1);
819 assert!(!summary.all_acceptable());
820 }
821
822 #[test]
823 fn run_all_with_logs_captures_checkpoint() {
824 let runtime = MinimalRuntime;
825 let test = ConformanceTest::new(
826 TestMeta {
827 id: "log-001".to_string(),
828 name: "logger checkpoint".to_string(),
829 description: "records checkpoints in logger".to_string(),
830 category: TestCategory::Spawn,
831 tags: vec!["logger".to_string()],
832 expected: "checkpoint is captured".to_string(),
833 },
834 |_rt| {
835 crate::checkpoint("checkpoint-1", serde_json::json!({"value": 1}));
836 TestResult::passed()
837 },
838 );
839
840 let runner = TestRunner::new(&runtime, "minimal", RunConfig::default());
841 let summary = runner.run_all_with_logs(&[test]);
842
843 assert_eq!(summary.total, 1);
844 let events = &summary.results[0].events;
845 assert!(events.iter().any(|e| e.kind == TestEventKind::Checkpoint));
846 }
847
848 #[test]
849 fn run_comparison_with_minimal_runtime() {
850 let runtime_a = MinimalRuntime;
851 let runtime_b = MinimalRuntime;
852
853 let meta = TestMeta {
854 id: "cmp-001".to_string(),
855 name: "comparison baseline".to_string(),
856 description: "comparison test returns pass".to_string(),
857 category: TestCategory::Spawn,
858 tags: vec!["comparison".to_string()],
859 expected: "both runtimes pass".to_string(),
860 };
861
862 let tests_a = vec![ConformanceTest::new(meta.clone(), |_rt| {
863 TestResult::passed()
864 })];
865 let tests_b = vec![ConformanceTest::new(meta, |_rt| TestResult::passed())];
866
867 let summary = run_comparison(
868 &runtime_a,
869 "A",
870 &runtime_b,
871 "B",
872 &tests_a,
873 &tests_b,
874 RunConfig::default(),
875 );
876
877 assert_eq!(summary.total, 1);
878 assert_eq!(summary.both_passed_equivalent, 1);
879 }
880
881 struct MinimalRuntime;
886
887 struct MinimalMpscSender<T> {
888 queue: Arc<Mutex<VecDeque<T>>>,
889 }
890
891 impl<T> Clone for MinimalMpscSender<T> {
892 fn clone(&self) -> Self {
893 Self {
894 queue: Arc::clone(&self.queue),
895 }
896 }
897 }
898
899 struct MinimalMpscReceiver<T> {
900 queue: Arc<Mutex<VecDeque<T>>>,
901 }
902
903 struct MinimalOneshotSender<T> {
904 value: Arc<Mutex<Option<T>>>,
905 }
906
907 struct MinimalBroadcastSender<T> {
908 latest: Arc<Mutex<Option<T>>>,
909 }
910
911 impl<T> Clone for MinimalBroadcastSender<T> {
912 fn clone(&self) -> Self {
913 Self {
914 latest: Arc::clone(&self.latest),
915 }
916 }
917 }
918
919 struct MinimalBroadcastReceiver<T> {
920 latest: Arc<Mutex<Option<T>>>,
921 }
922
923 struct MinimalWatchSender<T> {
924 value: Arc<Mutex<T>>,
925 }
926
927 impl<T> Clone for MinimalWatchSender<T> {
928 fn clone(&self) -> Self {
929 Self {
930 value: Arc::clone(&self.value),
931 }
932 }
933 }
934
935 struct MinimalWatchReceiver<T> {
936 value: Arc<Mutex<T>>,
937 }
938
939 impl<T> Clone for MinimalWatchReceiver<T> {
940 fn clone(&self) -> Self {
941 Self {
942 value: Arc::clone(&self.value),
943 }
944 }
945 }
946
947 #[derive(Debug)]
948 struct MinimalFile;
949
950 #[derive(Debug)]
951 struct MinimalTcpListener;
952
953 #[derive(Debug)]
954 struct MinimalTcpStream;
955
956 #[derive(Debug)]
957 struct MinimalUdpSocket;
958
959 fn minimal_unsupported(label: &'static str) -> io::Error {
960 io::Error::new(
961 io::ErrorKind::Unsupported,
962 format!("minimal runtime does not expose {label}"),
963 )
964 }
965
966 impl<T: Send> MpscSender<T> for MinimalMpscSender<T> {
967 fn send(&self, value: T) -> Pin<Box<dyn Future<Output = Result<(), T>> + Send + '_>> {
968 let queue = Arc::clone(&self.queue);
969 Box::pin(async move {
970 queue
971 .lock()
972 .expect("minimal mpsc queue lock poisoned")
973 .push_back(value);
974 Ok(())
975 })
976 }
977 }
978
979 impl<T: Send> MpscReceiver<T> for MinimalMpscReceiver<T> {
980 fn recv(&mut self) -> Pin<Box<dyn Future<Output = Option<T>> + Send + '_>> {
981 let queue = Arc::clone(&self.queue);
982 Box::pin(async move {
983 queue
984 .lock()
985 .expect("minimal mpsc queue lock poisoned")
986 .pop_front()
987 })
988 }
989 }
990
991 impl<T: Send> OneshotSender<T> for MinimalOneshotSender<T> {
992 fn send(self, value: T) -> Result<(), T> {
993 let mut slot = self.value.lock().expect("minimal oneshot lock poisoned");
994 if slot.is_some() {
995 Err(value)
996 } else {
997 *slot = Some(value);
998 Ok(())
999 }
1000 }
1001 }
1002
1003 impl<T: Send + Clone + 'static> BroadcastSender<T> for MinimalBroadcastSender<T> {
1004 fn send(&self, value: T) -> Result<usize, T> {
1005 *self.latest.lock().expect("minimal broadcast lock poisoned") = Some(value);
1006 Ok(1)
1007 }
1008
1009 fn subscribe(&self) -> Box<dyn BroadcastReceiver<T>> {
1010 Box::new(MinimalBroadcastReceiver {
1011 latest: Arc::clone(&self.latest),
1012 })
1013 }
1014 }
1015
1016 impl<T: Send + Clone + 'static> BroadcastReceiver<T> for MinimalBroadcastReceiver<T> {
1017 fn recv(
1018 &mut self,
1019 ) -> Pin<Box<dyn Future<Output = Result<T, BroadcastRecvError>> + Send + '_>> {
1020 let latest = Arc::clone(&self.latest);
1021 Box::pin(async move {
1022 latest
1023 .lock()
1024 .expect("minimal broadcast lock poisoned")
1025 .clone()
1026 .ok_or(BroadcastRecvError::Closed)
1027 })
1028 }
1029 }
1030
1031 impl<T: Send + Sync> WatchSender<T> for MinimalWatchSender<T> {
1032 fn send(&self, value: T) -> Result<(), T> {
1033 *self.value.lock().expect("minimal watch lock poisoned") = value;
1034 Ok(())
1035 }
1036 }
1037
1038 impl<T: Send + Sync + Clone> WatchReceiver<T> for MinimalWatchReceiver<T> {
1039 fn changed(
1040 &mut self,
1041 ) -> Pin<Box<dyn Future<Output = Result<(), WatchRecvError>> + Send + '_>> {
1042 Box::pin(async { Ok(()) })
1043 }
1044
1045 fn borrow_and_clone(&self) -> T {
1046 self.value
1047 .lock()
1048 .expect("minimal watch lock poisoned")
1049 .clone()
1050 }
1051 }
1052
1053 impl crate::AsyncFile for MinimalFile {
1054 fn write_all<'a>(
1055 &'a mut self,
1056 _buf: &'a [u8],
1057 ) -> Pin<Box<dyn Future<Output = std::io::Result<()>> + Send + 'a>> {
1058 Box::pin(async { Err(minimal_unsupported("file write_all")) })
1059 }
1060
1061 fn read_exact<'a>(
1062 &'a mut self,
1063 _buf: &'a mut [u8],
1064 ) -> Pin<Box<dyn Future<Output = std::io::Result<()>> + Send + 'a>> {
1065 Box::pin(async { Err(minimal_unsupported("file read_exact")) })
1066 }
1067
1068 fn read_to_end<'a>(
1069 &'a mut self,
1070 _buf: &'a mut Vec<u8>,
1071 ) -> Pin<Box<dyn Future<Output = std::io::Result<usize>> + Send + 'a>> {
1072 Box::pin(async { Err(minimal_unsupported("file read_to_end")) })
1073 }
1074
1075 fn seek<'a>(
1076 &'a mut self,
1077 _pos: std::io::SeekFrom,
1078 ) -> Pin<Box<dyn Future<Output = std::io::Result<u64>> + Send + 'a>> {
1079 Box::pin(async { Err(minimal_unsupported("file seek")) })
1080 }
1081
1082 fn sync_all(&self) -> Pin<Box<dyn Future<Output = std::io::Result<()>> + Send + '_>> {
1083 Box::pin(async { Err(minimal_unsupported("file sync_all")) })
1084 }
1085
1086 fn shutdown(&mut self) -> Pin<Box<dyn Future<Output = std::io::Result<()>> + Send + '_>> {
1087 Box::pin(async { Err(minimal_unsupported("file shutdown")) })
1088 }
1089 }
1090
1091 impl crate::TcpListener for MinimalTcpListener {
1092 type Stream = MinimalTcpStream;
1093
1094 fn local_addr(&self) -> std::io::Result<SocketAddr> {
1095 Err(minimal_unsupported("tcp listener local_addr"))
1096 }
1097
1098 fn accept(
1099 &mut self,
1100 ) -> Pin<Box<dyn Future<Output = std::io::Result<(Self::Stream, SocketAddr)>> + Send + '_>>
1101 {
1102 Box::pin(async { Err(minimal_unsupported("tcp listener accept")) })
1103 }
1104 }
1105
1106 impl crate::TcpStream for MinimalTcpStream {
1107 fn read<'a>(
1108 &'a mut self,
1109 _buf: &'a mut [u8],
1110 ) -> Pin<Box<dyn Future<Output = std::io::Result<usize>> + Send + 'a>> {
1111 Box::pin(async { Err(minimal_unsupported("tcp stream read")) })
1112 }
1113
1114 fn read_exact<'a>(
1115 &'a mut self,
1116 _buf: &'a mut [u8],
1117 ) -> Pin<Box<dyn Future<Output = std::io::Result<()>> + Send + 'a>> {
1118 Box::pin(async { Err(minimal_unsupported("tcp stream read_exact")) })
1119 }
1120
1121 fn write_all<'a>(
1122 &'a mut self,
1123 _buf: &'a [u8],
1124 ) -> Pin<Box<dyn Future<Output = std::io::Result<()>> + Send + 'a>> {
1125 Box::pin(async { Err(minimal_unsupported("tcp stream write_all")) })
1126 }
1127
1128 fn shutdown(&mut self) -> Pin<Box<dyn Future<Output = std::io::Result<()>> + Send + '_>> {
1129 Box::pin(async { Err(minimal_unsupported("tcp stream shutdown")) })
1130 }
1131 }
1132
1133 impl crate::UdpSocket for MinimalUdpSocket {
1134 fn local_addr(&self) -> std::io::Result<SocketAddr> {
1135 Err(minimal_unsupported("udp socket local_addr"))
1136 }
1137
1138 fn send_to<'a>(
1139 &'a self,
1140 _buf: &'a [u8],
1141 _addr: SocketAddr,
1142 ) -> Pin<Box<dyn Future<Output = std::io::Result<usize>> + Send + 'a>> {
1143 Box::pin(async { Err(minimal_unsupported("udp socket send_to")) })
1144 }
1145
1146 fn recv_from<'a>(
1147 &'a self,
1148 _buf: &'a mut [u8],
1149 ) -> Pin<Box<dyn Future<Output = std::io::Result<(usize, SocketAddr)>> + Send + 'a>>
1150 {
1151 Box::pin(async { Err(minimal_unsupported("udp socket recv_from")) })
1152 }
1153 }
1154
1155 impl RuntimeInterface for MinimalRuntime {
1156 type JoinHandle<T: Send + 'static> = Pin<Box<dyn Future<Output = T> + Send + 'static>>;
1157 type MpscSender<T: Send + 'static> = MinimalMpscSender<T>;
1158 type MpscReceiver<T: Send + 'static> = MinimalMpscReceiver<T>;
1159 type OneshotSender<T: Send + 'static> = MinimalOneshotSender<T>;
1160 type OneshotReceiver<T: Send + 'static> =
1161 Pin<Box<dyn Future<Output = Result<T, OneshotRecvError>> + Send>>;
1162 type BroadcastSender<T: Send + Clone + 'static> = MinimalBroadcastSender<T>;
1163 type BroadcastReceiver<T: Send + Clone + 'static> = MinimalBroadcastReceiver<T>;
1164 type WatchSender<T: Send + Sync + 'static> = MinimalWatchSender<T>;
1165 type WatchReceiver<T: Send + Sync + Clone + 'static> = MinimalWatchReceiver<T>;
1166 type File = MinimalFile;
1167 type TcpListener = MinimalTcpListener;
1168 type TcpStream = MinimalTcpStream;
1169 type UdpSocket = MinimalUdpSocket;
1170
1171 fn spawn<F>(&self, future: F) -> Self::JoinHandle<F::Output>
1172 where
1173 F: Future + Send + 'static,
1174 F::Output: Send + 'static,
1175 {
1176 Box::pin(future)
1177 }
1178
1179 fn block_on<F: Future>(&self, future: F) -> F::Output {
1180 block_on_simple(future)
1181 }
1182
1183 fn sleep(&self, _duration: Duration) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
1184 Box::pin(async move {})
1185 }
1186
1187 fn timeout<'a, F: Future + Send + 'a>(
1188 &'a self,
1189 _duration: Duration,
1190 future: F,
1191 ) -> Pin<Box<dyn Future<Output = Result<F::Output, crate::TimeoutError>> + Send + 'a>>
1192 where
1193 F::Output: Send,
1194 {
1195 Box::pin(async move { Ok(future.await) })
1196 }
1197
1198 fn mpsc_channel<T: Send + 'static>(
1199 &self,
1200 _capacity: usize,
1201 ) -> (Self::MpscSender<T>, Self::MpscReceiver<T>) {
1202 let queue = Arc::new(Mutex::new(VecDeque::new()));
1203 (
1204 MinimalMpscSender {
1205 queue: Arc::clone(&queue),
1206 },
1207 MinimalMpscReceiver { queue },
1208 )
1209 }
1210
1211 fn oneshot_channel<T: Send + 'static>(
1212 &self,
1213 ) -> (Self::OneshotSender<T>, Self::OneshotReceiver<T>) {
1214 let value = Arc::new(Mutex::new(None));
1215 let receiver_value = Arc::clone(&value);
1216 let receiver: Self::OneshotReceiver<T> = Box::pin(async move {
1217 receiver_value
1218 .lock()
1219 .expect("minimal oneshot lock poisoned")
1220 .take()
1221 .ok_or(OneshotRecvError)
1222 });
1223 (MinimalOneshotSender { value }, receiver)
1224 }
1225
1226 fn broadcast_channel<T: Send + Clone + 'static>(
1227 &self,
1228 _capacity: usize,
1229 ) -> (Self::BroadcastSender<T>, Self::BroadcastReceiver<T>) {
1230 let latest = Arc::new(Mutex::new(None));
1231 (
1232 MinimalBroadcastSender {
1233 latest: Arc::clone(&latest),
1234 },
1235 MinimalBroadcastReceiver { latest },
1236 )
1237 }
1238
1239 fn watch_channel<T: Send + Sync + Clone + 'static>(
1240 &self,
1241 initial: T,
1242 ) -> (Self::WatchSender<T>, Self::WatchReceiver<T>) {
1243 let value = Arc::new(Mutex::new(initial));
1244 (
1245 MinimalWatchSender {
1246 value: Arc::clone(&value),
1247 },
1248 MinimalWatchReceiver { value },
1249 )
1250 }
1251
1252 fn file_create<'a>(
1253 &'a self,
1254 _path: &'a Path,
1255 ) -> Pin<Box<dyn Future<Output = std::io::Result<Self::File>> + Send + 'a>> {
1256 Box::pin(async { Err(minimal_unsupported("file_create")) })
1257 }
1258
1259 fn file_open<'a>(
1260 &'a self,
1261 _path: &'a Path,
1262 ) -> Pin<Box<dyn Future<Output = std::io::Result<Self::File>> + Send + 'a>> {
1263 Box::pin(async { Err(minimal_unsupported("file_open")) })
1264 }
1265
1266 fn tcp_listen<'a>(
1267 &'a self,
1268 _addr: &'a str,
1269 ) -> Pin<Box<dyn Future<Output = std::io::Result<Self::TcpListener>> + Send + 'a>> {
1270 Box::pin(async { Err(minimal_unsupported("tcp_listen")) })
1271 }
1272
1273 fn tcp_connect<'a>(
1274 &'a self,
1275 _addr: SocketAddr,
1276 ) -> Pin<Box<dyn Future<Output = std::io::Result<Self::TcpStream>> + Send + 'a>> {
1277 Box::pin(async { Err(minimal_unsupported("tcp_connect")) })
1278 }
1279
1280 fn udp_bind<'a>(
1281 &'a self,
1282 _addr: &'a str,
1283 ) -> Pin<Box<dyn Future<Output = std::io::Result<Self::UdpSocket>> + Send + 'a>> {
1284 Box::pin(async { Err(minimal_unsupported("udp_bind")) })
1285 }
1286 }
1287
1288 #[test]
1289 fn minimal_runtime_channels_are_non_panicking() {
1290 let runtime = MinimalRuntime;
1291
1292 let (tx, mut rx) = runtime.mpsc_channel::<u32>(4);
1293 assert_eq!(runtime.block_on(tx.send(7)), Ok(()));
1294 assert_eq!(runtime.block_on(rx.recv()), Some(7));
1295 assert_eq!(runtime.block_on(rx.recv()), None);
1296
1297 let (tx, rx) = runtime.oneshot_channel::<u32>();
1298 assert_eq!(tx.send(9), Ok(()));
1299 assert_eq!(runtime.block_on(rx), Ok(9));
1300
1301 let (tx, mut rx) = runtime.broadcast_channel::<u32>(4);
1302 assert_eq!(tx.send(11), Ok(1));
1303 assert_eq!(runtime.block_on(rx.recv()), Ok(11));
1304
1305 let mut rx2 = tx.subscribe();
1306 assert_eq!(runtime.block_on(rx2.recv()), Ok(11));
1307
1308 let (tx, mut rx) = runtime.watch_channel(13_u32);
1309 assert_eq!(rx.borrow_and_clone(), 13);
1310 assert_eq!(tx.send(17), Ok(()));
1311 assert_eq!(runtime.block_on(rx.changed()), Ok(()));
1312 assert_eq!(rx.borrow_and_clone(), 17);
1313 }
1314
1315 #[test]
1316 fn minimal_runtime_io_surfaces_fail_closed_with_unsupported_errors() {
1317 let runtime = MinimalRuntime;
1318
1319 let create_err = runtime
1320 .block_on(runtime.file_create(Path::new("minimal.txt")))
1321 .expect_err("minimal file_create should fail closed");
1322 assert_eq!(create_err.kind(), io::ErrorKind::Unsupported);
1323
1324 let open_err = runtime
1325 .block_on(runtime.file_open(Path::new("minimal.txt")))
1326 .expect_err("minimal file_open should fail closed");
1327 assert_eq!(open_err.kind(), io::ErrorKind::Unsupported);
1328
1329 let listen_err = runtime
1330 .block_on(runtime.tcp_listen("127.0.0.1:0"))
1331 .expect_err("minimal tcp_listen should fail closed");
1332 assert_eq!(listen_err.kind(), io::ErrorKind::Unsupported);
1333
1334 let connect_err = runtime
1335 .block_on(runtime.tcp_connect(SocketAddr::from(([127, 0, 0, 1], 80))))
1336 .expect_err("minimal tcp_connect should fail closed");
1337 assert_eq!(connect_err.kind(), io::ErrorKind::Unsupported);
1338
1339 let bind_err = runtime
1340 .block_on(runtime.udp_bind("127.0.0.1:0"))
1341 .expect_err("minimal udp_bind should fail closed");
1342 assert_eq!(bind_err.kind(), io::ErrorKind::Unsupported);
1343
1344 let mut file = MinimalFile;
1345 assert_eq!(
1346 runtime
1347 .block_on(file.write_all(b"abc"))
1348 .expect_err("minimal file write_all should fail closed")
1349 .kind(),
1350 io::ErrorKind::Unsupported
1351 );
1352 let mut buf = [0_u8; 4];
1353 assert_eq!(
1354 runtime
1355 .block_on(file.read_exact(&mut buf))
1356 .expect_err("minimal file read_exact should fail closed")
1357 .kind(),
1358 io::ErrorKind::Unsupported
1359 );
1360 let mut bytes = Vec::new();
1361 assert_eq!(
1362 runtime
1363 .block_on(file.read_to_end(&mut bytes))
1364 .expect_err("minimal file read_to_end should fail closed")
1365 .kind(),
1366 io::ErrorKind::Unsupported
1367 );
1368 assert_eq!(
1369 runtime
1370 .block_on(file.seek(std::io::SeekFrom::Start(0)))
1371 .expect_err("minimal file seek should fail closed")
1372 .kind(),
1373 io::ErrorKind::Unsupported
1374 );
1375 assert_eq!(
1376 runtime
1377 .block_on(file.sync_all())
1378 .expect_err("minimal file sync_all should fail closed")
1379 .kind(),
1380 io::ErrorKind::Unsupported
1381 );
1382 assert_eq!(
1383 runtime
1384 .block_on(file.shutdown())
1385 .expect_err("minimal file shutdown should fail closed")
1386 .kind(),
1387 io::ErrorKind::Unsupported
1388 );
1389
1390 let mut listener = MinimalTcpListener;
1391 assert_eq!(
1392 listener
1393 .local_addr()
1394 .expect_err("minimal tcp local_addr should fail closed")
1395 .kind(),
1396 io::ErrorKind::Unsupported
1397 );
1398 assert_eq!(
1399 runtime
1400 .block_on(listener.accept())
1401 .expect_err("minimal tcp accept should fail closed")
1402 .kind(),
1403 io::ErrorKind::Unsupported
1404 );
1405
1406 let mut stream = MinimalTcpStream;
1407 assert_eq!(
1408 runtime
1409 .block_on(stream.read(&mut buf))
1410 .expect_err("minimal tcp read should fail closed")
1411 .kind(),
1412 io::ErrorKind::Unsupported
1413 );
1414 assert_eq!(
1415 runtime
1416 .block_on(stream.read_exact(&mut buf))
1417 .expect_err("minimal tcp read_exact should fail closed")
1418 .kind(),
1419 io::ErrorKind::Unsupported
1420 );
1421 assert_eq!(
1422 runtime
1423 .block_on(stream.write_all(b"abc"))
1424 .expect_err("minimal tcp write_all should fail closed")
1425 .kind(),
1426 io::ErrorKind::Unsupported
1427 );
1428 assert_eq!(
1429 runtime
1430 .block_on(stream.shutdown())
1431 .expect_err("minimal tcp shutdown should fail closed")
1432 .kind(),
1433 io::ErrorKind::Unsupported
1434 );
1435
1436 let socket = MinimalUdpSocket;
1437 assert_eq!(
1438 socket
1439 .local_addr()
1440 .expect_err("minimal udp local_addr should fail closed")
1441 .kind(),
1442 io::ErrorKind::Unsupported
1443 );
1444 assert_eq!(
1445 runtime
1446 .block_on(socket.send_to(b"abc", SocketAddr::from(([127, 0, 0, 1], 80))))
1447 .expect_err("minimal udp send_to should fail closed")
1448 .kind(),
1449 io::ErrorKind::Unsupported
1450 );
1451 assert_eq!(
1452 runtime
1453 .block_on(socket.recv_from(&mut buf))
1454 .expect_err("minimal udp recv_from should fail closed")
1455 .kind(),
1456 io::ErrorKind::Unsupported
1457 );
1458 }
1459
1460 #[test]
1461 fn minimal_runtime_contains_no_panic_based_markers() {
1462 let runner_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("src/runner.rs");
1463 let source = std::fs::read_to_string(&runner_path)
1464 .unwrap_or_else(|_| panic!("could not read {}", runner_path.display()));
1465 assert!(
1466 !source.contains("panic!(\"minimal"),
1467 "runner minimal runtime contains panic-based markers"
1468 );
1469 }
1470
1471 fn block_on_simple<F: Future>(future: F) -> F::Output {
1472 let waker = std::task::Waker::noop().clone();
1473 let mut context = Context::from_waker(&waker);
1474 let mut future = std::pin::pin!(future);
1475
1476 loop {
1477 match future.as_mut().poll(&mut context) {
1478 Poll::Ready(value) => return value,
1479 Poll::Pending => std::thread::yield_now(),
1480 }
1481 }
1482 }
1483}