use proptest::prelude::*;
use sloop::flow::{Actor, Builtin, Check, FailAction, Flow, Stage};
#[derive(Debug, Clone)]
pub enum WrittenAction {
Agent,
AgentMapping(String),
Exec(Vec<String>),
Merge,
}
impl WrittenAction {
fn expected(&self) -> Actor {
match self {
Self::Agent | Self::AgentMapping(_) => Actor::Agent,
Self::Exec(cmd) => Actor::Exec { cmd: cmd.clone() },
Self::Merge => Actor::Builtin(Builtin::Merge),
}
}
fn render(&self) -> String {
match self {
Self::Agent => "agent".to_owned(),
Self::AgentMapping(prompt) => format!("{{ agent: {} }}", quote(prompt)),
Self::Exec(cmd) => format!("{{ exec: {} }}", render_command(cmd)),
Self::Merge => "{ builtin: merge }".to_owned(),
}
}
}
#[derive(Debug, Clone)]
pub enum WrittenCheck {
Omitted,
None,
Commits,
Reported,
Exec(Vec<String>),
}
impl WrittenCheck {
fn expected(&self, action: &Actor) -> Check {
match self {
Self::Omitted => match action {
Actor::Agent => Check::Actor(Actor::Builtin(Builtin::Commits)),
Actor::Exec { .. } | Actor::Builtin(_) => Check::None,
},
Self::None => Check::None,
Self::Commits => Check::Actor(Actor::Builtin(Builtin::Commits)),
Self::Reported => Check::Reported,
Self::Exec(cmd) => Check::Actor(Actor::Exec { cmd: cmd.clone() }),
}
}
fn render(&self) -> Option<String> {
match self {
Self::Omitted => None,
Self::None => Some("none".to_owned()),
Self::Commits => Some("{ builtin: commits }".to_owned()),
Self::Reported => Some("reported".to_owned()),
Self::Exec(cmd) => Some(format!("{{ exec: {} }}", render_command(cmd))),
}
}
}
#[derive(Debug, Clone)]
pub struct WrittenStage {
pub action: WrittenAction,
pub result_check: WrittenCheck,
}
fn command() -> impl Strategy<Value = Vec<String>> {
prop::collection::vec("[a-zA-Z0-9./_-]{1,8}", 1..4)
}
fn result_check() -> impl Strategy<Value = WrittenCheck> {
prop_oneof![
Just(WrittenCheck::Omitted),
Just(WrittenCheck::None),
Just(WrittenCheck::Commits),
Just(WrittenCheck::Reported),
command().prop_map(WrittenCheck::Exec),
]
}
fn agent_check() -> impl Strategy<Value = WrittenCheck> {
prop_oneof![
Just(WrittenCheck::Omitted),
Just(WrittenCheck::Commits),
Just(WrittenCheck::Reported),
command().prop_map(WrittenCheck::Exec),
]
}
fn agent_stage() -> impl Strategy<Value = WrittenStage> {
let action = prop_oneof![
Just(WrittenAction::Agent),
"[a-z][a-z ]{0,15}".prop_map(WrittenAction::AgentMapping),
];
(action, agent_check()).prop_map(|(action, result_check)| WrittenStage {
action,
result_check,
})
}
fn exec_stage() -> impl Strategy<Value = WrittenStage> {
(command(), result_check()).prop_map(|(cmd, result_check)| WrittenStage {
action: WrittenAction::Exec(cmd),
result_check,
})
}
fn merge_stage() -> impl Strategy<Value = WrittenStage> {
let check = prop_oneof![Just(WrittenCheck::Omitted), Just(WrittenCheck::None)];
check.prop_map(|result_check| WrittenStage {
action: WrittenAction::Merge,
result_check,
})
}
fn quote(value: &str) -> String {
format!("\"{value}\"")
}
fn render_command(cmd: &[String]) -> String {
let quoted: Vec<String> = cmd.iter().map(|part| quote(part)).collect();
format!("[{}]", quoted.join(", "))
}
fn render_stage(name: &str, stage: &WrittenStage, indent: &str) -> String {
let mut yaml = format!(
"{indent}- name: {}\n{indent} action: {}\n",
quote(name),
stage.action.render()
);
if let Some(check) = stage.result_check.render() {
yaml.push_str(&format!("{indent} result_check: {check}\n"));
}
yaml
}
fn expected_stage(name: &str, stage: &WrittenStage) -> Stage {
let action = stage.action.expected();
let result_check = stage.result_check.expected(&action);
Stage {
name: name.to_owned(),
action,
result_check,
fail_action: FailAction::Halt,
ff_only: false,
}
}
pub fn flow_file() -> impl Strategy<Value = (String, Flow)> {
(
agent_stage(),
prop::collection::vec(exec_stage(), 0..3),
prop::option::of(merge_stage()),
any::<bool>(),
)
.prop_flat_map(|(agent, execs, merge, as_map)| {
let count = 1 + execs.len() + usize::from(merge.is_some());
(
Just((agent, execs, merge, as_map)),
prop::collection::btree_set("[a-z][a-z0-9_]{0,7}", count),
)
})
.prop_map(|((agent, execs, merge, as_map), names)| {
let names: Vec<String> = names.into_iter().collect();
let mut written: Vec<(&str, &WrittenStage)> = vec![(&names[0], &agent)];
for (index, exec) in execs.iter().enumerate() {
written.push((&names[1 + index], exec));
}
if let Some(merge) = &merge {
written.push((names.last().expect("names cover every stage"), merge));
}
let indent = if as_map { " " } else { "" };
let mut yaml = if as_map {
"stages:\n".to_owned()
} else {
String::new()
};
let mut stages = Vec::with_capacity(written.len());
for (name, stage) in &written {
yaml.push_str(&render_stage(name, stage, indent));
stages.push(expected_stage(name, stage));
}
let flow = Flow {
name: "generated".into(),
stages,
};
(yaml, flow)
})
}