mod common;
use camel_integration_test::{DocError, ScenarioVerdict, parse_scenario_document};
use common::{lock_run, run_logs_document};
const MARKER_INFO_DOC: &str = r#"
routeFiles: [routes.yaml]
scenario:
- send:
to: direct:start
body: cache served HIT
logs:
contains: ['cache served HIT']
"#;
const MARKER_MISS_DOC: &str = r#"
routeFiles: [routes.yaml]
scenario:
- send:
to: direct:start
body: cache served HIT
logs:
contains: ['cache served Miss']
"#;
const REGEX_DOC: &str = r#"
routeFiles: [routes.yaml]
scenario:
- send:
to: direct:start
body: processor 7 emitted order
logs:
regex: ['processor .*emitted']
"#;
const CLEAN_LEVEL_DOC: &str = r#"
routeFiles: [routes.yaml]
scenario:
- send:
to: direct:start
body: cache served HIT
logs:
contains: ['cache served HIT']
noLevelAbove: info
"#;
const ROUTE_WARN_DOC: &str = r#"
routeFiles: [routes.yaml]
scenario:
- send:
to: direct:start
body: route warn marker body
logs:
noLevelAbove: info
"#;
const MULTI_THREAD_DOC: &str = r#"
routeFiles: [routes.yaml]
scenario:
- sleep: {duration: 250ms}
- send:
to: direct:start
body: route warn marker body
logs:
noLevelAbove: info
"#;
const EMPTY_WINDOW_DOC: &str = r#"
routeFiles: [routes.yaml]
scenario:
- sleep: {duration: 10ms}
logs:
noLevelAbove: warn
"#;
const OUTSIDE_DOC: &str = r#"
routeFiles: [routes.yaml]
scenario:
- send:
to: direct:start
body: cache served HIT
logs:
contains: ['outside marker']
"#;
const SEQUENTIAL_A_DOC: &str = r#"
routeFiles: [routes.yaml]
scenario:
- send:
to: direct:start
body: alpha marker A
logs:
contains: ['alpha marker A']
"#;
const SEQUENTIAL_B_DOC: &str = r#"
routeFiles: [routes.yaml]
scenario:
- send:
to: direct:start
body: beta marker B
logs:
contains: ['beta marker B']
"#;
#[allow(clippy::await_holding_lock)] #[tokio::test]
async fn contains_passes_on_window_event() {
let _guard = lock_run();
let outcome = run_logs_document(MARKER_INFO_DOC, "marker-info.routes.yaml").await;
assert_eq!(
outcome.verdict,
Some(ScenarioVerdict::Pass),
"the marker must pass: {outcome:?}"
);
assert!(outcome.logs_failure.is_none());
}
#[allow(clippy::await_holding_lock)] #[tokio::test]
async fn contains_fails_without_match() {
let _guard = lock_run();
let outcome = run_logs_document(MARKER_MISS_DOC, "marker-info.routes.yaml").await;
assert_eq!(outcome.verdict, None, "the miss must fail: {outcome:?}");
let failure = outcome
.logs_failure
.as_deref()
.expect("the diagnostic must be present");
assert!(
failure.contains("`logs.contains` entry `cache served Miss` matched no captured event"),
"the diagnostic must name the entry: {failure}"
);
}
#[allow(clippy::await_holding_lock)] #[tokio::test]
async fn regex_matches_window_event() {
let _guard = lock_run();
let outcome = run_logs_document(REGEX_DOC, "processor-marker.routes.yaml").await;
assert_eq!(
outcome.verdict,
Some(ScenarioVerdict::Pass),
"the unanchored regex must match: {outcome:?}"
);
assert!(outcome.logs_failure.is_none());
}
#[allow(clippy::await_holding_lock)] #[tokio::test]
async fn no_level_above_clean_window_passes() {
let _guard = lock_run();
let outcome = run_logs_document(CLEAN_LEVEL_DOC, "marker-info.routes.yaml").await;
assert_eq!(
outcome.verdict,
Some(ScenarioVerdict::Pass),
"an INFO-only window must satisfy the info ceiling: {outcome:?}"
);
assert!(outcome.logs_failure.is_none());
}
#[allow(clippy::await_holding_lock)] #[tokio::test]
async fn no_level_above_fails_on_route_warn() {
let _guard = lock_run();
let outcome = run_logs_document(ROUTE_WARN_DOC, "marker-warn.routes.yaml").await;
assert_eq!(
outcome.verdict, None,
"the route warn must fail: {outcome:?}"
);
let failure = outcome
.logs_failure
.as_deref()
.expect("the diagnostic must be present");
assert!(
failure.contains("`logs.noLevelAbove`"),
"the diagnostic must name the clause: {failure}"
);
assert!(failure.contains("WARN"), "level named: {failure}");
assert!(
failure.contains("camel_component_log"),
"camel-log target named: {failure}"
);
assert!(
failure.contains("route warn marker body"),
"marker message named: {failure}"
);
}
#[allow(clippy::await_holding_lock)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn multi_thread_document_fails_on_warn() {
let _guard = lock_run();
let (done, mut running) = tokio::sync::oneshot::channel::<()>();
let noise = tokio::spawn(async move {
loop {
tracing::warn!("harness task warn marker");
tokio::time::sleep(std::time::Duration::from_millis(5)).await;
if matches!(
running.try_recv(),
Err(tokio::sync::oneshot::error::TryRecvError::Closed)
) {
break;
}
}
});
let outcome = run_logs_document(MULTI_THREAD_DOC, "marker-warn.routes.yaml").await;
drop(done);
let _ = noise.await;
assert_eq!(outcome.verdict, None, "both warns must fail: {outcome:?}");
let failure = outcome
.logs_failure
.as_deref()
.expect("the diagnostic must be present");
assert!(
failure.contains("harness task warn marker"),
"the spawned-task warn must be named: {failure}"
);
assert!(
failure.contains("route warn marker body"),
"the route warn must be named: {failure}"
);
}
#[allow(clippy::await_holding_lock)] #[tokio::test]
async fn no_level_above_empty_window_passes() {
let _guard = lock_run();
let outcome = run_logs_document(EMPTY_WINDOW_DOC, "marker-warn.routes.yaml").await;
assert_eq!(
outcome.verdict,
Some(ScenarioVerdict::Pass),
"an empty window satisfies the ceiling vacuously: {outcome:?}"
);
assert!(outcome.logs_failure.is_none());
}
#[allow(clippy::await_holding_lock)] #[tokio::test]
async fn outside_window_never_satisfies() {
let _guard = lock_run();
tracing::info!("outside marker");
let outcome = run_logs_document(OUTSIDE_DOC, "marker-info.routes.yaml").await;
assert_eq!(outcome.verdict, None, "the miss must fail: {outcome:?}");
let failure = outcome
.logs_failure
.as_deref()
.expect("the diagnostic must be present");
assert!(
failure.contains("`logs.contains` entry `outside marker` matched no captured event"),
"the diagnostic must name the entry: {failure}"
);
}
#[allow(clippy::await_holding_lock)] #[tokio::test]
async fn sequential_documents_capture_persists() {
let _guard = lock_run();
let outcome_a = run_logs_document(SEQUENTIAL_A_DOC, "marker-info.routes.yaml").await;
assert_eq!(
outcome_a.verdict,
Some(ScenarioVerdict::Pass),
"document A must pass: {outcome_a:?}"
);
let outcome_b = run_logs_document(SEQUENTIAL_B_DOC, "marker-info.routes.yaml").await;
assert_eq!(
outcome_b.verdict,
Some(ScenarioVerdict::Pass),
"document B must pass after re-boot: {outcome_b:?}"
);
}
#[test]
fn malformed_logs_block_load_errors() {
let _guard = lock_run();
let cases: &[(&str, &str)] = &[
(
r#"
routeFiles: [routes.yaml]
scenario:
- send:
to: direct:start
logs:
contains: ['x']
bogus: 1
"#,
"bogus",
),
(
r#"
routeFiles: [routes.yaml]
scenario:
- send:
to: direct:start
logs:
noLevelAbove: verbose
"#,
"verbose",
),
(
r#"
routeFiles: [routes.yaml]
scenario:
- send:
to: direct:start
logs:
regex: ['[']
"#,
"`[`",
),
];
for (text, named) in cases {
let dir = tempfile::tempdir().expect("temp dir");
let path = dir.path().join("case.test.yaml");
std::fs::write(&path, text).expect("write case file");
let err = parse_scenario_document(&path)
.expect_err("a malformed logs block must be a load error");
assert!(
matches!(err, DocError::LogsBlock { .. }),
"expected LogsBlock, got {err}"
);
assert!(
err.to_string().contains(named),
"the error must name the offending clause {named}: {err}"
);
}
}