axbuild 0.4.22

An OS build lib toolkit used by arceos
use std::sync::{Arc, Mutex};

use anyhow::{Result, bail};
use regex_automata::{
    Input,
    dfa::{Automaton, dense},
    util::primitives::StateID,
};

const TRANSCRIPT_TAIL_BYTES: usize = 2048;

#[derive(Clone)]
pub(crate) struct QemuSuccessOutput {
    state: Arc<Mutex<QemuSuccessOutputState>>,
}

impl QemuSuccessOutput {
    fn new(success_regex: &[String]) -> Self {
        Self {
            state: Arc::new(Mutex::new(QemuSuccessOutputState {
                matcher: StreamingMatcher::new(success_regex),
                tail: Vec::with_capacity(TRANSCRIPT_TAIL_BYTES),
            })),
        }
    }

    pub(crate) fn append(&self, chunk: &[u8]) {
        let mut state = self
            .state
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner);
        state.append(chunk);
    }

    fn snapshot(&self) -> QemuSuccessSnapshot {
        let state = self
            .state
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner);
        QemuSuccessSnapshot {
            matched: state.matcher.as_ref().is_ok_and(StreamingMatcher::is_match),
            matcher_error: state.matcher.as_ref().err().cloned(),
            tail: state.tail.clone(),
        }
    }
}

struct QemuSuccessOutputState {
    matcher: std::result::Result<StreamingMatcher, String>,
    tail: Vec<u8>,
}

impl QemuSuccessOutputState {
    fn append(&mut self, chunk: &[u8]) {
        if let Ok(matcher) = &mut self.matcher {
            matcher.append(chunk);
        }
        append_bounded_tail(&mut self.tail, chunk);
    }
}

struct StreamingMatcher {
    dfa: dense::DFA<Vec<u32>>,
    state: StateID,
    matched: bool,
}

impl StreamingMatcher {
    fn new(patterns: &[String]) -> std::result::Result<Self, String> {
        let dfa = dense::Builder::new()
            .build_many(patterns)
            .map_err(|err| err.to_string())?;
        let state = dfa
            .start_state_forward(&Input::new(b""))
            .map_err(|err| err.to_string())?;
        Ok(Self {
            dfa,
            state,
            matched: false,
        })
    }

    fn append(&mut self, chunk: &[u8]) {
        if self.matched {
            return;
        }
        for &byte in chunk {
            self.state = self.dfa.next_state(self.state, byte);
            if self.dfa.is_match_state(self.state) {
                self.matched = true;
                return;
            }
        }
    }

    fn is_match(&self) -> bool {
        self.matched || self.dfa.is_match_state(self.dfa.next_eoi_state(self.state))
    }
}

struct QemuSuccessSnapshot {
    matched: bool,
    matcher_error: Option<String>,
    tail: Vec<u8>,
}

fn append_bounded_tail(tail: &mut Vec<u8>, chunk: &[u8]) {
    if chunk.len() >= TRANSCRIPT_TAIL_BYTES {
        tail.clear();
        tail.extend_from_slice(&chunk[chunk.len() - TRANSCRIPT_TAIL_BYTES..]);
        return;
    }

    let overflow = tail
        .len()
        .saturating_add(chunk.len())
        .saturating_sub(TRANSCRIPT_TAIL_BYTES);
    if overflow > 0 {
        tail.drain(..overflow);
    }
    tail.extend_from_slice(chunk);
}

pub(crate) fn capture_required_success_output(
    success_regex: &[String],
    capture: Option<crate::backtrace::BacktraceQemuCapture>,
) -> (
    Option<crate::backtrace::BacktraceQemuCapture>,
    Option<QemuSuccessOutput>,
) {
    if success_regex.is_empty() {
        return (capture, None);
    }

    let success_output = QemuSuccessOutput::new(success_regex);
    let capture = match capture {
        Some(capture) => capture.with_success_output(success_output.clone()),
        None => crate::backtrace::BacktraceQemuCapture::success_output_only(success_output.clone()),
    };
    (Some(capture), Some(success_output))
}

pub(crate) fn verify_qemu_success_contract(
    run_result: Result<()>,
    success_output: Option<&QemuSuccessOutput>,
) -> Result<()> {
    run_result?;
    let Some(success_output) = success_output else {
        return Ok(());
    };

    let snapshot = success_output.snapshot();
    if let Some(err) = snapshot.matcher_error {
        bail!("failed to compile QEMU success regex set: {err}");
    }
    if snapshot.matched {
        return Ok(());
    }

    let tail = String::from_utf8_lossy(&snapshot.tail);
    bail!("QEMU stopped without matching a configured success regex; transcript tail:\n{tail}")
}

#[cfg(test)]
mod tests {
    use super::{QemuSuccessOutput, TRANSCRIPT_TAIL_BYTES, verify_qemu_success_contract};

    fn captured_output(patterns: &[&str], chunks: &[&[u8]]) -> QemuSuccessOutput {
        let patterns = patterns.iter().map(ToString::to_string).collect::<Vec<_>>();
        let output = QemuSuccessOutput::new(&patterns);
        for chunk in chunks {
            output.append(chunk);
        }
        output
    }

    #[test]
    fn exit_zero_without_required_success_marker_is_an_error() {
        let output = captured_output(
            &[r"(?m)^STARRY_GROUPED_TESTS_PASSED\s*$"],
            &[b"guest exited before completing the suite\n"],
        );
        let err = verify_qemu_success_contract(Ok(()), Some(&output)).unwrap_err();

        assert!(
            err.to_string()
                .contains("without matching a configured success regex")
        );
    }

    #[test]
    fn configured_success_marker_is_accepted() {
        let output = captured_output(
            &[r"(?m)^STARRY_GROUPED_TESTS_PASSED\s*$"],
            &[b"booting\nSTARRY_GROUPED_TESTS_PASSED\n"],
        );
        verify_qemu_success_contract(Ok(()), Some(&output)).unwrap();
    }

    #[test]
    fn empty_success_contract_preserves_normal_exit() {
        verify_qemu_success_contract(Ok(()), None).unwrap();
    }

    #[test]
    fn runner_error_takes_precedence_over_missing_marker() {
        let output = captured_output(&["PASS"], &[]);
        let err = verify_qemu_success_contract(Err(anyhow::anyhow!("QEMU timeout")), Some(&output))
            .unwrap_err();

        assert_eq!(err.to_string(), "QEMU timeout");
    }

    #[test]
    fn streaming_matcher_preserves_chunk_boundaries() {
        let output = captured_output(
            &[r"(?m)^STARRY_GROUPED_TESTS_PASSED$"],
            &[b"STARRY_GROUPED_", b"TESTS_PASSED\n"],
        );
        verify_qemu_success_contract(Ok(()), Some(&output)).unwrap();
    }

    #[test]
    fn sustained_noise_keeps_only_the_bounded_diagnostic_tail() {
        let output = captured_output(&["PASS"], &[]);
        let chunk = [b'x'; 8192];

        for _ in 0..1024 {
            output.append(&chunk);
        }

        let snapshot = output.snapshot();
        assert_eq!(snapshot.tail.len(), TRANSCRIPT_TAIL_BYTES);
        assert!(!snapshot.matched);
    }

    #[test]
    fn runner_error_takes_precedence_over_invalid_success_regex() {
        let output = captured_output(&["("], &[]);

        let err = verify_qemu_success_contract(Err(anyhow::anyhow!("QEMU timeout")), Some(&output))
            .unwrap_err();

        assert_eq!(err.to_string(), "QEMU timeout");
    }

    #[test]
    fn success_only_output_does_not_enable_backtrace_block_capture() {
        let output = captured_output(&["PASS"], &[]);
        let capture = crate::backtrace::BacktraceQemuCapture::success_output_only(output);

        assert!(!capture.captures_backtrace_blocks());
    }
}