use std::collections::BTreeSet;
use super::domain::Count;
use super::ir::Span;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct TargetSet {
pub hosts: BTreeSet<String>,
pub unresolved: BTreeSet<Unresolved>,
pub local: bool,
}
impl TargetSet {
pub fn host(h: impl Into<String>) -> TargetSet {
TargetSet {
hosts: BTreeSet::from([h.into()]),
unresolved: BTreeSet::new(),
local: false,
}
}
pub fn local() -> TargetSet {
TargetSet {
hosts: BTreeSet::new(),
unresolved: BTreeSet::new(),
local: true,
}
}
pub fn unresolved(why: Unresolved) -> TargetSet {
TargetSet {
hosts: BTreeSet::new(),
unresolved: BTreeSet::from([why]),
local: false,
}
}
pub fn union(&self, other: &TargetSet) -> TargetSet {
TargetSet {
hosts: self.hosts.union(&other.hosts).cloned().collect(),
unresolved: self.unresolved.union(&other.unresolved).cloned().collect(),
local: self.local || other.local,
}
}
pub fn is_local_only(&self) -> bool {
self.local && self.hosts.is_empty() && self.unresolved.is_empty()
}
pub fn only_host(&self) -> Option<&str> {
(self.hosts.len() == 1 && self.unresolved.is_empty() && !self.local)
.then(|| self.hosts.iter().next().map(String::as_str))
.flatten()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Unresolved {
Dynamic,
UnknownOption,
FromFile,
}
impl Unresolved {
pub fn describe(self) -> &'static str {
match self {
Unresolved::Dynamic => "built at run time",
Unresolved::UnknownOption => "after an option the analysis does not model",
Unresolved::FromFile => "read from a file",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Auth {
GhApi,
Other,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Factor {
pub kind: FactorKind,
pub count: Count,
pub span: Span,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FactorKind {
Loop { bound: String },
FirstIteration,
LaterIterations,
Call { function: String },
Evaluation,
Builtin { what: String },
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Contribution {
pub program: &'static str,
pub transfers: Count,
pub targets: TargetSet,
pub auth: Auth,
pub factors: Vec<Factor>,
pub uncounted: Vec<&'static str>,
pub span: Span,
pub paced: Option<Pace>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Pace {
pub secs: u64,
pub burst: Count,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Effects {
pub contributions: Vec<Contribution>,
pub unknown: Vec<(Span, &'static str)>,
}
impl Effects {
pub fn then(mut self, other: Effects) -> Effects {
self.contributions.extend(other.contributions);
self.unknown.extend(other.unknown);
self
}
pub fn scaled(self, count: Count, factor: Option<Factor>) -> Effects {
if count.is_zero() {
return Effects::default();
}
let contributions = self
.contributions
.into_iter()
.map(|mut c| {
c.transfers = c.transfers.mul(count);
if let Some(f) = &factor {
c.factors.insert(0, f.clone());
}
c
})
.collect();
Effects {
contributions,
unknown: self.unknown,
}
}
pub fn alternatives(alts: Vec<Effects>) -> Effects {
let n = alts.len();
let mut merged: Vec<Contribution> = Vec::new();
let mut unknown = Vec::new();
type Key = (TargetSet, Auth, &'static str, bool);
let mut classes: Vec<(Key, Vec<Count>, Contribution)> = Vec::new();
for (i, alt) in alts.into_iter().enumerate() {
unknown.extend(alt.unknown);
for c in alt.contributions {
let key: Key = (c.targets.clone(), c.auth, c.program, c.paced.is_some());
match classes.iter_mut().find(|(k, _, _)| *k == key) {
Some((_, per_branch, rep)) => {
per_branch[i] = per_branch[i].add(c.transfers);
rep.paced = rep.paced.zip(c.paced).map(|(a, b)| Pace {
secs: a.secs.min(b.secs),
burst: a.burst.join(b.burst),
});
for f in c.factors {
if !rep.factors.contains(&f) {
rep.factors.push(f);
}
}
for u in c.uncounted {
if !rep.uncounted.contains(&u) {
rep.uncounted.push(u);
}
}
}
None => {
let mut per_branch = vec![Count::ZERO; n];
per_branch[i] = c.transfers;
classes.push((key, per_branch, c));
}
}
}
}
for (_, per_branch, mut rep) in classes {
rep.transfers = per_branch
.into_iter()
.reduce(Count::join)
.unwrap_or(Count::ZERO);
merged.push(rep);
}
Effects {
contributions: merged,
unknown,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn curl_to(host: &str, at: usize) -> Contribution {
Contribution {
program: "curl",
transfers: Count::ONE,
targets: TargetSet::host(host),
auth: Auth::Other,
factors: Vec::new(),
uncounted: Vec::new(),
span: at..at + 4,
paced: None,
}
}
fn one(c: Contribution) -> Effects {
Effects {
contributions: vec![c],
unknown: Vec::new(),
}
}
#[test]
fn exclusive_branches_to_one_host_join_rather_than_add() {
let e = Effects::alternatives(vec![one(curl_to("a", 0)), one(curl_to("a", 10))]);
assert_eq!(e.contributions.len(), 1);
assert_eq!(e.contributions[0].transfers, Count::ONE);
let looped = e.scaled(Count::exactly(60), None);
assert_eq!(looped.contributions[0].transfers, Count::exactly(60));
}
#[test]
fn a_call_in_one_branch_only_is_maybe() {
let e = Effects::alternatives(vec![one(curl_to("a", 0)), Effects::default()]);
assert_eq!(e.contributions[0].transfers, Count::MAYBE);
}
#[test]
fn different_destinations_stay_separate() {
let e = Effects::alternatives(vec![one(curl_to("a", 0)), one(curl_to("b", 10))]);
assert_eq!(e.contributions.len(), 2);
assert!(e.contributions.iter().all(|c| c.transfers == Count::MAYBE));
}
#[test]
fn a_proven_zero_removes_contributions() {
let e = one(curl_to("a", 0)).scaled(Count::ZERO, None);
assert!(e.contributions.is_empty());
}
#[test]
fn sequence_keeps_both() {
let e = one(curl_to("a", 0)).then(one(curl_to("a", 10)));
assert_eq!(e.contributions.len(), 2);
}
}