use std::cell::Cell;
use crate::logs::{self, Stage as CommandStage};
use crate::recovery;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum Stage {
Implementation,
SpecReview,
SecurityReview,
Push,
PullRequestReadiness,
RepairLoopRound,
RepairSession,
CiWatch,
BaseFix,
SelfMerge,
}
impl Stage {
fn records(self) -> (recovery::Stage, Option<CommandStage>) {
match self {
Self::Implementation => (recovery::Stage::Implementation, None),
Self::SpecReview => (recovery::Stage::SpecReview, None),
Self::SecurityReview => (
recovery::Stage::SecurityReview,
Some(CommandStage::SecurityReview),
),
Self::Push => (recovery::Stage::Push, Some(CommandStage::PullRequest)),
Self::PullRequestReadiness => (
recovery::Stage::PullRequest,
Some(CommandStage::PullRequest),
),
Self::RepairLoopRound => (recovery::Stage::Repair, None),
Self::RepairSession => (recovery::Stage::Repair, Some(CommandStage::Repair)),
Self::CiWatch => (recovery::Stage::Ci, Some(CommandStage::CiWatch)),
Self::BaseFix => (recovery::Stage::BaseFix, None),
Self::SelfMerge => (recovery::Stage::SelfMerge, Some(CommandStage::SelfMerge)),
}
}
}
pub(super) trait CommandRecord {
fn stage(&self, stage: CommandStage);
}
pub(super) struct CommandLog;
impl CommandRecord for CommandLog {
fn stage(&self, stage: CommandStage) {
logs::stage(stage);
}
}
pub(super) struct Tracker<R = CommandLog> {
stopped_at: Cell<recovery::Stage>,
record: R,
}
impl<R: CommandRecord> Tracker<R> {
pub(super) fn new(first: Stage, record: R) -> Self {
let tracker = Self {
stopped_at: Cell::new(recovery::Stage::Implementation),
record,
};
tracker.enter(first);
tracker
}
pub(super) fn enter(&self, stage: Stage) {
let (stopped_at, recorded) = stage.records();
self.stopped_at.set(stopped_at);
if let Some(recorded) = recorded {
self.record.stage(recorded);
}
}
pub(super) fn stop_at_security_review(&self) {
self.stopped_at.set(recovery::Stage::SecurityReview);
}
pub(super) fn stopped_at(&self) -> recovery::Stage {
self.stopped_at.get()
}
}
#[cfg(test)]
#[derive(Default)]
pub(super) struct Recorded(std::cell::RefCell<Vec<&'static str>>);
#[cfg(test)]
impl Recorded {
pub(super) fn names(&self) -> Vec<&'static str> {
self.0.borrow().clone()
}
}
#[cfg(test)]
impl CommandRecord for Recorded {
fn stage(&self, stage: CommandStage) {
let mut names = self.0.borrow_mut();
if names.last() != Some(&stage.name()) {
names.push(stage.name());
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn entering_each_stage_sets_its_recovery_stage_and_records_its_command_record_stage() {
let table = [
(Stage::Implementation, recovery::Stage::Implementation, None),
(Stage::SpecReview, recovery::Stage::SpecReview, None),
(
Stage::SecurityReview,
recovery::Stage::SecurityReview,
Some("Security review"),
),
(Stage::Push, recovery::Stage::Push, Some("pull request")),
(
Stage::PullRequestReadiness,
recovery::Stage::PullRequest,
Some("pull request"),
),
(Stage::RepairLoopRound, recovery::Stage::Repair, None),
(
Stage::RepairSession,
recovery::Stage::Repair,
Some("Repair"),
),
(Stage::CiWatch, recovery::Stage::Ci, Some("CI watch")),
(Stage::BaseFix, recovery::Stage::BaseFix, None),
(
Stage::SelfMerge,
recovery::Stage::SelfMerge,
Some("Self-merge"),
),
];
for (stage, stopped_at, recorded) in table {
let tracker = Tracker::new(Stage::Implementation, Recorded::default());
tracker.enter(stage);
assert_eq!(tracker.stopped_at(), stopped_at, "{stage:?}");
assert_eq!(
tracker.record.names(),
Vec::from_iter(recorded),
"{stage:?}"
);
}
}
#[test]
fn a_stage_entered_again_is_recorded_once() {
let tracker = Tracker::new(Stage::Implementation, Recorded::default());
for stage in [
Stage::Push,
Stage::PullRequestReadiness,
Stage::RepairLoopRound,
Stage::Push,
Stage::CiWatch,
Stage::CiWatch,
] {
tracker.enter(stage);
}
assert_eq!(tracker.record.names(), ["pull request", "CI watch"]);
assert_eq!(tracker.stopped_at(), recovery::Stage::Ci);
}
#[test]
fn a_security_hold_stops_at_the_security_review_without_recording_it_again() {
let tracker = Tracker::new(Stage::Implementation, Recorded::default());
for stage in [
Stage::SecurityReview,
Stage::Push,
Stage::PullRequestReadiness,
Stage::RepairLoopRound,
Stage::CiWatch,
] {
tracker.enter(stage);
}
tracker.stop_at_security_review();
assert_eq!(tracker.stopped_at(), recovery::Stage::SecurityReview);
assert_eq!(
tracker.record.names(),
["Security review", "pull request", "CI watch"]
);
}
#[test]
fn a_new_tracker_has_entered_its_first_stage() {
let tracker = Tracker::new(Stage::SpecReview, Recorded::default());
assert_eq!(tracker.stopped_at(), recovery::Stage::SpecReview);
assert!(tracker.record.names().is_empty());
}
}