use super::site::{Id, Origin};
use crate::domain::action::identity::{ActionId, Version};
use crate::domain::action::uses_ref::ParsedRef;
use std::collections::{HashMap, HashSet};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WorkflowAction {
pub id: ActionId,
pub reference: ParsedRef,
}
#[derive(Debug, Default)]
pub struct ActionSet {
versions: HashMap<ActionId, HashSet<Version>>,
counts: HashMap<ActionId, HashMap<Version, usize>>,
}
impl ActionSet {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn from_located(actions: &[Located]) -> Self {
let mut set = Self::new();
for action in actions {
set.add(&action.action);
}
set
}
pub fn add(&mut self, interpreted: &WorkflowAction) {
let version = interpreted.reference.label_version();
self.versions
.entry(interpreted.id.clone())
.or_default()
.insert(version.clone());
let count = self
.counts
.entry(interpreted.id.clone())
.or_default()
.entry(version)
.or_insert(0);
*count = count.saturating_add(1);
}
#[must_use]
pub fn dominant_version(&self, id: &ActionId) -> Option<Version> {
let counts = self.counts.get(id)?;
let max_count = counts.values().max().copied()?;
let candidates: Vec<Version> = counts
.iter()
.filter(|(_, c)| **c == max_count)
.map(|(v, _)| v.clone())
.collect();
Version::highest(&candidates)
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.versions.is_empty()
}
pub fn versions_for(&self, id: &ActionId) -> impl Iterator<Item = &Version> {
self.versions
.get(id)
.map(|v| v.iter())
.into_iter()
.flatten()
}
pub fn action_ids(&self) -> impl Iterator<Item = &ActionId> {
self.versions.keys()
}
}
#[derive(Debug, Clone)]
pub struct Located {
pub action: WorkflowAction,
pub site: Id,
pub origin: Origin,
}
#[cfg(test)]
mod tests {
use super::{ActionId, ActionSet, Located, ParsedRef, Version, WorkflowAction};
use crate::domain::action::identity::CommitSha;
use crate::domain::file::site::{Id, JobId, Origin, Slot, StepIndex, WorkflowPath};
fn make_interpreted(name: &str, version: &str, sha: Option<&str>) -> WorkflowAction {
let reference = sha.map_or_else(
|| ParsedRef::Ref(Version::from(version)),
|sha_str| ParsedRef::Pinned {
sha: CommitSha::from(sha_str),
comment: Version::from(version),
},
);
WorkflowAction {
id: ActionId::from(name),
reference,
}
}
#[test]
fn most_used_version_two_vs_one() {
let mut set = ActionSet::new();
set.add(&make_interpreted("actions/checkout", "v3", None));
set.add(&make_interpreted("actions/checkout", "v3", None));
set.add(&make_interpreted("actions/checkout", "v4", None));
let dominant = set.dominant_version(&ActionId::from("actions/checkout"));
assert_eq!(dominant, Some(Version::from("v3")));
}
#[test]
fn dominant_version_tiebreak_highest_semver() {
let mut set = ActionSet::new();
set.add(&make_interpreted("actions/checkout", "v3", None));
set.add(&make_interpreted("actions/checkout", "v4", None));
let dominant = set.dominant_version(&ActionId::from("actions/checkout"));
assert_eq!(dominant, Some(Version::from("v4")));
}
#[test]
fn workflow_location_equality() {
let loc1 = Id {
file: WorkflowPath::new(".github/workflows/ci.yml"),
slot: Slot::WorkflowStep {
job: JobId::from("build"),
step: StepIndex::from(0_u16),
},
};
let loc2 = Id {
file: WorkflowPath::new(".github/workflows/ci.yml"),
slot: Slot::WorkflowStep {
job: JobId::from("build"),
step: StepIndex::from(0_u16),
},
};
assert_eq!(loc1, loc2);
}
#[test]
fn located_action_stores_location() {
let loc = Id {
file: WorkflowPath::new(".github/workflows/ci.yml"),
slot: Slot::WorkflowStep {
job: JobId::from("build"),
step: StepIndex::from(0_u16),
},
};
let action = Located {
action: WorkflowAction {
id: ActionId::from("actions/checkout"),
reference: ParsedRef::Ref(Version::from("v4")),
},
site: loc.clone(),
origin: Origin::default(),
};
assert_eq!(action.site, loc);
assert_eq!(action.action.id.as_str(), "actions/checkout");
}
#[test]
fn add_single_version() {
let mut set = ActionSet::new();
set.add(&make_interpreted("actions/checkout", "v4", None));
let versions: Vec<_> = set
.versions_for(&ActionId::from("actions/checkout"))
.collect();
assert_eq!(versions.len(), 1);
assert!(versions.contains(&&Version::from("v4")));
}
#[test]
fn add_multiple_versions() {
let mut set = ActionSet::new();
set.add(&make_interpreted("actions/checkout", "v4", None));
set.add(&make_interpreted("actions/checkout", "v3", None));
let versions: Vec<_> = set
.versions_for(&ActionId::from("actions/checkout"))
.collect();
assert_eq!(versions.len(), 2);
assert!(versions.contains(&&Version::from("v4")));
assert!(versions.contains(&&Version::from("v3")));
}
#[test]
fn add_duplicate_version() {
let mut set = ActionSet::new();
set.add(&make_interpreted("actions/checkout", "v4", None));
set.add(&make_interpreted("actions/checkout", "v4", None));
assert_eq!(
set.versions_for(&ActionId::from("actions/checkout"))
.count(),
1
);
}
#[test]
fn action_ids() {
let mut set = ActionSet::new();
set.add(&make_interpreted("actions/checkout", "v4", None));
set.add(&make_interpreted("actions/setup-node", "v3", None));
let ids: Vec<_> = set.action_ids().collect();
assert_eq!(ids.len(), 2);
assert!(ids.contains(&&ActionId::from("actions/checkout")));
assert!(ids.contains(&&ActionId::from("actions/setup-node")));
}
#[test]
fn versions_for_unknown_action() {
let set = ActionSet::new();
assert_eq!(
set.versions_for(&ActionId::from("unknown/action")).count(),
0
);
}
}