Skip to main content

asupersync_conformance/
runner.rs

1//! Test runner for executing conformance tests.
2//!
3//! The `TestRunner` executes conformance tests against one or more runtime
4//! implementations and collects results. When running in comparison mode,
5//! it runs each test against both runtimes and compares the outcomes.
6
7use 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/// Configuration for test execution.
14#[derive(Debug, Clone)]
15pub struct RunConfig {
16    /// Categories to run (empty = all).
17    pub categories: Vec<TestCategory>,
18    /// Tags to filter by (empty = all).
19    pub tags: Vec<String>,
20    /// Specific test IDs to run (empty = all).
21    pub test_ids: Vec<String>,
22    /// Timeout per test.
23    pub timeout: Duration,
24    /// Whether to continue on failure.
25    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    /// Create a new configuration with default settings.
42    pub fn new() -> Self {
43        Self::default()
44    }
45
46    /// Filter to specific categories.
47    pub fn with_categories(mut self, categories: Vec<TestCategory>) -> Self {
48        self.categories = categories;
49        self
50    }
51
52    /// Filter to specific tags.
53    pub fn with_tags(mut self, tags: Vec<String>) -> Self {
54        self.tags = tags;
55        self
56    }
57
58    /// Filter to specific test IDs.
59    pub fn with_test_ids(mut self, test_ids: Vec<String>) -> Self {
60        self.test_ids = test_ids;
61        self
62    }
63
64    /// Set the timeout per test.
65    pub fn with_timeout(mut self, timeout: Duration) -> Self {
66        self.timeout = timeout;
67        self
68    }
69
70    /// Set whether to stop on first failure.
71    pub fn with_fail_fast(mut self, fail_fast: bool) -> Self {
72        self.fail_fast = fail_fast;
73        self
74    }
75}
76
77/// Summary of a test run.
78#[derive(Debug, Clone, Default, Serialize, Deserialize)]
79pub struct RunSummary {
80    /// Total number of tests executed.
81    pub total: usize,
82    /// Number of tests that passed.
83    pub passed: usize,
84    /// Number of tests that failed.
85    pub failed: usize,
86    /// Number of tests that were skipped.
87    pub skipped: usize,
88    /// Total execution time.
89    pub duration_ms: u64,
90    /// Individual test results.
91    pub results: Vec<SingleRunResult>,
92}
93
94impl RunSummary {
95    /// Create an empty summary.
96    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    /// Check if all tests passed.
108    pub fn all_passed(&self) -> bool {
109        self.failed == 0
110    }
111}
112
113/// Result of running a single test.
114#[derive(Debug, Clone, Serialize, Deserialize)]
115pub struct SingleRunResult {
116    /// Test ID.
117    pub test_id: String,
118    /// Test name.
119    pub test_name: String,
120    /// Test category.
121    pub category: TestCategory,
122    /// The test result.
123    pub result: TestResult,
124}
125
126/// Result of running a test with structured events.
127#[derive(Debug, Clone, Serialize, Deserialize)]
128pub struct SuiteTestResult {
129    /// Test ID.
130    pub test_id: String,
131    /// Test name.
132    pub test_name: String,
133    /// Test category.
134    pub category: TestCategory,
135    /// Expected behavior description.
136    pub expected: String,
137    /// Test result payload.
138    pub result: TestResult,
139    /// Structured events captured during execution.
140    pub events: Vec<TestEvent>,
141}
142
143/// Summary of a full conformance suite run with structured events.
144#[derive(Debug, Clone, Serialize, Deserialize)]
145pub struct SuiteResult {
146    /// Runtime name.
147    pub runtime_name: String,
148    /// Total number of tests executed.
149    pub total: usize,
150    /// Number of tests that passed.
151    pub passed: usize,
152    /// Number of tests that failed.
153    pub failed: usize,
154    /// Number of tests that were skipped.
155    pub skipped: usize,
156    /// Total execution time.
157    pub duration_ms: u64,
158    /// Individual test results.
159    pub results: Vec<SuiteTestResult>,
160}
161
162impl SuiteResult {
163    /// Create a new suite result.
164    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/// Result of comparing a test run between two runtimes.
200#[derive(Debug, Clone, Serialize, Deserialize)]
201pub struct ComparisonResult {
202    /// Test ID.
203    pub test_id: String,
204    /// Test name.
205    pub test_name: String,
206    /// Test category.
207    pub category: TestCategory,
208    /// Result from the first runtime.
209    pub runtime_a_result: TestResult,
210    /// Result from the second runtime.
211    pub runtime_b_result: TestResult,
212    /// Name of runtime A.
213    pub runtime_a_name: String,
214    /// Name of runtime B.
215    pub runtime_b_name: String,
216    /// Comparison status.
217    pub status: ComparisonStatus,
218}
219
220/// Status of comparing test results between two runtimes.
221#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
222pub enum ComparisonStatus {
223    /// Both runtimes passed with equivalent behavior.
224    BothPassedEquivalent,
225    /// Both runtimes passed but with different behavior (may be acceptable).
226    BothPassedDifferent {
227        /// Description of the difference.
228        difference: String,
229    },
230    /// Both runtimes failed with the same error.
231    BothFailedSame,
232    /// Both runtimes failed but with different errors.
233    BothFailedDifferent {
234        /// Error from runtime A.
235        error_a: String,
236        /// Error from runtime B.
237        error_b: String,
238    },
239    /// Runtime A passed but runtime B failed (unexpected).
240    OnlyAPassed {
241        /// Error from runtime B.
242        error_b: String,
243    },
244    /// Runtime B passed but runtime A failed.
245    OnlyBPassed {
246        /// Error from runtime A.
247        error_a: String,
248    },
249}
250
251impl ComparisonStatus {
252    /// Check if this comparison indicates success (both passed).
253    pub fn is_success(&self) -> bool {
254        matches!(
255            self,
256            ComparisonStatus::BothPassedEquivalent | ComparisonStatus::BothPassedDifferent { .. }
257        )
258    }
259
260    /// Check if runtime A had an issue.
261    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/// Summary of a comparison run.
272#[derive(Debug, Clone, Default, Serialize, Deserialize)]
273pub struct ComparisonSummary {
274    /// Total tests compared.
275    pub total: usize,
276    /// Tests where both runtimes passed equivalently.
277    pub both_passed_equivalent: usize,
278    /// Tests where both passed but differed.
279    pub both_passed_different: usize,
280    /// Tests where both failed the same way.
281    pub both_failed_same: usize,
282    /// Tests where both failed differently.
283    pub both_failed_different: usize,
284    /// Tests where only runtime A passed.
285    pub only_a_passed: usize,
286    /// Tests where only runtime B passed.
287    pub only_b_passed: usize,
288    /// Total duration.
289    pub duration_ms: u64,
290    /// Individual comparison results.
291    pub results: Vec<ComparisonResult>,
292}
293
294impl ComparisonSummary {
295    /// Create an empty summary.
296    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    /// Check if all tests had acceptable outcomes.
311    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    /// Add a comparison result.
316    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
330/// Test runner that executes conformance tests.
331pub struct TestRunner<'a, RT: RuntimeInterface> {
332    /// The runtime to test against.
333    runtime: &'a RT,
334    /// Runtime name for logging.
335    runtime_name: &'a str,
336    /// Configuration.
337    config: RunConfig,
338}
339
340impl<'a, RT: RuntimeInterface> TestRunner<'a, RT> {
341    /// Create a new test runner.
342    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    /// Get the runtime name.
351    pub fn name(&self) -> &str {
352        self.runtime_name
353    }
354
355    /// Run all tests that match the configuration.
356    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    /// Run all tests with structured logging enabled.
395    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    /// Run a single test.
417    pub fn run_single(&self, test: &ConformanceTest<RT>) -> TestResult {
418        let start = Instant::now();
419
420        // Catch panics
421        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    /// Run a single test and return structured events.
448    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        // Ensure duration is always set.
482        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    /// Filter tests based on configuration.
491    fn filter_tests<'b>(&self, tests: &'b [ConformanceTest<RT>]) -> Vec<&'b ConformanceTest<RT>> {
492        tests
493            .iter()
494            .filter(|test| {
495                // Filter by category
496                if !self.config.categories.is_empty()
497                    && !self.config.categories.contains(&test.meta.category)
498                {
499                    return false;
500                }
501
502                // Filter by test ID
503                if !self.config.test_ids.is_empty() && !self.config.test_ids.contains(&test.meta.id)
504                {
505                    return false;
506                }
507
508                // Filter by tags
509                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
526/// Run the full conformance suite and collect structured logs.
527pub 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
544/// Compare test results between two runtimes.
545pub 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            // Both passed - check if checkpoints match
554            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            // Both failed - check if errors match
570            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
588/// Run comparison between two runtimes.
589pub 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    // Build map of tests by ID
602    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    // Find common test IDs
608    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        // Apply filters
622        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        // Run on both runtimes
636        let result_a = runner_a.run_single(test_a);
637        let result_b = runner_b.run_single(test_b);
638
639        // Compare
640        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    // ---------------------------------------------------------------------
882    // Minimal fail-closed runtime for runner unit tests.
883    // ---------------------------------------------------------------------
884
885    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}