use super::config::{ColumnLimit, WipDiagnostic, WipDiagnosticKind};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BoundLimit {
pub column: String,
pub max_items: Option<u32>,
pub max_estimate: Option<u32>,
pub unit: Option<String>,
}
impl BoundLimit {
pub fn of(column: &str, limit: &ColumnLimit) -> Self {
Self {
column: column.to_string(),
max_items: limit.max_items,
max_estimate: limit.max_estimate,
unit: limit.unit.clone(),
}
}
pub fn matches(&self, column: &str, limit: &ColumnLimit) -> bool {
self.column == column
&& self.max_items == limit.max_items
&& self.max_estimate == limit.max_estimate
&& self.unit == limit.unit
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OverrideRequest {
pub confirmed: bool,
pub actor: String,
pub timestamp: String,
pub reason: String,
pub limit: BoundLimit,
}
impl OverrideRequest {
pub fn new(
actor: impl Into<String>,
timestamp: impl Into<String>,
reason: impl Into<String>,
limit: BoundLimit,
) -> Self {
Self {
confirmed: true,
actor: actor.into(),
timestamp: timestamp.into(),
reason: reason.into(),
limit,
}
}
pub fn summary(&self) -> String {
format!("override by {} ({})", self.actor, self.reason)
}
pub fn findings(&self, column: &str, limit: &ColumnLimit) -> Vec<WipDiagnostic> {
let mut findings = Vec::new();
if !self.confirmed {
findings.push(WipDiagnostic::new(
WipDiagnosticKind::UnconfirmedOverride,
"an override must be confirmed explicitly; nothing was changed",
));
}
for (field, value) in [
("actor", self.actor.trim()),
("timestamp", self.timestamp.trim()),
("reason", self.reason.trim()),
] {
if value.is_empty() {
findings.push(WipDiagnostic::new(
WipDiagnosticKind::MissingOverrideField,
format!("an override needs a `{field}`; a blank `{field}` is not one"),
));
}
}
if !self.limit.matches(column, limit) {
findings.push(WipDiagnostic::new(
WipDiagnosticKind::StaleOverrideLimit,
format!(
"the override was authorized for {} on `{}`, but the limit now in force is \
{} on `{column}` — confirm the limit that is actually there",
self.limit.describe(),
self.limit.column,
BoundLimit::of(column, limit).describe(),
),
));
}
findings
}
}
impl BoundLimit {
pub fn describe(&self) -> String {
let mut parts = Vec::new();
if let Some(max) = self.max_items {
parts.push(format!("max_items {max}"));
}
if let Some(max) = self.max_estimate {
match self.unit.as_deref() {
Some(unit) => parts.push(format!("max_estimate {max}{unit}")),
None => parts.push(format!("max_estimate {max}")),
}
}
if parts.is_empty() {
"no ceilings".to_string()
} else {
parts.join(", ")
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::wip::{ColumnLimit, LimitPolicy, WipDiagnosticKind};
fn limit() -> ColumnLimit {
ColumnLimit {
max_items: Some(3),
max_estimate: Some(10),
unit: Some("d".to_string()),
policy: Some(LimitPolicy::Block),
enabled: true,
}
}
fn request(limit: &ColumnLimit) -> OverrideRequest {
OverrideRequest::new(
"alice",
"2026-01-02T03:04:05Z",
"release week",
BoundLimit::of("in progress", limit),
)
}
#[test]
fn wip_override_a_complete_request_has_no_findings() {
let limit = limit();
assert_eq!(
request(&limit).findings("in progress", &limit),
[] as [crate::wip::config::WipDiagnostic; 0]
);
}
#[test]
fn wip_override_every_missing_field_is_named() {
let limit = limit();
let cases = [
("confirmed", {
let mut request = request(&limit);
request.confirmed = false;
request
}),
("actor", {
let mut request = request(&limit);
request.actor = " ".to_string();
request
}),
("timestamp", {
let mut request = request(&limit);
request.timestamp = String::new();
request
}),
("reason", {
let mut request = request(&limit);
request.reason = String::new();
request
}),
];
for (field, request) in cases {
let findings = request.findings("in progress", &limit);
assert_eq!(findings.len(), 1, "{field}: {findings:?}");
assert!(
findings[0].message.contains(field) || field == "confirmed",
"{field}: {}",
findings[0].message
);
assert_ne!(
findings[0].kind,
WipDiagnosticKind::StaleOverrideLimit,
"{field} must not be reported as a stale limit"
);
}
let mut unconfirmed = request(&limit);
unconfirmed.confirmed = false;
assert_eq!(
unconfirmed.findings("in progress", &limit)[0].kind,
WipDiagnosticKind::UnconfirmedOverride
);
}
#[test]
fn wip_override_a_changed_limit_makes_the_override_stale() {
let original = limit();
let request = request(&original);
let mut raised = original.clone();
raised.max_items = Some(5);
let findings = request.findings("in progress", &raised);
assert_eq!(findings.len(), 1);
assert_eq!(findings[0].kind, WipDiagnosticKind::StaleOverrideLimit);
assert!(
findings[0].message.contains("max_items 3")
&& findings[0].message.contains("max_items 5"),
"{}",
findings[0].message
);
let mut in_weeks = original.clone();
in_weeks.unit = Some("w".to_string());
assert_eq!(
request.findings("in progress", &in_weeks)[0].kind,
WipDiagnosticKind::StaleOverrideLimit
);
assert_eq!(
request.findings("in review", &original)[0].kind,
WipDiagnosticKind::StaleOverrideLimit
);
}
#[test]
fn wip_override_a_disabled_limit_is_still_described() {
let limit = ColumnLimit {
enabled: false,
..limit()
};
let bound = BoundLimit::of("in progress", &limit);
assert_eq!(bound.describe(), "max_items 3, max_estimate 10d");
assert!(bound.matches("in progress", &limit));
}
}