#[allow(unused_imports)]
use super::super::*;
#[allow(unused_imports)]
use super::support::*;
#[tokio::test]
async fn cmd_drift_refresh_reloads_tf_state_and_pins_status() {
let mut app = test_app();
app.environments = vec![mk_env("prod-api", "shop", "Web", "Green")];
app.rebuild_view();
app.execute_command("drift refresh");
let msg = app.status_message.as_deref().unwrap_or("");
assert!(
msg.contains("tfstate"),
"expected tfstate status, got: {msg}"
);
}
#[tokio::test]
async fn cmd_drift_with_no_tfstate_loaded_hints_at_discovery() {
let mut app = test_app();
app.environments = vec![mk_env("prod-api", "shop", "Web", "Green")];
app.rebuild_view();
app.table_state.select(Some(0));
app.tf_state = None;
app.execute_command("drift");
let msg = app.status_message.as_deref().unwrap_or("");
assert!(
msg.contains("no terraform.tfstate found"),
"expected discovery hint, got: {msg}"
);
}
#[test]
fn render_lint_overlay_empty_shows_clean_stub() {
let body = crate::app::cmd_misc::render_lint_overlay("prod-api", &[], &[]);
assert!(body.contains("prod-api"));
assert!(body.contains("✓ No issues found"));
assert!(body.contains("esc / q to close"));
}
#[test]
fn render_lint_overlay_with_issues_renders_per_severity_glyph() {
use crate::lint::{Issue, Severity};
use std::collections::BTreeMap;
let issues = vec![
Issue {
rule_id: "EBL001".into(),
severity: Severity::Warn,
env_name: Some("prod".into()),
title: "AllAtOnce on 4-instance env".into(),
detail: "Deployment policy AllAtOnce with MaxSize=4 means full unavailability.".into(),
suggestion: Some(":deployment-policy Rolling".into()),
fields: BTreeMap::new(),
},
Issue {
rule_id: "EBL005".into(),
severity: Severity::Info,
env_name: Some("prod".into()),
title: "Single-instance env".into(),
detail: "MinSize=MaxSize=1.".into(),
suggestion: None,
fields: BTreeMap::new(),
},
];
let body = crate::app::cmd_misc::render_lint_overlay("prod-api", &issues, &[]);
assert!(body.contains("⚠ [EBL001]"));
assert!(body.contains("· [EBL005]"));
assert!(body.contains("→ :deployment-policy Rolling"));
assert!(body.contains(" Deployment policy AllAtOnce"));
assert!(body.contains("2 issues found"));
}
#[test]
fn ebl010_tells_an_untagged_env_from_an_unloaded_one() {
use crate::lint::LintContext;
let env = mk_env("api-prod", "poly", "Web", "Green");
let opts: Vec<(String, String, String)> = Vec::new();
let required = vec!["Owner".to_string(), "CostCentre".to_string()];
let rules = crate::lint::default_rules(&[]);
let ctx = LintContext::for_env(&env, &opts).with_required_tags(&required);
assert!(
!crate::lint::run_rules(&rules, &ctx)
.iter()
.any(|i| i.rule_id == "EBL010"),
"unloaded tags must not fire"
);
let none_at_all: Vec<String> = Vec::new();
let ctx = LintContext::for_env(&env, &opts)
.with_required_tags(&required)
.with_env_tag_keys(&none_at_all);
let issue = crate::lint::run_rules(&rules, &ctx)
.into_iter()
.find(|i| i.rule_id == "EBL010")
.expect("an env with no tags at all must fire");
assert!(issue.detail.contains("Owner"), "{}", issue.detail);
assert!(issue.detail.contains("CostCentre"), "{}", issue.detail);
let all = vec!["Owner".to_string(), "CostCentre".to_string()];
let ctx = LintContext::for_env(&env, &opts)
.with_required_tags(&required)
.with_env_tag_keys(&all);
assert!(!crate::lint::run_rules(&rules, &ctx)
.iter()
.any(|i| i.rule_id == "EBL010"));
}
#[test]
fn no_lint_caller_flattens_a_failed_tag_fetch_into_an_empty_list() {
let mut bindings_seen = 0usize;
for (path, full) in super::scan::source_files() {
if super::scan::is_test_path(&path) {
continue;
}
let src = super::scan::production_half(&full);
let name = path.as_str();
let code: String = src
.lines()
.map(super::scan::strip_line_comment)
.collect::<Vec<_>>()
.join("\n");
let lines: Vec<&str> = code.lines().collect();
for (n, line) in lines.iter().enumerate() {
if !(line.contains("env_tag_keys") && line.contains('=')) {
continue;
}
let mut expr = String::new();
for l in &lines[n..] {
expr.push_str(l);
if l.trim_end().ends_with(';') {
break;
}
}
bindings_seen += 1;
assert!(
!expr.contains("unwrap_or_default"),
"{name}:{} flattens the tag-fetch failure into an empty list, \
which makes EBL010 fire instead of skip: {}",
n + 1,
expr.trim()
);
}
}
assert!(
bindings_seen > 0,
"no `env_tag_keys` binding found anywhere — the scan is looking at nothing"
);
}
#[test]
fn lint_overlay_never_shows_a_clean_result_over_checks_that_did_not_run() {
let warnings = vec![
"EBL012 could not be evaluated for prod-api: DescribeEnvironmentHealth failed: AccessDenied"
.to_string(),
];
let body = crate::app::cmd_misc::render_lint_overlay("prod-api", &[], &warnings);
assert!(
!body.contains('✓'),
"no check-mark over a check that did not run: {body}"
);
assert!(body.contains("could NOT run"), "{body}");
assert!(body.contains("EBL012"), "names the check: {body}");
}
#[test]
fn lint_overlay_lists_lost_coverage_beside_findings() {
let issue = crate::lint::Issue {
rule_id: "EBL001".into(),
severity: crate::lint::Severity::Warn,
env_name: Some("prod-api".into()),
title: "EBL001 fired".into(),
detail: String::new(),
suggestion: None,
fields: Default::default(),
};
let warnings = vec!["EBL010 could not be evaluated for prod-api: throttled".to_string()];
let body = crate::app::cmd_misc::render_lint_overlay("prod-api", &[issue], &warnings);
assert!(body.contains("EBL001 fired"), "{body}");
assert!(body.contains("EBL010 could not be evaluated"), "{body}");
}
#[test]
fn the_tui_lint_paths_use_the_shared_assembly() {
let runner = super::scan::production_source("app/tui_lint.rs");
let body = runner
.split("pub(crate) async fn run_tui_lint(")
.nth(1)
.unwrap_or_else(|| panic!("run_tui_lint not found"));
assert!(
body.contains("fetch_env_lint_inputs(") && body.contains("snap.finish(inputs"),
"run_tui_lint must fetch through the shared assembly and finish from the snapshot"
);
for (file, func, run) in [
("app/cmd_misc.rs", "fn cmd_lint(", "run_tui_lint("),
(
"app/cmd_inspect.rs",
"fn cmd_explain_issue(",
"run_tui_lint(",
),
(
"app/spawn_deploy.rs",
"fn spawn_confirm_lint(",
"snap.finish_fetched(",
),
] {
let prod = super::scan::production_source(file);
let body = prod
.split(func)
.nth(1)
.and_then(|rest| rest.split("\n }\n").next())
.unwrap_or_else(|| panic!("{file}: `{func}` not found"));
assert!(
body.contains("self.lint_snapshot(&env)"),
"{file} `{func}` must take the shared lint snapshot"
);
assert!(
body.contains(run),
"{file} `{func}` must complete its inputs from the snapshot ({run})"
);
assert!(
!body.contains("LintContext::for_env("),
"{file} `{func}` builds its own LintContext — a private copy of the \
assembly again"
);
for cache in ["latest_stacks", "worker_dlq_depths", "lint_disable"] {
assert!(
!body.contains(cache),
"{file} `{func}` reads `{cache}` itself instead of through the snapshot"
);
}
}
}
#[test]
fn a_cached_input_that_is_missing_is_reported_not_read_as_clean() {
use crate::app::tui_lint::{dlq_depth_gap, platforms_from_cache, worker_dlq, WorkerDlq};
use crate::lint::inputs::{explain_verdict, input_gaps, ExplainVerdict, Platforms};
let mut web = mk_env("api", "poly", "WebServer", "Green");
web.solution_stack = "64bit Amazon Linux 2023 v4.1.0 running Corretto 17".into();
let worker = mk_env("jobs", "poly", "Worker", "Green");
let stacks: std::collections::HashMap<String, String> =
[("Java".to_string(), "4.1".to_string())]
.into_iter()
.collect();
let loaded = platforms_from_cache(&stacks, None, "us-east-1", "us-east-1");
let empty = std::collections::HashMap::new();
let waiting = platforms_from_cache(&empty, None, "us-east-1", "us-east-1");
let gaps = input_gaps(&web, &[], &[], &waiting, None, None, &[]);
assert_eq!(gaps.len(), 1, "{gaps:?}");
assert!(
gaps[0].starts_with("EBL008 could not be evaluated for api"),
"{gaps:?}"
);
assert!(gaps[0].contains("has not loaded yet"), "{gaps:?}");
assert!(matches!(
explain_verdict("EBL008", &[], &gaps),
ExplainVerdict::NotEvaluated(_)
));
let denied = platforms_from_cache(
&empty,
Some("ListAvailableSolutionStacks failed: AccessDenied"),
"us-east-1",
"us-east-1",
);
let gaps = input_gaps(&web, &[], &[], &denied, None, None, &[]);
assert!(
gaps[0].contains("AccessDenied"),
"a failure names itself: {gaps:?}"
);
assert!(!gaps[0].contains("not loaded"), "{gaps:?}");
assert!(input_gaps(&web, &[], &[], &loaded, None, None, &[]).is_empty());
let off = vec!["EBL008".to_string()];
assert!(input_gaps(&web, &off, &[], &waiting, None, None, &[]).is_empty());
let Platforms::Unavailable(why) = platforms_from_cache(&stacks, None, "us-east-1", "eu-west-2")
else {
panic!("another region's env must not read the home catalogue as loaded");
};
assert!(
why.contains("us-east-1") && why.contains("eu-west-2"),
"{why}"
);
let mut custom = web.clone();
custom.solution_stack = String::new();
assert!(
input_gaps(&custom, &[], &[], &waiting, None, None, &[]).is_empty(),
"EBL008 cannot apply to an env with no versioned platform"
);
assert!(matches!(
worker_dlq(Some(3), false, false),
WorkerDlq::Depth(3)
));
assert!(matches!(worker_dlq(None, true, false), WorkerDlq::NoDlq));
for (depth, absent) in [(Some(3), false), (None, true), (None, false)] {
let WorkerDlq::Unknown(why) = worker_dlq(depth, absent, true) else {
panic!("stale must not be usable ({depth:?}, {absent})");
};
assert!(why.contains("failed"), "{why}");
}
let WorkerDlq::Unknown(why) = worker_dlq(None, false, false) else {
panic!("never checked is not 'no DLQ'");
};
assert!(why.contains("completed yet"), "{why}");
let unknown = WorkerDlq::Unknown("the last worker-queue check failed".into());
let gap = dlq_depth_gap(&worker, &[], &unknown).expect("a worker with no answer");
assert!(
gap.starts_with("EBL011 could not be evaluated for jobs"),
"{gap}"
);
assert!(dlq_depth_gap(&worker, &[], &WorkerDlq::NoDlq).is_none());
assert!(dlq_depth_gap(&worker, &[], &WorkerDlq::Depth(0)).is_none());
assert!(dlq_depth_gap(&web, &[], &unknown).is_none(), "not a worker");
assert!(dlq_depth_gap(&worker, &["EBL011".to_string()], &unknown).is_none());
}
#[test]
fn the_pre_deploy_lint_reports_a_failed_run() {
let prod = super::scan::production_source("app/spawn_deploy.rs");
let body = prod
.split("fn spawn_confirm_lint(")
.nth(1)
.and_then(|rest| rest.split("\n }\n").next())
.unwrap_or_else(|| panic!("spawn_confirm_lint not found"));
let code: String = body
.lines()
.map(super::scan::strip_line_comment)
.collect::<Vec<_>>()
.join("\n");
assert!(
!code.contains("Err(_) => Vec::new()"),
"a failed lint must not become an empty — i.e. clean — issue list"
);
assert!(
code.matches("lint could not run").count() >= 2,
"both whole-lint failure paths (client, option fetch) must carry a reason"
);
for call in [".list_tags(", ".fetch_env_instance_counts("] {
let at = code
.find(call)
.unwrap_or_else(|| panic!("spawn_confirm_lint no longer calls {call}"));
let tail = code[at..].split(';').next().unwrap_or_default();
assert!(
!tail.contains(".ok()"),
"{call} drops its error with `.ok()` — a failed fetch must be reported: {tail}"
);
}
let at = code
.find("snap.finish_fetched(")
.unwrap_or_else(|| panic!("spawn_confirm_lint no longer finishes through the snapshot"));
let args = code[at..].split(';').next().unwrap_or_default();
for arg in ["options", "tags_res", "health_res", "&disabled"] {
assert!(
args.contains(arg),
"finish_fetched is not given {arg}: {args}"
);
}
}
#[test]
fn only_the_shared_assembly_builds_a_lint_context() {
let mut found: Vec<(String, usize)> = Vec::new();
let mut scanned = 0usize;
for (path, full) in super::scan::source_files() {
if super::scan::is_test_path(&path) || path.contains("src/lint/") {
continue;
}
scanned += 1;
let prod = super::scan::production_half(&full);
let n = prod
.lines()
.map(super::scan::strip_line_comment)
.filter(|l| l.contains("LintContext::for_env("))
.count();
if n > 0 {
found.push((path, n));
}
}
assert!(scanned > 50, "scanned only {scanned} files");
assert!(
found.is_empty(),
"these build their own LintContext — a private copy of the lint assembly. \
Use `lint::inputs::run_rules_for_env`: {found:?}"
);
}
#[test]
fn a_failed_tag_or_health_fetch_is_listed_as_not_run() {
use crate::lint::inputs::{input_gaps, Platforms};
let env = fake_env("api-prod", "Ready", "Green", "v1");
let loaded = Platforms::Loaded(Default::default());
let tags = vec!["Owner".to_string()];
let gaps = input_gaps(
&env,
&[],
&tags,
&loaded,
Some("ListTagsForResource failed: AccessDenied"),
Some("DescribeEnvironmentHealth failed: Throttling"),
&[],
);
assert_eq!(gaps.len(), 2, "{gaps:?}");
assert!(gaps[0].starts_with("EBL010 could not be evaluated for api-prod"));
assert!(gaps[1].starts_with("EBL012 could not be evaluated for api-prod"));
for (gap, op) in gaps
.iter()
.zip(["ListTagsForResource", "DescribeEnvironmentHealth"])
{
assert_eq!(gap.matches(op).count(), 1, "{gap}");
}
}
#[test]
fn a_failed_fetch_is_not_listed_when_its_rule_could_not_have_fired() {
use crate::lint::inputs::{input_gaps, Platforms};
let env = fake_env("api-prod", "Ready", "Green", "v1");
let loaded = Platforms::Loaded(Default::default());
let basic = vec![(
"aws:elasticbeanstalk:healthreporting:system".to_string(),
"SystemType".to_string(),
"basic".to_string(),
)];
let (tags_err, health_err) = (Some("AccessDenied"), Some("Throttling"));
let quiet = input_gaps(&env, &[], &[], &loaded, tags_err, health_err, &basic);
assert!(quiet.is_empty(), "{quiet:?}");
let disabled = vec!["EBL010".to_string(), "EBL012".to_string()];
let owner = vec!["Owner".to_string()];
let off = input_gaps(&env, &disabled, &owner, &loaded, tags_err, health_err, &[]);
assert!(off.is_empty(), "{off:?}");
let clean = input_gaps(&env, &[], &owner, &loaded, None, None, &[]);
assert!(clean.is_empty(), "{clean:?}");
}
#[test]
fn a_failed_platform_listing_reaches_lint_as_a_gap_on_every_surface() {
use crate::lint::inputs::Platforms;
let failed: Result<Vec<String>, &str> = Err("ListAvailableSolutionStacks failed: AccessDenied");
let Platforms::Unavailable(why) = Platforms::from_listing(failed, str::to_string) else {
panic!("a failed listing is not a loaded one");
};
assert!(why.contains("AccessDenied"), "{why}");
let ok: Result<Vec<String>, &str> = Ok(vec![
"64bit Amazon Linux 2023 v4.1.0 running Corretto 17".to_string(),
]);
assert!(matches!(
Platforms::from_listing(ok, str::to_string),
Platforms::Loaded(m) if !m.is_empty()
));
let prod = super::scan::production_source("cli/explain.rs");
assert!(
prod.contains("Platforms::from_listing("),
"cli/explain.rs lists platforms for lint without `Platforms::from_listing`"
);
assert!(
!prod.contains("latest_stack_versions(") && !prod.contains("report_once("),
"explain is per env: it must not build the map itself, nor report the gap once"
);
}
#[tokio::test]
async fn the_lint_snapshot_reads_the_caches_as_lint_may_use_them() {
use crate::app::tui_lint::WorkerDlq;
use crate::lint::inputs::{EnvLintInputs, Platforms};
let mut app = test_app();
let worker = mk_env("jobs", "poly", "Worker", "Green");
app.latest_stacks_error = Some("ListAvailableSolutionStacks failed: AccessDenied".into());
let snap = app.lint_snapshot(&worker);
let Platforms::Unavailable(why) = &snap.platforms else {
panic!("an empty cache is not a loaded list");
};
assert!(why.contains("AccessDenied"), "{why}");
app.latest_stacks = [("Java".to_string(), "4.1".to_string())]
.into_iter()
.collect();
app.latest_stacks_error = None;
let mut abroad = mk_env("far", "poly", "WebServer", "Green");
abroad.region = Some("eu-west-2".into());
app.environments = vec![abroad.clone()];
let Platforms::Unavailable(why) = &app.lint_snapshot(&abroad).platforms else {
panic!("another region's env read the home catalogue as loaded");
};
assert!(why.contains("eu-west-2"), "{why}");
assert!(matches!(
app.lint_snapshot(&worker).platforms,
Platforms::Loaded(_)
));
let mut twin = abroad.clone();
twin.region = Some(app.context.region.clone());
app.environments = vec![twin.clone(), abroad.clone()];
assert!(
matches!(
app.lint_snapshot(&abroad).platforms,
Platforms::Unavailable(_)
),
"the abroad row read its home-region twin's answer"
);
assert!(matches!(
app.lint_snapshot(&twin).platforms,
Platforms::Loaded(_)
));
app.worker_dlq_depths.insert("jobs".into(), 250);
let snap = app.lint_snapshot(&worker);
let run = snap.finish(EnvLintInputs::bare(Vec::new()), &[]);
assert!(
run.issues.iter().any(|i| i.rule_id == "EBL011"),
"fires on 250"
);
assert!(!run
.coverage_warnings
.iter()
.any(|w| w.starts_with("EBL011")));
app.worker_dlq_stale.insert("jobs".into());
let snap = app.lint_snapshot(&worker);
assert!(matches!(snap.dlq, WorkerDlq::Unknown(_)));
let run = snap.finish(EnvLintInputs::bare(Vec::new()), &[]);
assert!(
!run.issues.iter().any(|i| i.rule_id == "EBL011"),
"a stale depth is not judged on"
);
assert!(
run.coverage_warnings
.iter()
.any(|w| w.starts_with("EBL011")),
"{:?}",
run.coverage_warnings
);
}
#[tokio::test]
async fn the_platform_fetch_error_is_cleared_by_a_success_and_a_context_switch() {
let _cache_guard = crate::aws::CACHE_TEST_LOCK.lock().await;
let mut app = test_app();
app.handle_msg(AppMsg::SolutionStacks {
gen: app.generation,
result: Err("ListAvailableSolutionStacks failed: AccessDenied".into()),
});
assert!(app.latest_stacks_error.is_some());
app.handle_msg(AppMsg::SolutionStacks {
gen: app.generation,
result: Ok(vec![
"64bit Amazon Linux 2023 v4.1.0 running Corretto 17".into()
]),
});
assert!(app.latest_stacks_error.is_none(), "a success clears it");
app.latest_stacks_error = Some("old account's failure".into());
app.handle_msg(AppMsg::Rebuild {
epoch: app.rebuild_epoch,
result: Ok(Box::new(crate::aws::AwsClient::stub())),
});
assert!(
app.latest_stacks_error.is_none(),
"a context switch clears it"
);
}
#[test]
fn only_tui_lint_calls_the_lint_engine_outside_lint_and_cli() {
const ENGINE: &[&str] = &[
"fetch_env_lint_inputs(",
"run_rules_for_env(",
"default_rules(",
"assemble(",
"input_gaps(",
"EnvLintInputs",
"LintContext",
"run_rules(",
];
let mut offenders: Vec<String> = Vec::new();
let mut scanned = 0usize;
for (path, full) in super::scan::source_files() {
if super::scan::is_test_path(&path)
|| path.contains("src/lint/")
|| path.contains("src/cli/")
|| path.ends_with("src/app/tui_lint.rs")
{
continue;
}
scanned += 1;
let prod = super::scan::production_half(&full);
for line in prod.lines().map(super::scan::strip_line_comment) {
if let Some(hit) = ENGINE.iter().find(|n| line.contains(*n)) {
offenders.push(format!("{path}: {hit}"));
}
}
}
assert!(scanned > 60, "scanned only {scanned} files");
assert!(
offenders.is_empty(),
"these call the lint engine outside `app::tui_lint` — go through \
`LintSnapshot` so the cached inputs and their gaps come along: {offenders:?}"
);
}
#[test]
fn reported_once_is_built_only_by_report_once() {
let mut offenders: Vec<String> = Vec::new();
for (path, full) in super::scan::source_files() {
if super::scan::is_test_path(&path) || path.ends_with("src/lint/inputs.rs") {
continue;
}
let prod = super::scan::production_half(&full);
if prod
.lines()
.map(super::scan::strip_line_comment)
.any(|l| l.contains("ReportedOnce"))
{
offenders.push(path);
}
}
assert!(
offenders.is_empty(),
"built outside report_once: {offenders:?}"
);
let inputs = super::scan::production_source("lint/inputs.rs");
let built = inputs
.lines()
.map(super::scan::strip_line_comment)
.filter(|l| l.contains("Self::ReportedOnce") && !l.contains("=>"))
.count();
assert_eq!(
built, 1,
"lint/inputs.rs builds ReportedOnce outside report_once"
);
}
#[test]
fn assemble_carries_every_fetched_input_to_the_rules() {
use crate::lint::inputs::{assemble, run_rules_for_env, Platforms};
let mut env = mk_env("api", "poly", "WebServer", "Green");
env.solution_stack = "64bit Amazon Linux 2023 v4.1.0 running Corretto 17".into();
let newest: std::collections::HashMap<String, String> = [(
"64bit Amazon Linux 2023 running Corretto 17".to_string(),
"4.2.0".to_string(),
)]
.into_iter()
.collect();
assert!(
crate::aws::newer_stack_version(&env.solution_stack, &newest).is_some(),
"fixture: the catalogue must hold a newer version of the env's family"
);
let required = vec!["Owner".to_string()];
let inputs = assemble(
&env,
Vec::new(),
Some(Ok(vec!["Team".to_string()])),
Ok(0),
&Platforms::Loaded(newest),
&[],
&required,
);
assert_eq!(
inputs.env_tag_keys.as_deref(),
Some(&["Team".to_string()][..])
);
assert_eq!(inputs.healthy_count, Some(0));
assert_eq!(inputs.newer_stack.as_deref(), Some("4.2.0"));
assert!(
inputs.coverage_warnings.is_empty(),
"{:?}",
inputs.coverage_warnings
);
let rules = crate::lint::default_rules(&[]);
let fired: Vec<String> = run_rules_for_env(&rules, &env, &inputs, &required)
.into_iter()
.map(|i| i.rule_id)
.collect();
for rule in ["EBL008", "EBL010", "EBL012"] {
assert!(
fired.iter().any(|r| r == rule),
"{rule} did not fire: {fired:?}"
);
}
}