mycotest/
runner.rs

1use crate::{report, Test};
2use core::{
3    ffi,
4    fmt::{self, Write},
5    mem, ptr, slice,
6    sync::atomic::{AtomicPtr, Ordering},
7};
8use mycelium_trace::writer::MakeWriter;
9
10// These symbols are auto-generated by lld (and similar linkers) for data
11// `link_section` sections, and are located at the beginning and end of the
12// section.
13//
14// The memory region between the two symbols will contain an array of `Test`
15// instances.
16extern "C" {
17    static __start_MyceliumTests: ffi::c_void;
18    static __stop_MyceliumTests: ffi::c_void;
19}
20
21static CURRENT_TEST: AtomicPtr<Test> = AtomicPtr::new(ptr::null_mut());
22
23#[derive(Debug)]
24pub struct TestsFailed {
25    failed: usize,
26    passed: usize,
27}
28
29/// Run all tests linked into the current binary, outputting test reports to the
30/// provided `mk_writer`.
31///
32/// # Returns
33///
34/// - `Err(())` if any test failed
35/// - `Ok(())` if all tests passed
36pub fn run_tests(mk_writer: impl for<'writer> MakeWriter<'writer>) -> Result<(), TestsFailed> {
37    let _span = tracing::info_span!("run tests").entered();
38
39    let mut passed = 0;
40    let mut failed = 0;
41    let tests = all_tests();
42    writeln!(
43        mk_writer.make_writer(),
44        "{}{}",
45        report::TEST_COUNT,
46        tests.len()
47    )
48    .expect("write failed");
49    for test in tests {
50        CURRENT_TEST.store(test as *const _ as *mut _, Ordering::Release);
51
52        writeln!(
53            mk_writer.make_writer(),
54            "{}{} {}",
55            report::START_TEST,
56            test.descr.module,
57            test.descr.name
58        )
59        .expect("write failed");
60
61        let _span =
62            tracing::info_span!("test", name = %test.descr.name, module = %test.descr.module)
63                .entered();
64
65        let outcome = (test.run)();
66        tracing::info!(?outcome);
67        CURRENT_TEST.store(ptr::null_mut(), Ordering::Release);
68        test.write_outcome(outcome, mk_writer.make_writer())
69            .expect("write failed");
70        if outcome.is_ok() {
71            passed += 1;
72        } else {
73            failed += 1;
74        }
75    }
76
77    tracing::warn!("{} passed | {} failed", passed, failed);
78
79    if failed > 0 {
80        Err(TestsFailed { passed, failed })
81    } else {
82        Ok(())
83    }
84}
85
86/// Returns the current test, if a test is currently running.
87pub fn current_test() -> Option<&'static Test> {
88    let ptr = CURRENT_TEST.load(Ordering::Acquire);
89    ptr::NonNull::new(ptr).map(|ptr| unsafe {
90        // Safety: the current test is always set from a `&'static`ally declared `Test`.
91        &*(ptr.as_ptr() as *const _)
92    })
93}
94
95/// Get a list of `Test` objects.
96pub fn all_tests() -> &'static [Test] {
97    unsafe {
98        // FIXME: These should probably be `&raw const __start_*`.
99        let start: *const ffi::c_void = &__start_MyceliumTests;
100        let stop: *const ffi::c_void = &__stop_MyceliumTests;
101
102        let len_bytes = (stop as usize) - (start as usize);
103        let len = len_bytes / mem::size_of::<Test>();
104        assert!(
105            len_bytes % mem::size_of::<Test>() == 0,
106            "Section should contain a whole number of `Test`s"
107        );
108
109        if len > 0 {
110            slice::from_raw_parts(start as *const Test, len)
111        } else {
112            &[]
113        }
114    }
115}
116
117impl fmt::Display for TestsFailed {
118    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
119        write!(
120            f,
121            "{} tests failed (out of {})",
122            self.failed,
123            self.failed + self.passed
124        )
125    }
126}