moonlight-cli 0.1.2

Compare command behavior against one or two reference commands.
Documentation
use crate::eval_config::CheckConfig;
use anyhow::Context;
use bytes::Bytes;
use moonlight_core::{compare::capture_body, target::CapturedTarget};
use regex::Regex;

pub(super) struct PreparedEvalCheck {
    pub(super) config: CheckConfig,
    stdout_patterns: Vec<Regex>,
    stderr_patterns: Vec<Regex>,
}

pub(super) fn prepare_checks(checks: Vec<CheckConfig>) -> anyhow::Result<Vec<PreparedEvalCheck>> {
    checks.into_iter().map(PreparedEvalCheck::new).collect()
}

impl PreparedEvalCheck {
    fn new(config: CheckConfig) -> anyhow::Result<Self> {
        let stdout_patterns = compile_patterns(
            &config.normalize_stdout_patterns,
            &format!("invalid normalize stdout pattern in check {}", config.id),
        )?;
        let stderr_patterns = compile_patterns(
            &config.normalize_stderr_patterns,
            &format!("invalid normalize stderr pattern in check {}", config.id),
        )?;

        Ok(Self {
            config,
            stdout_patterns,
            stderr_patterns,
        })
    }
}

pub(super) fn normalize_target(
    target: &mut CapturedTarget,
    check: &PreparedEvalCheck,
    max_body_capture_bytes: usize,
) -> anyhow::Result<()> {
    if check.config.ignore_stdout {
        replace_stdout(target, Bytes::new(), max_body_capture_bytes);
    } else if !check.stdout_patterns.is_empty() {
        let bytes = normalize_bytes(&target.body_bytes, &check.stdout_patterns);
        replace_stdout(target, bytes, max_body_capture_bytes);
    }

    if let Some(stderr) = &target.observation.stderr {
        if !check.stderr_patterns.is_empty() {
            let bytes = normalize_bytes(&target.stderr_bytes, &check.stderr_patterns);
            target.stderr_bytes = bytes.clone();
            target.observation.stderr = Some(capture_body(&bytes, max_body_capture_bytes));
        } else {
            target.observation.stderr = Some(stderr.clone());
        }
    }
    Ok(())
}

fn replace_stdout(target: &mut CapturedTarget, bytes: Bytes, max_body_capture_bytes: usize) {
    target.body_bytes = bytes.clone();
    target.observation.body = capture_body(&bytes, max_body_capture_bytes);
}

fn normalize_bytes(bytes: &Bytes, patterns: &[Regex]) -> Bytes {
    let mut value = String::from_utf8_lossy(bytes).to_string();
    for pattern in patterns {
        value = pattern.replace_all(&value, "<normalized>").to_string();
    }
    Bytes::from(value)
}

fn compile_patterns(patterns: &[String], context: &str) -> anyhow::Result<Vec<Regex>> {
    patterns
        .iter()
        .map(|pattern| Regex::new(pattern).with_context(|| context.to_string()))
        .collect()
}

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

    #[test]
    fn normalization_replaces_matching_output() {
        let patterns = compile_patterns(
            &["[0-9]+ms".to_string(), "/tmp/[^ ]+".to_string()],
            "test pattern",
        )
        .unwrap();
        let output = normalize_bytes(&Bytes::from("built in 123ms at /tmp/demo"), &patterns);

        assert_eq!(
            String::from_utf8(output.to_vec()).unwrap(),
            "built in <normalized> at <normalized>"
        );
    }
}