mycotest/
runner.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
use crate::{report, Test};
use core::{
    ffi,
    fmt::{self, Write},
    mem, ptr, slice,
    sync::atomic::{AtomicPtr, Ordering},
};
use mycelium_trace::writer::MakeWriter;

// These symbols are auto-generated by lld (and similar linkers) for data
// `link_section` sections, and are located at the beginning and end of the
// section.
//
// The memory region between the two symbols will contain an array of `Test`
// instances.
extern "C" {
    static __start_MyceliumTests: ffi::c_void;
    static __stop_MyceliumTests: ffi::c_void;
}

static CURRENT_TEST: AtomicPtr<Test> = AtomicPtr::new(ptr::null_mut());

#[derive(Debug)]
pub struct TestsFailed {
    failed: usize,
    passed: usize,
}

/// Run all tests linked into the current binary, outputting test reports to the
/// provided `mk_writer`.
///
/// # Returns
///
/// - `Err(())` if any test failed
/// - `Ok(())` if all tests passed
pub fn run_tests(mk_writer: impl for<'writer> MakeWriter<'writer>) -> Result<(), TestsFailed> {
    let _span = tracing::info_span!("run tests").entered();

    let mut passed = 0;
    let mut failed = 0;
    let tests = all_tests();
    writeln!(
        mk_writer.make_writer(),
        "{}{}",
        report::TEST_COUNT,
        tests.len()
    )
    .expect("write failed");
    for test in tests {
        CURRENT_TEST.store(test as *const _ as *mut _, Ordering::Release);

        writeln!(
            mk_writer.make_writer(),
            "{}{} {}",
            report::START_TEST,
            test.descr.module,
            test.descr.name
        )
        .expect("write failed");

        let _span =
            tracing::info_span!("test", name = %test.descr.name, module = %test.descr.module)
                .entered();

        let outcome = (test.run)();
        tracing::info!(?outcome);
        CURRENT_TEST.store(ptr::null_mut(), Ordering::Release);
        test.write_outcome(outcome, mk_writer.make_writer())
            .expect("write failed");
        if outcome.is_ok() {
            passed += 1;
        } else {
            failed += 1;
        }
    }

    tracing::warn!("{} passed | {} failed", passed, failed);

    if failed > 0 {
        Err(TestsFailed { passed, failed })
    } else {
        Ok(())
    }
}

/// Returns the current test, if a test is currently running.
pub fn current_test() -> Option<&'static Test> {
    let ptr = CURRENT_TEST.load(Ordering::Acquire);
    ptr::NonNull::new(ptr).map(|ptr| unsafe {
        // Safety: the current test is always set from a `&'static`ally declared `Test`.
        &*(ptr.as_ptr() as *const _)
    })
}

/// Get a list of `Test` objects.
pub fn all_tests() -> &'static [Test] {
    unsafe {
        // FIXME: These should probably be `&raw const __start_*`.
        let start: *const ffi::c_void = &__start_MyceliumTests;
        let stop: *const ffi::c_void = &__stop_MyceliumTests;

        let len_bytes = (stop as usize) - (start as usize);
        let len = len_bytes / mem::size_of::<Test>();
        assert!(
            len_bytes % mem::size_of::<Test>() == 0,
            "Section should contain a whole number of `Test`s"
        );

        if len > 0 {
            slice::from_raw_parts(start as *const Test, len)
        } else {
            &[]
        }
    }
}

impl fmt::Display for TestsFailed {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            f,
            "{} tests failed (out of {})",
            self.failed,
            self.failed + self.passed
        )
    }
}