use std::fmt;
use std::io;
use std::time::Instant;
use miette::{Diagnostic, GraphicalReportHandler, GraphicalTheme};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CheckStatus {
Pass,
SpecViolation,
NetworkError,
Advisory,
Skipped,
}
impl CheckStatus {
pub fn glyph(self) -> &'static str {
match self {
CheckStatus::Pass => "[OK]",
CheckStatus::SpecViolation => "[FAIL]",
CheckStatus::NetworkError => "[NET]",
CheckStatus::Advisory => "[WARN]",
CheckStatus::Skipped => "[SKIP]",
}
}
pub fn styled_glyph(self, no_color: bool) -> &'static str {
if no_color {
return self.glyph();
}
match self {
CheckStatus::Pass => "\x1b[1;32m[OK]\x1b[0m",
CheckStatus::SpecViolation => "\x1b[1;31m[FAIL]\x1b[0m",
CheckStatus::NetworkError => "\x1b[1;35m[NET]\x1b[0m",
CheckStatus::Advisory => "\x1b[1;33m[WARN]\x1b[0m",
CheckStatus::Skipped => "\x1b[2m[SKIP]\x1b[0m",
}
}
}
impl fmt::Display for CheckStatus {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.glyph())
}
}
#[derive(Debug)]
pub struct CheckResult {
pub id: &'static str,
pub stage: Stage,
pub status: CheckStatus,
pub summary: std::borrow::Cow<'static, str>,
pub diagnostic: Option<Box<dyn Diagnostic + Send + Sync>>,
pub skipped_reason: Option<std::borrow::Cow<'static, str>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Stage {
Identity,
Http,
Subscription,
Crypto,
Report,
}
impl Stage {
pub fn label(self) -> &'static str {
match self {
Stage::Identity => "Identity",
Stage::Http => "HTTP",
Stage::Subscription => "Subscription",
Stage::Crypto => "Crypto",
Stage::Report => "Report",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SummaryCounts {
pub pass: usize,
pub spec_violation: usize,
pub network_error: usize,
pub advisory: usize,
pub skipped: usize,
}
impl SummaryCounts {
pub fn from_results(results: &[CheckResult]) -> Self {
let mut counts = SummaryCounts {
pass: 0,
spec_violation: 0,
network_error: 0,
advisory: 0,
skipped: 0,
};
for result in results {
match result.status {
CheckStatus::Pass => counts.pass += 1,
CheckStatus::SpecViolation => counts.spec_violation += 1,
CheckStatus::NetworkError => counts.network_error += 1,
CheckStatus::Advisory => counts.advisory += 1,
CheckStatus::Skipped => counts.skipped += 1,
}
}
counts
}
}
#[derive(Debug, Clone)]
pub struct ReportHeader {
pub target: String,
pub resolved_did: Option<String>,
pub pds_endpoint: Option<String>,
pub labeler_endpoint: Option<String>,
}
#[derive(Debug, Clone)]
pub struct RenderConfig {
pub no_color: bool,
}
#[derive(Debug)]
pub struct LabelerReport {
pub header: ReportHeader,
pub results: Vec<CheckResult>,
pub started_at: Instant,
pub finished_at: Option<Instant>,
}
impl LabelerReport {
pub fn new(header: ReportHeader) -> Self {
LabelerReport {
header,
results: Vec::new(),
started_at: Instant::now(),
finished_at: None,
}
}
pub fn record(&mut self, result: CheckResult) {
self.results.push(result);
}
pub fn finish(&mut self) {
self.finished_at = Some(Instant::now());
}
pub fn exit_code(&self) -> i32 {
let mut has_spec_violation = false;
let mut has_network_error = false;
for r in &self.results {
match r.status {
CheckStatus::SpecViolation => has_spec_violation = true,
CheckStatus::NetworkError => has_network_error = true,
_ => {}
}
}
if has_spec_violation {
1
} else if has_network_error {
2
} else {
0
}
}
pub fn summary_counts(&self) -> SummaryCounts {
SummaryCounts::from_results(&self.results)
}
pub fn render<W: io::Write>(&self, out: &mut W, config: &RenderConfig) -> io::Result<()> {
let elapsed = self
.finished_at
.map(|f| f.duration_since(self.started_at).as_millis())
.unwrap_or(0);
writeln!(out, "Target: {}", self.header.target)?;
if let Some(did) = &self.header.resolved_did {
writeln!(out, " Resolved DID: {did}")?;
}
if let Some(pds) = &self.header.pds_endpoint {
writeln!(out, " PDS endpoint: {pds}")?;
}
if let Some(labeler) = &self.header.labeler_endpoint {
writeln!(out, " Labeler endpoint: {labeler}")?;
}
writeln!(out, " elapsed: {elapsed}ms")?;
writeln!(out)?;
let mut current_stage: Option<Stage> = None;
for result in &self.results {
if Some(result.stage) != current_stage {
current_stage = Some(result.stage);
writeln!(out, "== {} ==", result.stage.label())?;
}
write!(
out,
"{} {} ",
result.status.styled_glyph(config.no_color),
result.summary
)?;
if let Some(reason) = &result.skipped_reason {
write!(out, "— {reason}")?;
}
writeln!(out)?;
if let Some(diag) = &result.diagnostic {
if result.status != CheckStatus::Skipped {
let theme = if config.no_color {
GraphicalTheme::unicode_nocolor()
} else {
GraphicalTheme::default()
};
let handler = GraphicalReportHandler::new().with_theme(theme);
let mut buf = String::new();
if let Err(_e) = handler.render_report(&mut buf, diag.as_ref()) {
writeln!(out, " (diagnostic rendering failed)")?;
} else {
for line in buf.lines() {
writeln!(out, " {line}")?;
}
}
}
}
}
writeln!(out)?;
let counts = self.summary_counts();
write!(
out,
"Summary: {} passed, {} failed (spec), {} network errors, {} advisories, {} skipped. ",
counts.pass,
counts.spec_violation,
counts.network_error,
counts.advisory,
counts.skipped
)?;
writeln!(out, "Exit code: {}", self.exit_code())?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn exit_code_only_advisory_is_zero() {
let header = ReportHeader {
target: "test".to_string(),
resolved_did: None,
pds_endpoint: None,
labeler_endpoint: None,
};
let mut report = LabelerReport::new(header);
report.record(CheckResult {
id: "test",
stage: Stage::Identity,
status: CheckStatus::Advisory,
summary: "advisory check".into(),
diagnostic: None,
skipped_reason: None,
});
assert_eq!(report.exit_code(), 0);
}
#[test]
fn exit_code_only_network_errors_is_two() {
let header = ReportHeader {
target: "test".to_string(),
resolved_did: None,
pds_endpoint: None,
labeler_endpoint: None,
};
let mut report = LabelerReport::new(header);
report.record(CheckResult {
id: "test",
stage: Stage::Identity,
status: CheckStatus::NetworkError,
summary: "network check".into(),
diagnostic: None,
skipped_reason: None,
});
assert_eq!(report.exit_code(), 2);
}
#[test]
fn exit_code_spec_violation_takes_precedence_over_network_error() {
let header = ReportHeader {
target: "test".to_string(),
resolved_did: None,
pds_endpoint: None,
labeler_endpoint: None,
};
let mut report = LabelerReport::new(header);
report.record(CheckResult {
id: "net",
stage: Stage::Identity,
status: CheckStatus::NetworkError,
summary: "network check".into(),
diagnostic: None,
skipped_reason: None,
});
report.record(CheckResult {
id: "spec",
stage: Stage::Identity,
status: CheckStatus::SpecViolation,
summary: "spec check".into(),
diagnostic: None,
skipped_reason: None,
});
assert_eq!(report.exit_code(), 1);
}
#[test]
fn exit_code_with_spec_violation_is_one() {
let header = ReportHeader {
target: "test".to_string(),
resolved_did: None,
pds_endpoint: None,
labeler_endpoint: None,
};
let mut report = LabelerReport::new(header);
report.record(CheckResult {
id: "test",
stage: Stage::Identity,
status: CheckStatus::SpecViolation,
summary: "spec check".into(),
diagnostic: None,
skipped_reason: None,
});
assert_eq!(report.exit_code(), 1);
}
#[test]
fn summary_counts_partition_correct() {
let header = ReportHeader {
target: "test".to_string(),
resolved_did: None,
pds_endpoint: None,
labeler_endpoint: None,
};
let mut report = LabelerReport::new(header);
report.record(CheckResult {
id: "test1",
stage: Stage::Identity,
status: CheckStatus::Pass,
summary: "pass check".into(),
diagnostic: None,
skipped_reason: None,
});
report.record(CheckResult {
id: "test2",
stage: Stage::Identity,
status: CheckStatus::SpecViolation,
summary: "fail check".into(),
diagnostic: None,
skipped_reason: None,
});
report.record(CheckResult {
id: "test3",
stage: Stage::Http,
status: CheckStatus::NetworkError,
summary: "net check".into(),
diagnostic: None,
skipped_reason: None,
});
report.record(CheckResult {
id: "test4",
stage: Stage::Http,
status: CheckStatus::Advisory,
summary: "warn check".into(),
diagnostic: None,
skipped_reason: None,
});
report.record(CheckResult {
id: "test5",
stage: Stage::Subscription,
status: CheckStatus::Skipped,
summary: "skip check".into(),
diagnostic: None,
skipped_reason: Some("not implemented".into()),
});
let counts = report.summary_counts();
assert_eq!(counts.pass, 1);
assert_eq!(counts.spec_violation, 1);
assert_eq!(counts.network_error, 1);
assert_eq!(counts.advisory, 1);
assert_eq!(counts.skipped, 1);
}
#[test]
fn render_basic_glyphs() {
let header = ReportHeader {
target: "test.example".to_string(),
resolved_did: None,
pds_endpoint: None,
labeler_endpoint: None,
};
let mut report = LabelerReport::new(header);
report.record(CheckResult {
id: "test1",
stage: Stage::Identity,
status: CheckStatus::Pass,
summary: "pass check".into(),
diagnostic: None,
skipped_reason: None,
});
report.record(CheckResult {
id: "test2",
stage: Stage::Identity,
status: CheckStatus::SpecViolation,
summary: "fail check".into(),
diagnostic: None,
skipped_reason: None,
});
report.record(CheckResult {
id: "test3",
stage: Stage::Http,
status: CheckStatus::Skipped,
summary: "skip check".into(),
diagnostic: None,
skipped_reason: Some("not yet implemented".into()),
});
report.finish();
let mut buf = Vec::new();
let config = RenderConfig { no_color: true };
report.render(&mut buf, &config).expect("render failed");
let output = String::from_utf8(buf).expect("invalid utf-8");
assert!(
output.contains("[OK]"),
"output should contain [OK] glyph:\n{output}"
);
assert!(
output.contains("[FAIL]"),
"output should contain [FAIL] glyph:\n{output}"
);
assert!(
output.contains("[SKIP]"),
"output should contain [SKIP] glyph:\n{output}"
);
assert!(
!output.contains('\x1b'),
"no_color output should not contain ANSI escapes:\n{output}"
);
}
#[test]
fn styled_glyph_emits_ansi_when_color_enabled() {
for status in [
CheckStatus::Pass,
CheckStatus::SpecViolation,
CheckStatus::NetworkError,
CheckStatus::Advisory,
CheckStatus::Skipped,
] {
let colored = status.styled_glyph(false);
assert!(
colored.starts_with("\x1b[") && colored.ends_with("\x1b[0m"),
"{status:?} colored form must be wrapped in SGR escapes: {colored:?}"
);
assert!(
colored.contains(status.glyph()),
"{status:?} colored form must contain the plain glyph"
);
}
}
#[test]
fn report_stage_ordering_places_report_last() {
assert!(Stage::Identity < Stage::Http);
assert!(Stage::Http < Stage::Subscription);
assert!(Stage::Subscription < Stage::Crypto);
assert!(Stage::Crypto < Stage::Report);
}
#[test]
fn render_with_color_wraps_glyphs_in_ansi() {
let header = ReportHeader {
target: "test.example".to_string(),
resolved_did: None,
pds_endpoint: None,
labeler_endpoint: None,
};
let mut report = LabelerReport::new(header);
report.record(CheckResult {
id: "test1",
stage: Stage::Identity,
status: CheckStatus::Pass,
summary: "pass check".into(),
diagnostic: None,
skipped_reason: None,
});
report.finish();
let mut buf = Vec::new();
report
.render(&mut buf, &RenderConfig { no_color: false })
.expect("render failed");
let output = String::from_utf8(buf).expect("invalid utf-8");
assert!(
output.contains(CheckStatus::Pass.styled_glyph(false)),
"colored output should contain the colored [OK] glyph:\n{output}"
);
}
}