Skip to main content

camel_integration_test/document/
logs.rs

1//! The document-level `logs:` assertion grammar (rc-p1x2a, split out
2//! of the parent module; mirrors the `document/error.rs` pattern).
3//!
4//! `LogsAssertion` and `LogLevel` are re-exported at
5//! `crate::document` and the crate root, so consumers keep the paths
6//! they had before the split.
7
8use noyalib::compat::serde_yaml;
9use serde::Deserialize;
10
11use super::DocError;
12
13/// The document-level `logs:` assertion block (rc-tdgh5): log-content
14/// expectations the runner evaluates against the capture window that
15/// spans the document run. Conjunction across clauses — every entry of
16/// every list must hold; `None`-valued clauses assert nothing.
17#[derive(Debug, Clone, PartialEq)]
18pub struct LogsAssertion {
19    /// Substring markers: each entry must appear in at least one
20    /// captured event's message.
21    pub contains: Vec<String>,
22    /// Unanchored patterns: each entry must match at least one
23    /// captured event's message. Every pattern compiles at load time;
24    /// a non-compiling pattern is a load error.
25    pub regex: Vec<String>,
26    /// Severity ceiling: no captured event may carry a level above
27    /// this cap. `None` asserts nothing about levels.
28    pub no_level_above: Option<LogLevel>,
29}
30
31/// A `noLevelAbove` severity. The grammar accepts exactly
32/// `trace|debug|info|warn|error`.
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum LogLevel {
35    /// Below `debug`.
36    Trace,
37    /// Below `info`.
38    Debug,
39    /// Below `warn`.
40    Info,
41    /// Below `error`.
42    Warn,
43    /// The most severe level.
44    Error,
45}
46
47/// Raw `logs:` block (rc-tdgh5): keys and level stay raw so the
48/// clause walk can name the offending entry; conversion happens during
49/// validation, never at the serde layer.
50#[derive(Deserialize)]
51#[serde(deny_unknown_fields, rename_all = "camelCase")]
52struct RawLogs {
53    contains: Option<Vec<String>>,
54    regex: Option<Vec<String>>,
55    no_level_above: Option<String>,
56}
57
58/// Converts the raw `logs:` node (rc-tdgh5). Malformed blocks are load
59/// errors through [`DocError::LogsBlock`]: an unknown key, a level
60/// outside `trace|debug|info|warn|error`, or a regex that does not
61/// compile — each error names the offending clause.
62pub(super) fn logs_from_raw(value: serde_yaml::Value) -> Result<LogsAssertion, DocError> {
63    let block_error = |detail: String| DocError::LogsBlock { detail };
64    let raw: RawLogs = serde_yaml::from_value(value).map_err(|e| block_error(e.to_string()))?;
65    let no_level_above = raw
66        .no_level_above
67        .as_deref()
68        .map(|raw_level| match raw_level {
69            "trace" => Ok(LogLevel::Trace),
70            "debug" => Ok(LogLevel::Debug),
71            "info" => Ok(LogLevel::Info),
72            "warn" => Ok(LogLevel::Warn),
73            "error" => Ok(LogLevel::Error),
74            other => Err(block_error(format!(
75                "`logs.noLevelAbove` must be one of trace|debug|info|warn|error, got `{other}`"
76            ))),
77        })
78        .transpose()?;
79    for pattern in raw.regex.iter().flatten() {
80        // Compile-time gate: the runner matches unanchored, so a
81        // pattern that compiles here always compiles there.
82        if let Err(error) = regex::Regex::new(pattern) {
83            return Err(block_error(format!(
84                "`logs.regex` entry `{pattern}` does not compile: {error}"
85            )));
86        }
87    }
88    Ok(LogsAssertion {
89        contains: raw.contains.unwrap_or_default(),
90        regex: raw.regex.unwrap_or_default(),
91        no_level_above,
92    })
93}