use crate::cli::color;
use crate::cli::output::{OutputConfig, OutputFormat};
const MARK_WIDTH: usize = 5;
const TITLE_WIDTH: usize = 11;
const CONTINUATION: &str = " ";
pub const EXIT_DEGRADED: i32 = 7;
mod verdict {
use std::sync::atomic::{AtomicI32, Ordering};
static PENDING: AtomicI32 = AtomicI32::new(0);
fn severity(code: i32) -> u8 {
match code {
0 => 0,
super::EXIT_DEGRADED => 1,
_ => 2,
}
}
pub fn record(code: i32) {
let mut current = PENDING.load(Ordering::SeqCst);
while severity(code) > severity(current) {
match PENDING.compare_exchange(current, code, Ordering::SeqCst, Ordering::SeqCst) {
Ok(_) => return,
Err(observed) => current = observed,
}
}
}
pub fn pending() -> i32 {
PENDING.load(Ordering::SeqCst)
}
#[cfg(test)]
pub fn reset() {
PENDING.store(0, Ordering::SeqCst);
}
}
#[cfg(test)]
pub use verdict::reset as reset_exit_code;
pub use verdict::{pending as pending_exit_code, record as record_exit_code};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Group {
Runtime,
Coverage,
Platform,
}
impl Group {
pub const ALL: [Group; 3] = [Group::Runtime, Group::Coverage, Group::Platform];
pub fn title(self) -> &'static str {
match self {
Group::Runtime => "Runtime",
Group::Coverage => "Coverage",
Group::Platform => "Platform",
}
}
pub fn key(self) -> &'static str {
match self {
Group::Runtime => "runtime",
Group::Coverage => "coverage",
Group::Platform => "platform",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Section {
Environment,
Daemon,
Hooks,
Boundary,
Persistence,
Connection,
Cloud,
Policy,
Inventory,
Telemetry,
Update,
Integrity,
}
impl Section {
pub const ALL: [Section; 12] = [
Section::Environment,
Section::Daemon,
Section::Persistence,
Section::Update,
Section::Hooks,
Section::Boundary,
Section::Policy,
Section::Inventory,
Section::Integrity,
Section::Connection,
Section::Cloud,
Section::Telemetry,
];
pub fn group(self) -> Group {
match self {
Section::Environment | Section::Daemon | Section::Persistence | Section::Update => {
Group::Runtime
}
Section::Hooks
| Section::Boundary
| Section::Policy
| Section::Inventory
| Section::Integrity => Group::Coverage,
Section::Connection | Section::Cloud | Section::Telemetry => Group::Platform,
}
}
pub fn of_group(group: Group) -> impl Iterator<Item = Section> {
Section::ALL.into_iter().filter(move |s| s.group() == group)
}
pub fn title(self) -> &'static str {
match self {
Section::Environment => "Environment",
Section::Daemon => "Daemon",
Section::Hooks => "Hooks",
Section::Boundary => "Boundary",
Section::Persistence => "Persistence",
Section::Connection => "Connection",
Section::Cloud => "Cloud",
Section::Policy => "Policy",
Section::Inventory => "Inventory",
Section::Telemetry => "Telemetry",
Section::Update => "Update",
Section::Integrity => "Integrity",
}
}
pub fn key(self) -> &'static str {
match self {
Section::Environment => "environment",
Section::Daemon => "daemon",
Section::Hooks => "hooks",
Section::Boundary => "boundary",
Section::Persistence => "persistence",
Section::Connection => "connection",
Section::Cloud => "cloud",
Section::Policy => "policy",
Section::Inventory => "inventory",
Section::Telemetry => "telemetry",
Section::Update => "update",
Section::Integrity => "integrity",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum State {
Ok,
Off,
Degraded,
Pending,
Failed,
Unknown(Section),
NotApplicable,
}
impl State {
fn severity(self) -> u8 {
match self {
State::NotApplicable => 0,
State::Ok => 1,
State::Pending => 2,
State::Off => 3,
State::Degraded => 4,
State::Unknown(_) => 5,
State::Failed => 6,
}
}
pub fn key(self) -> &'static str {
match self {
State::Ok => "ok",
State::Off => "off",
State::Degraded => "degraded",
State::Pending => "pending",
State::Failed => "failed",
State::Unknown(_) => "unknown",
State::NotApplicable => "not_applicable",
}
}
pub fn is_failure(self) -> bool {
self == State::Failed
}
pub fn is_warning(self) -> bool {
matches!(
self,
State::Off | State::Degraded | State::Pending | State::Unknown(_)
)
}
pub fn requires_remedy(self) -> bool {
self.is_failure() || self.is_warning()
}
pub fn mark(self, color_enabled: bool) -> String {
if color_enabled {
let glyph = match self {
State::Ok => color::checkmark(true).to_string(),
State::Failed => color::cross(true).to_string(),
State::NotApplicable => color::dim("\u{00b7}", true),
_ => color::warning_mark(true).to_string(),
};
format!("{glyph} ")
} else {
let label = match self {
State::Ok => "OK ",
State::Failed => "ERR ",
State::NotApplicable => "-- ",
_ => "WARN ",
};
label.to_string()
}
}
}
#[derive(Debug, Clone)]
pub struct Check {
pub section: Section,
pub state: State,
pub headline: String,
pub detail: Vec<String>,
pub code: Option<&'static str>,
pub remedy: Option<String>,
pub source: Option<String>,
pub agent: Option<&'static str>,
generated_headline: bool,
}
impl Check {
fn new(section: Section, state: State, headline: impl Into<String>) -> Self {
Self {
section,
state,
headline: headline.into(),
detail: Vec::new(),
code: None,
remedy: None,
source: None,
agent: None,
generated_headline: false,
}
}
pub fn ok(section: Section, headline: impl Into<String>) -> Self {
Self::new(section, State::Ok, headline)
}
pub fn off(section: Section, headline: impl Into<String>) -> Self {
Self::new(section, State::Off, headline)
}
pub fn degraded(section: Section, headline: impl Into<String>) -> Self {
Self::new(section, State::Degraded, headline)
}
pub fn pending(section: Section, headline: impl Into<String>) -> Self {
Self::new(section, State::Pending, headline)
}
pub fn failed(section: Section, headline: impl Into<String>) -> Self {
Self::new(section, State::Failed, headline)
}
pub fn unknown(section: Section, blocker: Section) -> Self {
Self {
section,
state: State::Unknown(blocker),
headline: format!("Cannot check — waiting on {}", blocker.title()),
detail: Vec::new(),
code: Some(crate::error::ERR_SUBSYSTEM_DEGRADED),
remedy: Some(format!(
"Fix {} first, then run `openlatch doctor` again.",
blocker.title()
)),
source: None,
agent: None,
generated_headline: true,
}
}
pub fn not_applicable(section: Section, headline: impl Into<String>) -> Self {
Self::new(section, State::NotApplicable, headline)
}
#[must_use]
pub fn code(mut self, code: &'static str) -> Self {
self.code = Some(code);
self
}
#[must_use]
pub fn remedy(mut self, remedy: impl Into<String>) -> Self {
self.remedy = Some(remedy.into());
self
}
#[must_use]
pub fn source(mut self, source: impl Into<String>) -> Self {
self.source = Some(source.into());
self
}
#[must_use]
pub fn headline(mut self, headline: impl Into<String>) -> Self {
self.headline = headline.into();
self.generated_headline = false;
self
}
#[must_use]
pub fn detail(mut self, line: impl Into<String>) -> Self {
self.detail.push(line.into());
self
}
#[must_use]
pub fn detail_opt(mut self, line: Option<impl Into<String>>) -> Self {
if let Some(line) = line {
self.detail.push(line.into());
}
self
}
#[must_use]
pub fn agent(mut self, agent: &'static str) -> Self {
self.agent = Some(agent);
self
}
pub fn validate(&self) -> Option<String> {
if !self.state.requires_remedy() {
return None;
}
match (self.code, &self.remedy) {
(Some(_), Some(_)) => None,
(None, Some(_)) => Some(format!(
"{}: '{}' is {} but carries no OL-XXXX code",
self.section.title(),
self.headline,
self.state.key()
)),
(Some(_), None) => Some(format!(
"{}: '{}' is {} but carries no remedy",
self.section.title(),
self.headline,
self.state.key()
)),
(None, None) => Some(format!(
"{}: '{}' is {} but carries neither code nor remedy",
self.section.title(),
self.headline,
self.state.key()
)),
}
}
pub fn to_json(&self) -> serde_json::Value {
let mut value = serde_json::json!({
"section": self.section.key(),
"state": self.state.key(),
"blocked_by": match self.state {
State::Unknown(b) => Some(b.key()),
_ => None,
},
"pass": !self.state.is_failure(),
"headline": self.headline,
"detail": self.detail,
"code": self.code,
"remedy": self.remedy,
"source": self.source,
});
if let (Some(agent), Some(object)) = (self.agent, value.as_object_mut()) {
object.insert("agent".into(), serde_json::json!(agent));
}
value
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Overall {
Healthy,
Degraded,
Broken,
}
impl Overall {
pub fn key(self) -> &'static str {
match self {
Overall::Healthy => "healthy",
Overall::Degraded => "degraded",
Overall::Broken => "broken",
}
}
fn label(self) -> &'static str {
match self {
Overall::Healthy => "HEALTHY",
Overall::Degraded => "DEGRADED",
Overall::Broken => "BROKEN",
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct Counts {
pub ok: usize,
pub warned: usize,
pub failed: usize,
}
#[derive(Debug, Clone, Default)]
pub struct Report {
checks: Vec<Check>,
}
impl Report {
pub fn new() -> Self {
Self::default()
}
pub fn push(&mut self, check: Check) {
debug_assert!(
check.validate().is_none(),
"contract violation: {}",
check.validate().unwrap_or_default()
);
self.checks.push(check);
}
#[must_use]
pub fn with(mut self, check: Check) -> Self {
self.push(check);
self
}
pub fn replace_section(&mut self, section: Section, check: Check) {
self.checks.retain(|c| c.section != section);
self.push(check);
}
pub fn is_empty(&self) -> bool {
self.checks.is_empty()
}
pub fn checks(&self) -> &[Check] {
&self.checks
}
pub fn section_checks(&self, section: Section) -> impl Iterator<Item = &Check> {
self.checks.iter().filter(move |c| c.section == section)
}
pub fn section_state(&self, section: Section) -> State {
self.section_checks(section)
.map(|c| c.state)
.max_by_key(|s| s.severity())
.unwrap_or(State::NotApplicable)
}
pub fn counts(&self) -> Counts {
let mut counts = Counts::default();
for check in &self.checks {
if check.state.is_failure() {
counts.failed += 1;
} else if check.state.is_warning() {
counts.warned += 1;
} else if check.state == State::Ok {
counts.ok += 1;
}
}
counts
}
pub fn overall(&self) -> Overall {
let counts = self.counts();
if counts.failed > 0 {
Overall::Broken
} else if counts.warned > 0 {
Overall::Degraded
} else {
Overall::Healthy
}
}
pub fn exit_code(&self) -> i32 {
match self.overall() {
Overall::Healthy => 0,
Overall::Degraded => EXIT_DEGRADED,
Overall::Broken => 1,
}
}
pub fn record_verdict(&self) {
record_exit_code(self.exit_code());
}
pub fn validate(&self) -> Vec<String> {
let mut problems: Vec<String> = self.checks.iter().filter_map(Check::validate).collect();
for section in Section::ALL {
if self.section_checks(section).next().is_none() {
problems.push(format!(
"{} has no check — every section is always reported (P6)",
section.title()
));
}
}
problems
}
pub fn issues(&self) -> Vec<String> {
self.checks
.iter()
.filter(|c| c.state.requires_remedy())
.map(|c| {
let mut line = format!("{} — {}", c.section.title(), c.headline);
if let Some(remedy) = &c.remedy {
line.push(' ');
line.push_str(remedy);
}
line
})
.collect()
}
pub fn render(&self, output: &OutputConfig) {
debug_assert!(
self.validate().is_empty(),
"contract violations: {:?}",
self.validate()
);
if output.format == OutputFormat::Json || output.quiet {
return;
}
let counts = self.counts();
let overall = self.overall();
let headline = format!(
"Overall: {} — {} failed, {} warning{}, {} ok",
overall.label(),
counts.failed,
counts.warned,
if counts.warned == 1 { "" } else { "s" },
counts.ok,
);
eprintln!(
"{}",
match overall {
Overall::Healthy => color::green(&headline, output.color),
Overall::Degraded => headline.clone(),
Overall::Broken => color::red(&headline, output.color),
}
);
eprintln!();
for group in Group::ALL {
eprintln!("{}", color::bold(group.title(), output.color));
for section in Section::of_group(group) {
self.render_section_line(section, output);
}
eprintln!();
}
}
fn render_section_line(&self, section: Section, output: &OutputConfig) {
let state = self.section_state(section);
let title_cell = format!(" {:<TITLE_WIDTH$} ", section.title());
let blank_cell = " ".repeat(title_cell.len());
let interesting: Vec<&Check> = if output.verbose {
self.section_checks(section).collect()
} else {
self.section_checks(section)
.filter(|c| c.state.requires_remedy())
.collect()
};
if interesting.is_empty() {
eprintln!(
"{title_cell}{}{}",
state.mark(output.color),
self.section_summary(section)
);
return;
}
for (index, check) in interesting.iter().enumerate() {
let cell = if index == 0 { &title_cell } else { &blank_cell };
eprintln!(
"{cell}{}{}{}",
check.state.mark(output.color),
check.headline,
match check.code {
Some(code) if check.state.requires_remedy() =>
format!(" {}", color::dim(&format!("[{code}]"), output.color)),
_ => String::new(),
}
);
self.render_continuations(check, &" ".repeat(title_cell.len() + MARK_WIDTH), output);
}
}
fn section_summary(&self, section: Section) -> String {
let checks: Vec<&Check> = self.section_checks(section).collect();
match checks.as_slice() {
[] => "no data".to_string(),
[only] => only.headline.clone(),
many => {
let passed = many.iter().filter(|c| c.state == State::Ok).count();
let skipped = many.len() - passed;
if skipped == 0 {
format!("{passed} checks passed")
} else {
format!("{passed} passed, {skipped} not applicable")
}
}
}
}
fn render_continuations(&self, check: &Check, indent: &str, output: &OutputConfig) {
if let (State::Unknown(blocker), false) = (check.state, check.generated_headline) {
eprintln!(
"{indent}{}",
color::dim(&format!("waiting on : {}", blocker.title()), output.color)
);
}
for line in &check.detail {
eprintln!("{indent}{}", color::dim(line, output.color));
}
if let Some(source) = &check.source {
eprintln!(
"{indent}{}",
color::dim(&format!("found in : {source}"), output.color)
);
}
if let Some(remedy) = &check.remedy {
eprintln!(
"{indent}{}",
color::dim(&format!("to fix : {remedy}"), output.color)
);
}
}
pub fn render_section(&self, section: Section, output: &OutputConfig) {
if output.format == OutputFormat::Json || output.quiet {
return;
}
eprintln!("{}", color::bold(section.title(), output.color));
for check in self.section_checks(section) {
eprintln!(
" {}{}{}",
check.state.mark(output.color),
check.headline,
match check.code {
Some(code) if check.state.requires_remedy() =>
format!(" {}", color::dim(&format!("[{code}]"), output.color)),
_ => String::new(),
}
);
self.render_continuations(check, CONTINUATION, output);
}
}
pub fn render_compact(&self, output: &OutputConfig) {
if output.format == OutputFormat::Json || output.quiet {
return;
}
for group in Group::ALL {
eprintln!(" {}", color::bold(group.title(), output.color));
for section in Section::of_group(group) {
let state = self.section_state(section);
let headline = if state.requires_remedy() {
self.section_checks(section)
.filter(|c| c.state == state)
.map(|c| c.headline.clone())
.next()
.unwrap_or_else(|| "no data".to_string())
} else {
self.section_summary(section)
};
eprintln!(
" {:<TITLE_WIDTH$} {}{}",
section.title(),
state.mark(output.color),
headline
);
}
}
if self.overall() != Overall::Healthy {
eprintln!();
eprintln!(" Run `openlatch doctor` for causes and remedies.");
}
}
pub fn to_json(&self) -> serde_json::Value {
let counts = self.counts();
let sections: Vec<serde_json::Value> = Section::ALL
.iter()
.map(|section| {
serde_json::json!({
"section": section.key(),
"group": section.group().key(),
"state": self.section_state(*section).key(),
"summary": self.section_summary(*section),
"checks": self
.section_checks(*section)
.map(Check::to_json)
.collect::<Vec<_>>(),
})
})
.collect();
serde_json::json!({
"overall": self.overall().key(),
"exit_code": self.exit_code(),
"groups": Group::ALL
.iter()
.map(|g| serde_json::json!({
"group": g.key(),
"sections": Section::of_group(*g)
.map(|s| s.key())
.collect::<Vec<_>>(),
}))
.collect::<Vec<_>>(),
"summary": {
"ok": counts.ok,
"warned": counts.warned,
"failed": counts.failed,
},
"sections": sections,
"issues": self.issues(),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
fn plain() -> OutputConfig {
OutputConfig {
format: OutputFormat::Human,
verbose: false,
debug: false,
quiet: true, color: false,
}
}
fn all_green() -> Report {
let mut report = Report::new();
for section in Section::ALL {
report.push(Check::ok(section, "fine"));
}
report
}
#[test]
fn a_healthy_report_exits_zero() {
let report = all_green();
assert_eq!(report.overall(), Overall::Healthy);
assert_eq!(report.exit_code(), 0);
}
#[test]
fn a_disabled_feature_warns_and_exits_two() {
let mut report = all_green();
report.push(
Check::off(Section::Boundary, "Disabled in config")
.code(crate::error::ERR_BOUNDARY_NOT_RUNNING)
.remedy("set [boundary] enabled = true, then `openlatch restart`"),
);
assert_eq!(report.section_state(Section::Boundary), State::Off);
assert_eq!(report.overall(), Overall::Degraded);
assert_eq!(report.exit_code(), EXIT_DEGRADED);
}
#[test]
fn a_failure_outranks_a_warning() {
let mut report = all_green();
report.push(
Check::off(Section::Persistence, "Disabled")
.code(crate::error::ERR_NO_SUPERVISOR)
.remedy("`openlatch supervision enable`"),
);
report.push(
Check::failed(Section::Boundary, "Cannot bind 7600")
.code(crate::error::ERR_BOUNDARY_PORT_IN_USE)
.remedy("free the port"),
);
assert_eq!(report.overall(), Overall::Broken);
assert_eq!(report.exit_code(), 1);
}
#[test]
fn a_section_takes_the_worst_state_of_its_checks() {
let mut report = all_green();
report.push(Check::ok(Section::Daemon, "port bound"));
report.push(
Check::degraded(Section::Daemon, "one subsystem restarting")
.code(crate::error::ERR_SUBSYSTEM_DEGRADED)
.remedy("check the daemon log"),
);
assert_eq!(report.section_state(Section::Daemon), State::Degraded);
}
#[test]
fn every_non_ok_check_carries_a_code_and_a_remedy() {
let offenders = [
Check::off(Section::Cloud, "x"),
Check::degraded(Section::Cloud, "x"),
Check::pending(Section::Cloud, "x"),
Check::failed(Section::Cloud, "x"),
];
for check in offenders {
assert!(
check.validate().is_some(),
"{} without code/remedy must be rejected",
check.state.key()
);
}
assert!(Check::unknown(Section::Cloud, Section::Daemon)
.validate()
.is_none());
assert!(Check::ok(Section::Cloud, "x").validate().is_none());
assert!(Check::not_applicable(Section::Cloud, "x")
.validate()
.is_none());
}
#[test]
fn a_dead_daemon_produces_exactly_one_failure() {
let mut report = Report::new();
report.push(Check::ok(Section::Environment, "fine"));
report.push(
Check::failed(Section::Daemon, "not running")
.code(crate::error::ERR_DAEMON_START_FAILED)
.remedy("`openlatch start`"),
);
for section in [
Section::Hooks,
Section::Boundary,
Section::Connection,
Section::Cloud,
Section::Policy,
Section::Inventory,
Section::Integrity,
] {
report.push(Check::unknown(section, Section::Daemon));
}
report.push(Check::ok(Section::Persistence, "fine"));
report.push(Check::not_applicable(Section::Telemetry, "opt-out"));
report.push(Check::ok(Section::Update, "current"));
assert_eq!(report.counts().failed, 1);
assert_eq!(report.overall(), Overall::Broken);
assert!(report.validate().is_empty(), "{:?}", report.validate());
}
#[test]
fn validate_rejects_a_missing_section() {
let mut report = Report::new();
report.push(Check::ok(Section::Daemon, "fine"));
let problems = report.validate();
assert!(
problems.iter().any(|p| p.contains("Boundary")),
"a missing section must be reported: {problems:?}"
);
}
#[test]
fn every_section_belongs_to_exactly_one_non_empty_group() {
let grouped: Vec<Section> = Group::ALL
.iter()
.flat_map(|g| Section::of_group(*g))
.collect();
assert_eq!(
grouped.len(),
Section::ALL.len(),
"every section belongs to exactly one group"
);
for group in Group::ALL {
assert!(
Section::of_group(group).next().is_some(),
"{} has no sections",
group.title()
);
}
}
#[test]
fn section_order_follows_group_order() {
let grouped: Vec<Section> = Group::ALL
.iter()
.flat_map(|g| Section::of_group(*g))
.collect();
assert_eq!(grouped, Section::ALL.to_vec());
}
#[test]
fn a_green_section_collapses_and_a_problem_does_not() {
let mut report = Report::new();
for section in Section::ALL {
report.push(Check::ok(section, "fine"));
}
report.push(Check::ok(Section::Hooks, "also fine"));
assert_eq!(
report.section_summary(Section::Hooks),
"2 checks passed",
"several green checks collapse to a count"
);
assert_eq!(
report.section_summary(Section::Daemon),
"fine",
"a lone check speaks for itself"
);
report.push(Check::not_applicable(Section::Update, "not compiled in"));
assert_eq!(
report.section_summary(Section::Update),
"1 passed, 1 not applicable"
);
}
#[test]
fn human_and_json_carry_the_same_sections() {
let report = all_green();
let json = report.to_json();
let sections = json["sections"].as_array().expect("sections array");
assert_eq!(sections.len(), Section::ALL.len());
for (rendered, expected) in sections.iter().zip(Section::ALL) {
assert_eq!(rendered["section"], expected.key());
}
}
#[test]
fn json_keeps_the_legacy_pass_field_meaning_not_a_failure() {
let warn = Check::off(Section::Boundary, "off")
.code(crate::error::ERR_BOUNDARY_NOT_RUNNING)
.remedy("x");
assert_eq!(warn.to_json()["pass"], serde_json::Value::Bool(true));
let fail = Check::failed(Section::Boundary, "broken")
.code(crate::error::ERR_BOUNDARY_PORT_IN_USE)
.remedy("x");
assert_eq!(fail.to_json()["pass"], serde_json::Value::Bool(false));
}
#[test]
fn check_agent_is_absent_not_null_when_unset() {
let host_wide = Check::ok(Section::Boundary, "listening");
let json = host_wide.to_json();
assert!(
json.get("agent").is_none(),
"an unstamped check must carry no `agent` key, not a null one: {json}"
);
let stamped = Check::ok(Section::Hooks, "installed").agent("claude-code");
assert_eq!(stamped.to_json()["agent"], serde_json::json!("claude-code"));
}
#[test]
fn a_blocker_is_named_exactly_once() {
let generated = Check::unknown(Section::Policy, Section::Cloud);
assert!(
generated.headline.contains("Cloud"),
"the generated headline must name the blocker for the compact view"
);
assert!(
generated.generated_headline,
"an untouched `unknown` keeps its generated headline"
);
let overridden = Check::unknown(Section::Policy, Section::Cloud)
.headline("Enforcement state unknown — the platform is unreachable");
assert!(
!overridden.generated_headline,
"overriding the headline hands the blocker to the structural line"
);
assert!(
!overridden.headline.contains("Cloud"),
"an overridden headline is free not to repeat the blocker"
);
}
#[test]
fn marks_are_five_columns_wide_in_no_color_mode() {
for state in [
State::Ok,
State::Off,
State::Degraded,
State::Pending,
State::Failed,
State::Unknown(Section::Daemon),
State::NotApplicable,
] {
assert_eq!(state.mark(false).len(), 5, "{:?} misaligns", state);
}
}
#[test]
fn the_recorded_verdict_keeps_the_worst_code() {
reset_exit_code();
record_exit_code(EXIT_DEGRADED);
record_exit_code(1);
assert_eq!(pending_exit_code(), 1, "failure outranks degraded");
reset_exit_code();
record_exit_code(1);
record_exit_code(EXIT_DEGRADED);
assert_eq!(pending_exit_code(), 1, "degraded cannot clear a failure");
reset_exit_code();
record_exit_code(EXIT_DEGRADED);
record_exit_code(0);
assert_eq!(
pending_exit_code(),
EXIT_DEGRADED,
"success cannot clear a warning"
);
reset_exit_code();
assert_eq!(pending_exit_code(), 0);
}
#[test]
fn rendering_is_silent_in_quiet_mode() {
let report = all_green();
report.render(&plain());
report.render_compact(&plain());
}
}