use std::collections::HashSet;
use chrono::DateTime;
use serde::{Deserialize, Serialize};
use super::command_record::{CommandRecord, StageName};
use super::session_signals::{SessionSignals, Summary};
use super::usual_range::{UsualRange, UsualRanges};
#[derive(Clone, Serialize, Deserialize)]
pub(super) struct RunMetrics {
pub(super) outcome: String,
pub(super) repairs: usize,
pub(super) ci_rounds: usize,
#[serde(default)]
pub(super) unaddressed_findings: Option<crate::pull_request::UnaddressedFindings>,
pub(super) stages: Vec<StageMetrics>,
pub(super) total_minutes: Option<f64>,
#[serde(default)]
pub(super) session_signals: Option<std::collections::BTreeMap<String, Summary>>,
}
#[derive(Clone, Serialize, Deserialize)]
pub(super) struct StageMetrics {
pub(super) name: String,
pub(super) seconds: Option<f64>,
pub(super) minutes: Option<f64>,
pub(super) usual_range: Option<UsualRange>,
pub(super) position: Position,
pub(super) review_findings: Option<ReviewFindings>,
#[serde(default)]
pub(super) session_signals: Option<SessionSignals>,
}
#[derive(Clone, Serialize, Deserialize)]
pub(super) struct ReviewFindings {
pub(super) standards: Option<u64>,
pub(super) spec: Option<u64>,
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub(super) enum Position {
Below,
Within,
Above,
TooFewRuns,
}
impl RunMetrics {
pub(super) fn add_session_signals(
&mut self,
record: &CommandRecord,
mut read: impl FnMut(&std::path::Path) -> Option<SessionSignals>,
) {
if self.session_signals.is_some() {
return;
}
let mut seen = HashSet::new();
let mut summaries = std::collections::BTreeMap::<String, Summary>::new();
for (stage, metric) in record.stages.iter().zip(&mut self.stages) {
if let Some(log) = stage.session_log.as_deref()
&& seen.insert(super::session::original(log))
{
metric.session_signals = read(log);
summaries
.entry(stage.name.clone())
.or_default()
.add(metric.session_signals.as_ref());
}
}
self.session_signals = Some(summaries);
}
pub(super) fn of(
record: &CommandRecord,
ranges: &UsualRanges,
outside: &mut impl super::effects::Outside,
unaddressed_findings: Option<crate::pull_request::UnaddressedFindings>,
) -> Self {
let mut metrics = Self::calculate(record, ranges, unaddressed_findings, |log| {
outside
.review_summary(log)
.and_then(|bytes| serde_json::from_slice(&bytes).ok())
});
metrics.add_session_signals(record, |log| outside.session_signals(log));
metrics
}
pub(super) fn historical(record: &CommandRecord) -> Self {
Self::calculate(record, &UsualRanges::default(), None, |_| None)
}
fn calculate(
record: &CommandRecord,
ranges: &UsualRanges,
unaddressed_findings: Option<crate::pull_request::UnaddressedFindings>,
mut review: impl FnMut(&std::path::Path) -> Option<ReviewFindings>,
) -> Self {
let mut sessions = HashSet::new();
let count = |name: StageName| {
record
.stages
.iter()
.filter(|stage| stage.name == name.name())
.count()
};
Self {
outcome: record
.outcome
.clone()
.expect("a finished Run has an outcome"),
repairs: count(StageName::Repair),
ci_rounds: count(StageName::CiWatch),
unaddressed_findings,
stages: record
.stage_times(None)
.map(|timing| {
let seconds = timing.seconds();
let usual_range = ranges.get(&record.repository, &timing.stage.name);
let position = match seconds.zip(usual_range) {
Some((seconds, range)) if seconds < range.lower_seconds => Position::Below,
Some((seconds, range)) if seconds > range.upper_seconds => Position::Above,
Some(_) => Position::Within,
None => Position::TooFewRuns,
};
StageMetrics {
name: timing.stage.name.clone(),
seconds,
minutes: seconds.map(|seconds| seconds / 60.0),
usual_range,
position,
review_findings: timing
.stage
.session_log
.as_deref()
.filter(|log| sessions.insert(log.to_path_buf()))
.and_then(&mut review),
session_signals: None,
}
})
.collect(),
total_minutes: record.ended_at.as_deref().and_then(|ended| {
let began = DateTime::parse_from_rfc3339(&record.started_at).ok()?;
let ended = DateTime::parse_from_rfc3339(ended).ok()?;
let milliseconds = ended.signed_duration_since(began).num_milliseconds();
(milliseconds >= 0).then_some(milliseconds as f64 / 60_000.0)
}),
session_signals: None,
}
}
}