use std::fmt::Write as _;
use super::activity_model::ResolvedActivity;
pub(crate) const GENERATED_TEST_HEADER: &str =
"//// Generated by aion generate — a test scaffold to fill in, not a do-not-edit file.";
pub(crate) struct WorkflowTestFacts<'a> {
pub(crate) entry_module: &'a str,
pub(crate) entry_function: &'a str,
pub(crate) timer_count: usize,
}
pub(crate) fn emit_scaffold_module(
package_name: &str,
facts: &WorkflowTestFacts<'_>,
activities: &[ResolvedActivity<'_>],
) -> String {
let mut out = String::new();
let _ = writeln!(out, "{GENERATED_TEST_HEADER}");
out.push_str("////\n");
let _ = writeln!(
out,
"//// A pure `aion/testing` skeleton for the `{}` workflow: each activity",
facts.entry_module
);
out.push_str(
"//// is pre-mocked, the simulated clock is advanced once per durable timer,\n\
//// and the entry function is asserted to replay deterministically. Fill in\n\
//// each `todo` (every mock's returned output and the workflow input) to make\n\
//// the scenario concrete; the harness needs no engine, store, or beamr.\n",
);
out.push('\n');
out.push_str("import aion/testing\n");
out.push_str("import aion/duration\n");
let _ = writeln!(out, "import {} as workflow_under_test", facts.entry_module);
let _ = writeln!(out, "import {package_name}_activity_wrappers as wrappers");
out.push_str("import gleeunit\n");
out.push_str("\npub fn main() {\n gleeunit.main()\n}\n");
let _ = write!(
out,
"\n/// Replay-determinism scaffold for `{}.{}`. Every activity is mocked, the\n\
/// clock is advanced once per durable timer, and the entry function is\n\
/// asserted to emit an identical observable command sequence on replay.\n",
facts.entry_module, facts.entry_function
);
out.push_str("pub fn replay_determinism_scaffold_test() {\n");
out.push_str(" let assert Ok(env) = testing.new()\n\n");
if activities.is_empty() {
out.push_str(" // This workflow declares no activities, so there is nothing to mock.\n");
} else {
out.push_str(" // One typed mock per declared activity. Replace each `todo` with the\n");
out.push_str(" // output the activity should return in this scenario.\n");
}
for activity in activities {
emit_activity_mock(&mut out, activity);
}
if facts.timer_count == 0 {
out.push_str(
"\n // This workflow arms no durable timers, so the clock is not advanced.\n",
);
} else {
out.push_str(
"\n // One clock advance per durable timer this workflow arms. Replace each\n\
\x20\x20// `todo` with the duration to advance past that timer's deadline.\n",
);
for index in 0..facts.timer_count {
let _ = write!(
out,
" let assert Ok(env) =\n \
testing.advance(env, todo as \"duration past durable timer {}\")\n",
index + 1
);
}
}
let _ = write!(
out,
"\n // Drive the typed entry function and assert it replays deterministically.\n\
\x20\x20// Replace the `todo` with the workflow input for this scenario.\n\
\x20\x20let result =\n \
testing.assert_replay(env, fn() {{\n \
workflow_under_test.{}(todo as \"workflow input\")\n \
}})\n",
facts.entry_function
);
out.push_str(" let assert Ok(_) = result\n");
out.push_str("}\n");
out
}
fn emit_activity_mock(out: &mut String, activity: &ResolvedActivity<'_>) {
let name = &activity.declaration.name;
let _ = write!(
out,
" let assert Ok(env) =\n \
testing.mock_activity(\n \
env,\n \
wrappers.{name}_activity(todo as \"input the {name} wrapper is built with\"),\n \
fn(_input) {{ todo as \"the {name} activity's mocked output\" }},\n \
)\n"
);
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use super::{WorkflowTestFacts, emit_scaffold_module};
use crate::codegen::activity_model::{ResolvedActivity, ResolvedType};
use crate::codegen::declaration::{ActivityDeclaration, Tier};
use crate::codegen::schema::{GleamType, SchemaArtifact};
fn artifact() -> SchemaArtifact {
SchemaArtifact {
file: PathBuf::from("schemas/placeholder.json"),
stem: "placeholder".to_owned(),
root: GleamType::String,
defs: Vec::new(),
}
}
fn declaration(name: &str) -> ActivityDeclaration {
ActivityDeclaration {
name: name.to_owned(),
tier: Tier::InVm,
input_type: "Order".to_owned(),
output_type: "Receipt".to_owned(),
}
}
fn resolved<'a>(
declaration: &'a ActivityDeclaration,
art: &'a SchemaArtifact,
) -> ResolvedActivity<'a> {
ResolvedActivity {
declaration,
input: ResolvedType {
gleam_type: declaration.input_type.clone(),
fn_prefix: "order".to_owned(),
artifact: art,
},
output: ResolvedType {
gleam_type: declaration.output_type.clone(),
fn_prefix: "receipt".to_owned(),
artifact: art,
},
}
}
#[test]
fn scaffold_mocks_each_activity_and_advances_per_timer() {
let art = artifact();
let reserve = declaration("reserve_inventory");
let charge = declaration("charge_payment");
let activities = [resolved(&reserve, &art), resolved(&charge, &art)];
let facts = WorkflowTestFacts {
entry_module: "order_saga",
entry_function: "execute",
timer_count: 2,
};
let module = emit_scaffold_module("demo", &facts, &activities);
assert!(module.starts_with(super::GENERATED_TEST_HEADER));
assert!(module.contains("import aion/testing\n"));
assert!(module.contains("import gleeunit\n"));
assert!(module.contains("import order_saga as workflow_under_test\n"));
assert!(module.contains("import demo_activity_wrappers as wrappers\n"));
assert_eq!(module.matches("testing.mock_activity(").count(), 2);
assert!(module.contains("wrappers.reserve_inventory_activity("));
assert!(module.contains("wrappers.charge_payment_activity("));
assert_eq!(module.matches("testing.advance(").count(), 2);
assert!(module.contains("testing.assert_replay(env, fn()"));
assert!(module.contains("workflow_under_test.execute(todo"));
assert!(module.contains("todo as \"workflow input\""));
assert!(module.contains("todo as \"the reserve_inventory activity's mocked output\""));
}
#[test]
fn scaffold_with_no_timers_does_not_advance_the_clock() {
let art = artifact();
let only = declaration("greet");
let activities = [resolved(&only, &art)];
let facts = WorkflowTestFacts {
entry_module: "hello",
entry_function: "execute",
timer_count: 0,
};
let module = emit_scaffold_module("hello", &facts, &activities);
assert_eq!(module.matches("testing.advance(").count(), 0);
assert!(module.contains("arms no durable timers"));
}
#[test]
fn scaffold_with_no_activities_mocks_nothing_but_still_asserts_replay() {
let facts = WorkflowTestFacts {
entry_module: "pure_flow",
entry_function: "execute",
timer_count: 1,
};
let module = emit_scaffold_module("pure_flow", &facts, &[]);
assert_eq!(module.matches("testing.mock_activity(").count(), 0);
assert!(module.contains("declares no activities"));
assert_eq!(module.matches("testing.advance(").count(), 1);
assert!(module.contains("testing.assert_replay(env, fn()"));
}
}