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
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
#![no_std]

pub use flipperzero_test_macros::{tests, tests_runner};

/// The type of a Flipper Zero test function.
pub type TestFn = fn() -> TestResult;

/// The result type of a Flipper Zero test.
pub type TestResult = Result<(), TestFailure>;

/// A failure that occurred within a Flipper Zero test.
#[derive(Debug)]
pub enum TestFailure {
    AssertEq {
        left: &'static str,
        right: &'static str,
    },
    AssertNe {
        left: &'static str,
        right: &'static str,
    },
    Str(&'static str),
}

impl From<&'static str> for TestFailure {
    fn from(value: &'static str) -> Self {
        TestFailure::Str(value)
    }
}

impl ufmt::uDisplay for TestFailure {
    fn fmt<W>(&self, f: &mut ufmt::Formatter<'_, W>) -> Result<(), W::Error>
    where
        W: ufmt::uWrite + ?Sized,
    {
        match self {
            TestFailure::AssertEq { left, right } => {
                f.write_str("assertion failed: ")?;
                f.write_str(left)?;
                f.write_str(" == ")?;
                f.write_str(right)
            }
            TestFailure::AssertNe { left, right } => {
                f.write_str("assertion failed: ")?;
                f.write_str(left)?;
                f.write_str(" != ")?;
                f.write_str(right)
            }
            TestFailure::Str(s) => f.write_str(s),
        }
    }
}

pub mod __macro_support {
    use core::ffi::c_char;

    use flipperzero_sys as sys;
    use sys::furi::UnsafeRecord;

    use crate::TestFn;

    const RECORD_STORAGE: *const c_char = sys::c_string!("storage");

    struct OutputFile(*mut sys::File);

    impl Drop for OutputFile {
        fn drop(&mut self) {
            unsafe { sys::storage_file_free(self.0) };
        }
    }

    impl OutputFile {
        fn new(storage: &UnsafeRecord<sys::Storage>) -> Self {
            let output_file = unsafe { sys::storage_file_alloc(storage.as_ptr()) };
            unsafe {
                sys::storage_file_open(
                    output_file,
                    sys::c_string!("/ext/flipperzero-rs-stdout"),
                    sys::FS_AccessMode_FSAM_WRITE,
                    sys::FS_OpenMode_FSOM_CREATE_ALWAYS,
                );
            }
            Self(output_file)
        }
    }

    impl ufmt::uWrite for OutputFile {
        type Error = i32;

        fn write_str(&mut self, s: &str) -> Result<(), Self::Error> {
            assert!(s.len() <= u16::MAX as usize);
            let mut buf = s.as_bytes();
            while !buf.is_empty() {
                let written = unsafe {
                    sys::storage_file_write(self.0, s.as_bytes().as_ptr().cast(), s.len() as u16)
                };
                if written == 0 {
                    return Err(1); // TODO
                }
                buf = &buf[written as usize..];
            }
            Ok(())
        }
    }

    pub fn run_tests(
        test_count: usize,
        tests: impl Iterator<Item = (&'static str, &'static str, TestFn)>,
    ) -> Result<(), i32> {
        let storage: UnsafeRecord<sys::Storage> = unsafe { UnsafeRecord::open(RECORD_STORAGE) };
        let mut output_file = OutputFile::new(&storage);

        ufmt::uwriteln!(output_file, "")?;
        ufmt::uwriteln!(output_file, "running {} tests", test_count)?;

        let heap_before = unsafe { sys::memmgr_get_free_heap() };
        let cycle_counter = unsafe { sys::furi_get_tick() };
        let mut failed = 0;
        for (module, name, test_fn) in tests {
            ufmt::uwrite!(output_file, "test {}::{} ... ", module, name)?;
            if let Err(e) = test_fn() {
                failed += 1;
                ufmt::uwriteln!(output_file, "FAILED")?;
                ufmt::uwriteln!(output_file, "")?;
                ufmt::uwriteln!(output_file, "---- {}::{} stdout ----", module, name)?;
                ufmt::uwriteln!(output_file, "{}", e)?;
                ufmt::uwriteln!(output_file, "")?;
            } else {
                ufmt::uwriteln!(output_file, "ok")?;
            };
        }
        let time_taken = unsafe { sys::furi_get_tick() } - cycle_counter;

        // Wait for tested services and apps to deallocate memory
        unsafe { sys::furi_delay_us(10_000) };
        let heap_after = unsafe { sys::memmgr_get_free_heap() };

        // Final Report
        ufmt::uwriteln!(output_file, "")?;
        ufmt::uwriteln!(
            output_file,
            "test result: {}. {} passed; {} failed; 0 ignored; 0 measured; 0 filtered out; finished in {}ms",
            if failed == 0 { "ok" } else { "FAILED" },
            test_count - failed,
            failed,
            time_taken,
        )?;
        ufmt::uwriteln!(output_file, "leaked: {} bytes", heap_before - heap_after)?;
        ufmt::uwriteln!(output_file, "")?;

        if failed == 0 {
            Ok(())
        } else {
            Err(1)
        }
    }
}