use std::{collections::BTreeSet, fmt::Write as _};
use proptest::prelude::*;
use super::{pull_request_cases::parse, reader};
const MAX_WORKFLOWS: usize = 6;
const CALL_PREFIXES: [&str; 3] = ["./", "$/", ""];
#[derive(Debug, Clone)]
struct CallGraph {
starts_on_pull_request: Vec<bool>,
calls: Vec<bool>,
spellings: Vec<usize>,
}
impl CallGraph {
fn len(&self) -> usize { self.starts_on_pull_request.len() }
fn workflow(&self, i: usize) -> String {
let n = self.len();
let trigger = if self.starts_on_pull_request[i] {
"pull_request"
} else {
"workflow_call"
};
let jobs: String =
(0..n)
.filter(|j| self.calls[i * n + j])
.fold(String::new(), |mut jobs, j| {
let prefix = CALL_PREFIXES[self.spellings[i * n + j]];
let _ = writeln!(
jobs,
" call_{j}:\n uses: {prefix}.github/workflows/w{j}.yml"
);
jobs
});
if jobs.is_empty() {
format!("on: {trigger}\njobs: {{}}\n")
} else {
format!("on: {trigger}\njobs:\n{jobs}")
}
}
fn expected_closure(&self) -> BTreeSet<String> {
let n = self.len();
let mut reach = self.calls.clone();
for i in 0..n {
reach[i * n + i] = true;
}
for k in 0..n {
for i in 0..n {
for j in 0..n {
if reach[i * n + k] && reach[k * n + j] {
reach[i * n + j] = true;
}
}
}
}
(0..n)
.filter(|&j| (0..n).any(|i| self.starts_on_pull_request[i] && reach[i * n + j]))
.map(|j| format!("w{j}.yml"))
.collect()
}
}
fn call_graph() -> impl Strategy<Value = CallGraph> {
(1..=MAX_WORKFLOWS).prop_flat_map(|n| {
(
prop::collection::vec(any::<bool>(), n),
prop::collection::vec(prop::bool::weighted(0.3), n * n),
prop::collection::vec(0..CALL_PREFIXES.len(), n * n),
)
.prop_map(|(starts_on_pull_request, calls, spellings)| CallGraph {
starts_on_pull_request,
calls,
spellings,
})
})
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(256))]
#[test]
fn the_closure_is_exactly_what_a_pull_request_can_reach(graph in call_graph()) {
let mut all = reader::Workflows::new();
for i in 0..graph.len() {
let parsed = parse(&graph.workflow(i));
prop_assert!(parsed.is_ok(), "workflow {i} did not parse: {:?}", parsed.err());
if let Ok(workflow) = parsed {
all.insert(format!("w{i}.yml"), workflow);
}
}
prop_assert_eq!(reader::pull_request_closure(&all), graph.expected_closure());
}
}