use std::path::PathBuf;
use std::time::Duration;
use super::*;
use crate::core::trusty_tools_config::LOG_DRAIN_DEFAULT_INTERVAL_SECS;
use crate::daemon::log_drain::LogDrainDestinationStatus;
use trusty_common::log_drain::{DestinationUri, DrainTarget};
const FIXTURE_PROJECT: &str = "octocat/fixtures";
fn group(path: &str) -> crate::core::trusty_tools_config::ResolvedDrainDestination {
crate::core::trusty_tools_config::ResolvedDrainDestination {
destination: DestinationUri::File {
path: PathBuf::from(path),
},
destination_display: format!("file://{path}"),
target: DrainTarget {
owner: "octocat".to_string(),
project: "fixtures".to_string(),
},
sources: Vec::new(),
}
}
fn plan_over(
destinations: Vec<crate::core::trusty_tools_config::ResolvedDrainDestination>,
) -> LogDrainSetting {
plan_with_disabled(destinations, Vec::new())
}
fn plan_with_disabled(
destinations: Vec<crate::core::trusty_tools_config::ResolvedDrainDestination>,
disabled: Vec<crate::core::trusty_tools_config::DisabledSource>,
) -> LogDrainSetting {
LogDrainSetting::Enabled(Box::new(
crate::core::trusty_tools_config::ResolvedLogDrain {
destinations,
disabled,
interval: Duration::from_secs(LOG_DRAIN_DEFAULT_INTERVAL_SECS),
max_file_bytes: 1024,
max_wire_bytes: 1024,
secrets: Vec::new(),
prune_after_upload: true,
prune_retention: Duration::from_secs(30 * 86_400),
},
))
}
fn plan() -> LogDrainSetting {
plan_over(vec![group("/tmp/drain")])
}
fn recorded(path: &str, outcome: DrainOutcome, detail: &str) -> LogDrainDestinationStatus {
LogDrainDestinationStatus {
destination: format!("file://{path}"),
project: FIXTURE_PROJECT.to_string(),
scheme: "file".to_string(),
outcome,
uploaded: 3,
skipped_unchanged: 1,
pruned: 0,
detail: detail.to_string(),
}
}
fn status(outcome: DrainOutcome, detail: &str) -> LogDrainStatus {
status_over(
outcome,
vec![recorded("/tmp/drain", outcome, detail)],
detail,
)
}
fn status_over(
outcome: DrainOutcome,
destinations: Vec<LogDrainDestinationStatus>,
detail: &str,
) -> LogDrainStatus {
LogDrainStatus {
outcome,
at: "2026-09-01T00:00:00Z".to_string(),
destinations,
uploaded: 3,
skipped_unchanged: 1,
pruned: 0,
detail: detail.to_string(),
}
}
#[test]
fn config_error_fails() {
let err = LogDrainConfigError::MissingDestination;
let check = build_log_drain_check(Err(&err), None);
assert_eq!(check.name, "log_drain");
assert_eq!(check.status, CheckStatus::Fail);
assert!(
check.message.contains("destination is unset"),
"message must quote the config error: {}",
check.message
);
}
#[test]
fn disabled_is_ok() {
let check = build_log_drain_check(Ok(&LogDrainSetting::Disabled), None);
assert_eq!(check.status, CheckStatus::Ok);
assert!(check.message.contains("disabled"), "{}", check.message);
}
#[test]
fn enabled_with_no_run_warns() {
let setting = plan();
let check = build_log_drain_check(Ok(&setting), None);
assert_eq!(check.status, CheckStatus::Warn);
assert!(
check
.message
.contains("file → file:///tmp/drain [octocat/fixtures]"),
"the row must name the scheme, destination, and project: {}",
check.message
);
assert!(check.message.contains("no run"), "{}", check.message);
}
#[test]
fn a_successful_run_is_ok() {
let setting = plan();
let recorded = status(DrainOutcome::Success, "3 file(s) uploaded");
let check = build_log_drain_check(Ok(&setting), Some(&recorded));
assert_eq!(check.status, CheckStatus::Ok);
assert!(
check.message.contains("3 file(s) uploaded"),
"{}",
check.message
);
}
#[test]
fn a_failed_run_fails() {
let setting = plan();
let recorded = status(DrainOutcome::Failed, "cannot reach the destination: nope");
let check = build_log_drain_check(Ok(&setting), Some(&recorded));
assert_eq!(check.status, CheckStatus::Fail);
assert!(
check.message.contains("FAILED"),
"the row must say so out loud: {}",
check.message
);
assert!(
check.message.contains("cannot reach the destination"),
"{}",
check.message
);
}
#[test]
fn the_row_lists_every_destination_with_its_own_outcome() {
let setting = plan_over(vec![group("/tmp/drain-a"), group("/tmp/drain-b")]);
let recorded_status = status_over(
DrainOutcome::Failed,
vec![
recorded("/tmp/drain-a", DrainOutcome::Success, "2 file(s) uploaded"),
recorded(
"/tmp/drain-b",
DrainOutcome::Failed,
"cannot reach the destination: no such directory",
),
],
"aggregate detail nobody should be reading here",
);
let check = build_log_drain_check(Ok(&setting), Some(&recorded_status));
assert_eq!(check.status, CheckStatus::Fail);
assert!(
check.message.contains(
"file → file:///tmp/drain-a [octocat/fixtures], \
file → file:///tmp/drain-b [octocat/fixtures]"
),
"the row must name every configured pass: {}",
check.message
);
assert!(
check
.message
.contains("file:///tmp/drain-a [octocat/fixtures]: ok — 2 file(s) uploaded"),
"the healthy destination keeps its own verdict: {}",
check.message
);
assert!(
check
.message
.contains("file:///tmp/drain-b [octocat/fixtures]: FAILED — cannot reach"),
"the broken destination is named as the broken one: {}",
check.message
);
}
#[test]
fn a_record_predating_per_destination_outcomes_still_reads() {
let setting = plan();
let recorded_status = status_over(DrainOutcome::Success, Vec::new(), "3 file(s) uploaded");
let check = build_log_drain_check(Ok(&setting), Some(&recorded_status));
assert_eq!(check.status, CheckStatus::Ok);
assert!(
check.message.contains("3 file(s) uploaded"),
"{}",
check.message
);
}
#[test]
fn a_stale_disabled_record_under_an_enabled_config_warns() {
let setting = plan();
let recorded = status(DrainOutcome::SkippedDisabled, "log_drain is disabled");
let check = build_log_drain_check(Ok(&setting), Some(&recorded));
assert_eq!(check.status, CheckStatus::Warn);
assert!(check.message.contains("no run"), "{}", check.message);
}
#[test]
fn the_row_lists_a_disabled_source() {
let setting = plan_with_disabled(
vec![group("/tmp/drain")],
vec![crate::core::trusty_tools_config::DisabledSource {
crate_name: "trusty-code".to_string(),
destination_display: Some("s3://owner-bucket/live".to_string()),
}],
);
let check = build_log_drain_check(Ok(&setting), None);
assert!(
check
.message
.contains("trusty-code → s3://owner-bucket/live: disabled"),
"the row must name the opted-out project and where it would have gone: {}",
check.message
);
}