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}