use std::path::{Path, PathBuf};
use std::process::Command;
use aion_package::{
ActivityDeclaration, CodegenMode, Tier, boundary_types_from_interface, emit_schemas,
generate_activities, generate_codecs,
};
type TestError = Box<dyn std::error::Error>;
const GLEAM_TOML: &str = "name = \"demo\"\nversion = \"0.1.0\"\ntarget = \"erlang\"\n";
const INTERFACE_JSON: &str = r#"{
"name": "demo",
"version": "0.1.0",
"modules": {
"demo_io": {
"type-aliases": {}, "constants": {}, "functions": {},
"types": {
"ChargeInput": {
"parameters": 0,
"constructors": [{
"name": "ChargeInput",
"parameters": [
{ "label": "order_id", "type": { "kind": "named", "name": "String", "module": "gleam", "package": "", "parameters": [] } },
{ "label": "note", "type": { "kind": "named", "name": "Option", "module": "gleam/option", "package": "gleam_stdlib", "parameters": [ { "kind": "named", "name": "String", "module": "gleam", "package": "", "parameters": [] } ] } },
{ "label": "tags", "type": { "kind": "named", "name": "List", "module": "gleam", "package": "", "parameters": [ { "kind": "named", "name": "String", "module": "gleam", "package": "", "parameters": [] } ] } },
{ "label": "match", "type": { "kind": "named", "name": "String", "module": "gleam", "package": "", "parameters": [] } },
{ "label": "customer", "type": { "kind": "named", "name": "ChargeInputCustomer", "module": "demo_io", "package": "demo", "parameters": [] } },
{ "label": "kind", "type": { "kind": "named", "name": "ChargeInputKind", "module": "demo_io", "package": "demo", "parameters": [] } }
]
}]
},
"ChargeInputCustomer": {
"parameters": 0,
"constructors": [{
"name": "ChargeInputCustomer",
"parameters": [
{ "label": "customer_id", "type": { "kind": "named", "name": "String", "module": "gleam", "package": "", "parameters": [] } },
{ "label": "vip", "type": { "kind": "named", "name": "Option", "module": "gleam/option", "package": "gleam_stdlib", "parameters": [ { "kind": "named", "name": "Bool", "module": "gleam", "package": "", "parameters": [] } ] } }
]
}]
},
"ChargeInputKind": {
"parameters": 0,
"constructors": [
{ "name": "ChargeInputKindStandard", "parameters": [] },
{ "name": "ChargeInputKindExpress", "parameters": [] }
]
},
"Receipt": {
"parameters": 0,
"constructors": [{
"name": "Receipt",
"parameters": [
{ "label": "payment_id", "type": { "kind": "named", "name": "String", "module": "gleam", "package": "", "parameters": [] } }
]
}]
}
}
}
}
}"#;
fn aion_worker_dir() -> Result<PathBuf, TestError> {
Ok(PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../aion-worker")
.canonicalize()?)
}
fn write_file(root: &Path, relative: &str, contents: &[u8]) -> Result<(), TestError> {
let path = root.join(relative);
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(&path, contents)?;
Ok(())
}
fn stage_source_project(root: &Path) -> Result<(), TestError> {
write_file(root, "gleam.toml", GLEAM_TOML.as_bytes())?;
std::fs::create_dir_all(root.join("src"))?;
Ok(())
}
fn generate_worker(root: &Path) -> Result<(), TestError> {
let types = boundary_types_from_interface(INTERFACE_JSON.as_bytes(), "demo")?;
generate_codecs(root, &types, CodegenMode::Write)?;
emit_schemas(root, "demo", &types, CodegenMode::Write)?;
let declarations = vec![ActivityDeclaration {
name: "charge_payment".to_owned(),
tier: Tier::RemoteRust,
input_type: "ChargeInput".to_owned(),
output_type: "Receipt".to_owned(),
}];
generate_activities(root, &declarations, &types, CodegenMode::Write)?;
let main_rs = root.join("worker/src/main.rs");
if !main_rs.is_file() {
return Err(format!(
"generation did not emit the RemoteRust worker at {}",
main_rs.display()
)
.into());
}
Ok(())
}
fn write_worker_cargo_toml(root: &Path, worker_dir: &Path) -> Result<(), TestError> {
let manifest = format!(
r#"[package]
name = "generated-rust-worker"
version = "0.0.0"
edition = "2024"
publish = false
# Detach from the aion workspace: this crate stands in for the worker a
# deployment would build against the SDK, so it resolves its own dep tree.
[workspace]
[[bin]]
name = "generated-rust-worker"
path = "src/main.rs"
[dependencies]
aion-worker = {{ path = "{worker}" }}
tokio = {{ version = "1", features = ["macros", "rt-multi-thread"] }}
serde = {{ version = "1", features = ["derive"] }}
"#,
worker = worker_dir.display(),
);
write_file(root, "worker/Cargo.toml", manifest.as_bytes())?;
Ok(())
}
fn write_handlers(root: &Path) -> Result<(), TestError> {
const HANDLERS: &str = "//! Hand-written activity bodies for the generated worker (test stub).\n\
\n\
use aion_worker::{ActivityContext, HandlerFuture};\n\
\n\
use crate::{ChargeInput, Receipt};\n\
\n\
/// Trivial stub satisfying the generated `register_activity` bound. The\n\
/// input is ignored and a zero-valued receipt is returned; the body is\n\
/// only ever type-checked, never run.\n\
pub fn charge_payment(input: ChargeInput, _context: &ActivityContext) -> HandlerFuture<'_, Receipt> {\n \
let _ = input;\n \
Box::pin(async move {\n \
Ok(Receipt {\n \
payment_id: String::new(),\n \
})\n \
})\n\
}\n";
write_file(root, "worker/src/handlers.rs", HANDLERS.as_bytes())?;
Ok(())
}
fn build_worker(root: &Path, target_dir: &Path) -> Result<(bool, String), TestError> {
let output = Command::new(env!("CARGO"))
.current_dir(root.join("worker"))
.arg("build")
.env("CARGO_TARGET_DIR", target_dir)
.output()
.map_err(|error| {
format!("failed to spawn `cargo build` for the generated worker: {error}")
})?;
let combined = format!(
"{}{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
Ok((output.status.success(), combined))
}
#[test]
fn generated_rust_worker_compiles_against_real_aion_worker() -> Result<(), TestError> {
if option_env!("CARGO").is_none() {
eprintln!(
"SKIP generated_rust_worker_compiles_against_real_aion_worker: \
`CARGO` is not set, cannot run the nested build"
);
return Ok(());
}
let worker_dir = aion_worker_dir()?;
let project = tempfile::tempdir()?;
let root = project.path();
stage_source_project(root)?;
generate_worker(root)?;
write_worker_cargo_toml(root, &worker_dir)?;
write_handlers(root)?;
let target = tempfile::tempdir()?;
let (built, output) = build_worker(root, target.path())?;
if !built {
return Err(format!(
"the generated RemoteRust worker failed to compile against the real \
`aion-worker` crate — this is a generator/SDK drift, not a test bug. \
Fix the emitter in crates/aion-package/src/codegen/activity_worker_rust.rs, \
never the generated file.\n\ncargo output:\n{output}"
)
.into());
}
let binary = target.path().join("debug/generated-rust-worker");
if !binary.exists() {
return Err(format!(
"cargo reported success but produced no worker binary at {}\n\ncargo output:\n{output}",
binary.display()
)
.into());
}
Ok(())
}