use std::io::Read;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::core::error::{Error, Result};
use crate::core::eventlog::{Event, EventLog, Segment};
use crate::core::severity::Severity;
pub const METRICS_SNAPSHOT_VERSION: u32 = 1;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct MetricsSnapshot {
pub version: u32,
#[serde(default)]
pub git_sha: Option<String>,
#[serde(default)]
pub loc: Option<serde_json::Value>,
#[serde(default)]
pub complexity: Option<serde_json::Value>,
#[serde(default)]
pub churn: Option<serde_json::Value>,
#[serde(default)]
pub change_coupling: Option<serde_json::Value>,
#[serde(default)]
pub duplication: Option<serde_json::Value>,
#[serde(default)]
pub hotspot: Option<serde_json::Value>,
#[serde(default)]
pub lcom: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub severity_counts: Option<SeverityCounts>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub codebase_files: Option<u32>,
#[serde(default)]
pub delta: Option<serde_json::Value>,
}
impl Default for MetricsSnapshot {
fn default() -> Self {
Self {
version: METRICS_SNAPSHOT_VERSION,
git_sha: None,
loc: None,
complexity: None,
churn: None,
change_coupling: None,
duplication: None,
hotspot: None,
lcom: None,
severity_counts: None,
codebase_files: None,
delta: None,
}
}
}
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct SeverityCounts {
#[serde(default)]
pub critical: u32,
#[serde(default)]
pub high: u32,
#[serde(default)]
pub medium: u32,
#[serde(default)]
pub ok: u32,
}
impl SeverityCounts {
pub fn tally(&mut self, severity: Severity) {
let bucket = match severity {
Severity::Critical => &mut self.critical,
Severity::High => &mut self.high,
Severity::Medium => &mut self.medium,
Severity::Ok => &mut self.ok,
};
*bucket = bucket.saturating_add(1);
}
#[must_use]
pub fn render_inline(&self, colorize: bool) -> String {
format!(
"{} {} {} {} {} {} {} {}",
ansi_wrap(ANSI_RED, "[critical]", colorize),
self.critical,
ansi_wrap(ANSI_YELLOW, "[high]", colorize),
self.high,
ansi_wrap(ANSI_CYAN, "[medium]", colorize),
self.medium,
ansi_wrap(ANSI_GREEN, "[ok]", colorize),
self.ok,
)
}
}
pub(crate) const ANSI_RED: &str = "\x1b[31m";
pub(crate) const ANSI_GREEN: &str = "\x1b[32m";
pub(crate) const ANSI_YELLOW: &str = "\x1b[33m";
pub(crate) const ANSI_CYAN: &str = "\x1b[36m";
#[must_use]
pub(crate) fn ansi_wrap(color: &str, text: &str, enabled: bool) -> String {
if enabled {
format!("{color}{text}\x1b[0m")
} else {
text.to_owned()
}
}
impl MetricsSnapshot {
pub fn latest_in(log: &EventLog) -> Result<Option<(Event, Self)>> {
Self::latest_in_segments(&log.segments()?)
}
pub fn latest_in_segments(segments: &[Segment]) -> Result<Option<(Event, Self)>> {
for seg in segments.iter().rev() {
let mut buf = String::new();
seg.open()?
.read_to_string(&mut buf)
.map_err(|e| Error::Io {
path: seg.path.clone(),
source: e,
})?;
for line in buf.lines().rev() {
if line.trim().is_empty() {
continue;
}
let Ok(event) = serde_json::from_str::<Event>(line) else {
continue;
};
if let Ok(metrics) = serde_json::from_value::<Self>(event.data.clone()) {
return Ok(Some((event, metrics)));
}
}
}
Ok(None)
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
pub struct SnapshotDelta {
pub from_sha: Option<String>,
pub from_timestamp: Option<DateTime<Utc>>,
pub complexity: Option<ComplexityDelta>,
pub churn: Option<ChurnDelta>,
pub hotspot: Option<HotspotDelta>,
pub duplication: Option<DuplicationDelta>,
pub change_coupling: Option<ChangeCouplingDelta>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct ComplexityDelta {
pub max_ccn: i64,
pub max_cognitive: i64,
pub functions: i64,
pub files: i64,
pub new_top_ccn: Vec<String>,
pub new_top_cognitive: Vec<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct ChurnDelta {
pub commits_in_window: i64,
pub top_file_changed: bool,
pub previous_top_file: Option<String>,
pub current_top_file: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
pub struct HotspotDelta {
pub max_score: f64,
pub top_files_added: Vec<String>,
pub top_files_dropped: Vec<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct DuplicationDelta {
pub duplicate_blocks: i64,
pub duplicate_tokens: i64,
pub files_affected: i64,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct ChangeCouplingDelta {
pub pairs: i64,
pub files: i64,
pub max_pair_count: i64,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn severity_counts_render_inline_plain_has_no_ansi() {
let c = SeverityCounts {
critical: 3,
high: 12,
medium: 28,
ok: 412,
};
let s = c.render_inline(false);
assert!(
!s.contains('\x1b'),
"plain render must not include ANSI codes"
);
assert!(s.contains("[critical] 3"));
assert!(s.contains("[high] 12"));
assert!(s.contains("[medium] 28"));
assert!(s.contains("[ok] 412"));
}
#[test]
fn severity_counts_render_inline_colored_has_reset_after_each_label() {
let c = SeverityCounts::default();
let s = c.render_inline(true);
assert_eq!(s.matches("\x1b[0m").count(), 4);
}
}