hilt 0.1.0

Renode-based hardware-in-the-loop test fixtures for embedded Rust projects
Documentation
//! Captured output of a HIL run and the `assert_hil_ok!` macro.

use std::fmt;

/// Success marker firmware logs (via a CPU hook or semihosting print).
pub const HIL_OK_MARKER: &str = "HIL OK";

/// Result of a HIL run: captured stdout/stderr and the container exit code.
#[derive(Debug, Clone)]
pub struct HilOutput {
    stdout: String,
    stderr: String,
    exit_code: Option<i32>,
}

impl HilOutput {
    /// Builds an output from captured streams and exit code.
    #[must_use]
    pub fn new(stdout: String, stderr: String, exit_code: Option<i32>) -> Self {
        Self {
            stdout,
            stderr,
            exit_code,
        }
    }

    /// `true` if either stream contains the [`HIL_OK_MARKER`].
    #[must_use]
    pub fn passed(&self) -> bool {
        self.contains(HIL_OK_MARKER)
    }

    /// `true` if either stream contains `marker`.
    #[must_use]
    pub fn contains(&self, marker: &str) -> bool {
        self.stdout.contains(marker) || self.stderr.contains(marker)
    }

    /// Captured stdout.
    #[must_use]
    pub fn stdout(&self) -> &str {
        &self.stdout
    }

    /// Captured stderr.
    #[must_use]
    pub fn stderr(&self) -> &str {
        &self.stderr
    }

    /// Stdout and stderr joined with a newline — convenient for log scraping.
    #[must_use]
    pub fn combined(&self) -> String {
        format!("{}\n{}", self.stdout, self.stderr)
    }

    /// Container process exit code, if the process exited normally.
    #[must_use]
    pub fn exit_code(&self) -> Option<i32> {
        self.exit_code
    }
}

impl fmt::Display for HilOutput {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let code = match self.exit_code {
            Some(c) => c.to_string(),
            None => "none".to_string(),
        };
        write!(
            f,
            "exit={code}\n--- stdout ---\n{}\n--- stderr ---\n{}",
            self.stdout, self.stderr
        )
    }
}

/// Asserts that a [`HilOutput`] passed.
///
/// `assert_hil_ok!(output)` checks for `HIL OK`; `assert_hil_ok!(output,
/// marker)` checks for a custom marker. On failure it prints the full output.
#[macro_export]
macro_rules! assert_hil_ok {
    ($output:expr) => {
        assert!($output.passed(), "HIL test failed.\n{}", $output)
    };
    ($output:expr, $marker:expr) => {
        assert!(
            $output.contains($marker),
            "HIL test failed -- expected {:?}.\n{}",
            $marker,
            $output,
        )
    };
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn passed_detects_marker_in_either_stream() {
        assert!(HilOutput::new("boot\nHIL OK\n".into(), String::new(), Some(0)).passed());
        assert!(HilOutput::new(String::new(), "HIL OK".into(), Some(0)).passed());
        assert!(!HilOutput::new("timeout".into(), String::new(), Some(1)).passed());
    }

    #[test]
    fn contains_and_combined() {
        let o = HilOutput::new("hello".into(), "world".into(), Some(0));
        assert!(o.contains("hello"));
        assert!(o.contains("world"));
        assert_eq!(o.combined(), "hello\nworld");
    }

    #[test]
    fn display_includes_code_and_streams() {
        let s = HilOutput::new("hi".into(), "there".into(), Some(42)).to_string();
        assert!(s.contains("42"));
        assert!(s.contains("hi"));
        assert!(s.contains("there"));
        let none = HilOutput::new(String::new(), String::new(), None).to_string();
        assert!(none.contains("exit=none"));
    }

    #[test]
    fn assert_macro_passes() {
        let o = HilOutput::new("HIL OK".into(), String::new(), Some(0));
        assert_hil_ok!(o);
        let o2 = HilOutput::new("CUSTOM".into(), String::new(), Some(0));
        assert_hil_ok!(o2, "CUSTOM");
    }
}