#[cfg(feature = "metacall-deploy")]
mod deploy_tests {
use meta_ast::deploy::{DeployConfig, run_deploy};
use meta_ast::output::OutputFormat;
use std::fs;
use std::path::PathBuf;
use tempfile::tempdir;
#[test]
fn test_deploy_python_calls_js() {
let root =
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/mixed/python_calls_js");
let out_dir = tempdir().unwrap();
let out_path = out_dir.path().to_path_buf();
let config = DeployConfig {
root: root.clone(),
out: out_path.clone(),
format: OutputFormat::Json,
check: false,
max_pod_size: 20,
};
run_deploy(config).expect("Deploy failed");
assert!(out_path.join("metacall.pods.json").exists());
assert!(out_path.join("metacall.mesh.json").exists());
let pod_content = fs::read_to_string(out_path.join("metacall.pods.json")).unwrap();
let pod_json: serde_json::Value = serde_json::from_str(&pod_content).unwrap();
assert_eq!(pod_json["version"], "1.1");
assert!(!pod_json["deployments"].as_array().unwrap().is_empty());
let languages: Vec<&str> = pod_json["deployments"]
.as_array()
.unwrap()
.iter()
.filter_map(|d| d["language"].as_str())
.collect();
assert!(languages.contains(&"py"), "expected a Python pod");
assert!(languages.contains(&"node"), "expected a Node pod");
let mesh_content = fs::read_to_string(out_path.join("metacall.mesh.json")).unwrap();
let mesh_json: serde_json::Value = serde_json::from_str(&mesh_content).unwrap();
assert_eq!(mesh_json["version"], "1.0");
assert!(mesh_json["deployment_units"].as_array().unwrap().len() >= 2);
}
#[test]
fn test_deploy_check_mode_pass() {
let root =
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/mixed/python_calls_js");
let out_dir = tempdir().unwrap();
let out_path = out_dir.path().to_path_buf();
copy_dir_recursive(&root, &out_path).unwrap();
let config = DeployConfig {
root: out_path.clone(),
out: out_path.clone(),
format: OutputFormat::Json,
check: true,
max_pod_size: 20,
};
let result = run_deploy(config);
assert!(
result.is_ok(),
"check mode should pass without pre-existing metadata"
);
}
#[test]
fn max_pod_size_zero_returns_error() {
let root = tempdir().unwrap();
let config = DeployConfig {
root: root.path().to_path_buf(),
out: root.path().to_path_buf(),
format: OutputFormat::Json,
check: false,
max_pod_size: 0,
};
assert!(run_deploy(config).is_err());
}
#[test]
fn max_pod_size_forces_oversized_pod_cut() {
let fixture = tempdir().unwrap();
let root = fixture.path().join("proj");
fs::create_dir_all(&root).unwrap();
fs::write(root.join("a.py"), "import b\n").unwrap();
fs::write(root.join("b.py"), "import a\n").unwrap();
let run = |out_path: &std::path::Path, max_pod_size: usize| -> serde_json::Value {
let config = DeployConfig {
root: root.clone(),
out: out_path.to_path_buf(),
format: OutputFormat::Json,
check: false,
max_pod_size,
};
run_deploy(config).expect("Deploy failed");
let content = fs::read_to_string(out_path.join("metacall.pods.json")).unwrap();
serde_json::from_str(&content).unwrap()
};
let out_default = tempdir().unwrap();
let default_manifest = run(
out_default.path(),
meta_ast::deploy::cut::DEFAULT_MAX_POD_SIZE,
);
let oversized_stubs: Vec<&serde_json::Value> = default_manifest["edges"]
.as_array()
.unwrap()
.iter()
.filter(|e| e["kind"].as_str() == Some("rpc_stub"))
.filter(|e| {
e["cut_annotations"].as_array().is_some_and(|cuts| {
cuts.iter()
.any(|cut| cut["cut_reason"].get("OversizedPod").is_some())
})
})
.collect();
assert!(
oversized_stubs.is_empty(),
"expected no OversizedPod rpc_stub edge at the default threshold, got {oversized_stubs:?}"
);
let out_small = tempdir().unwrap();
let small_manifest = run(out_small.path(), 1);
let oversized: Vec<&serde_json::Value> = small_manifest["edges"]
.as_array()
.unwrap()
.iter()
.filter(|e| e["kind"].as_str() == Some("rpc_stub"))
.filter(|e| {
e["cut_annotations"].as_array().is_some_and(|cuts| {
cuts.iter()
.any(|cut| cut["cut_reason"].get("OversizedPod").is_some())
})
})
.collect();
assert!(
!oversized.is_empty(),
"expected an OversizedPod rpc_stub edge at max_pod_size 1, edges: {}",
small_manifest["edges"]
);
}
#[test]
fn config_path_resolves_scripts_relative_to_the_configuration() {
let fixture = tempdir().unwrap();
let proj = fixture.path();
fs::create_dir_all(proj.join("cfg")).unwrap();
fs::create_dir_all(proj.join("lib")).unwrap();
fs::write(
proj.join("caller.py"),
"from metacall import metacall_load_from_configuration\nmetacall_load_from_configuration('cfg/deploy.json')\n",
)
.unwrap();
fs::write(
proj.join("cfg/deploy.json"),
r#"{"language_id":"node","path":"../lib","scripts":["extra.js"]}"#,
)
.unwrap();
fs::write(
proj.join("lib/extra.js"),
"function helper() { return 1; }\n",
)
.unwrap();
let out = tempdir().unwrap();
let config = DeployConfig {
root: proj.to_path_buf(),
out: out.path().to_path_buf(),
format: OutputFormat::Json,
check: false,
max_pod_size: 20,
};
run_deploy(config).expect("Deploy failed");
let manifest: serde_json::Value = serde_json::from_str(
&fs::read_to_string(out.path().join("metacall.pods.json")).unwrap(),
)
.unwrap();
let files: Vec<&str> = manifest["deployments"]
.as_array()
.unwrap()
.iter()
.flat_map(|d| d["files"].as_array().unwrap().iter())
.filter_map(|f| f.as_str())
.collect();
assert!(
files.iter().any(|f| f.ends_with("lib/extra.js")),
"extra.js missing from pods: {files:?}"
);
}
#[test]
fn deploy_yaml_format_writes_yaml_artifacts() {
let root =
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/mixed/python_calls_js");
let out_dir = tempdir().unwrap();
let out_path = out_dir.path().to_path_buf();
let config = DeployConfig {
root,
out: out_path.clone(),
format: OutputFormat::Yaml,
check: false,
max_pod_size: 20,
};
run_deploy(config).expect("Deploy failed");
let pods = out_path.join("metacall.pods.yaml");
let mesh = out_path.join("metacall.mesh.yaml");
assert!(
pods.exists(),
"the pod manifest must be written in the requested format"
);
assert!(
mesh.exists(),
"the mesh annotation must be written in the requested format"
);
let pods_text = fs::read_to_string(&pods).unwrap();
assert!(
pods_text.contains("deployments"),
"pod manifest content: {pods_text}"
);
let mesh_text = fs::read_to_string(&mesh).unwrap();
assert!(
mesh_text.contains("deployment_units"),
"mesh content: {mesh_text}"
);
}
#[test]
fn deploy_reports_an_unknown_load_tag() {
let dir = tempdir().unwrap();
fs::write(
dir.path().join("app.py"),
"from metacall import metacall_load_from_file\n\nmetacall_load_from_file(\"wasm\", [\"a.wasm\"])\n",
)
.unwrap();
let output = std::process::Command::new(env!("CARGO_BIN_EXE_meta-ast"))
.arg("deploy")
.arg(dir.path())
.arg("--out")
.arg(dir.path().join("out"))
.env("RUST_LOG", "warn")
.output()
.unwrap();
let mut text = String::from_utf8_lossy(&output.stdout).to_string();
text.push_str(&String::from_utf8_lossy(&output.stderr));
assert!(
text.contains("wasm"),
"the unmappable tag must be named in a diagnostic, got:\n{text}"
);
}
#[test]
fn memory_load_is_not_named_by_its_code() {
let dir = tempdir().unwrap();
fs::write(
dir.path().join("package.json"),
"{\"name\":\"probe\",\"version\":\"1.0.0\"}\n",
)
.unwrap();
fs::write(
dir.path().join("app.py"),
"from metacall import metacall_load_from_memory\n\nmetacall_load_from_memory(\"node\", \"console.log(1)\")\n",
)
.unwrap();
let out_dir = tempdir().unwrap();
let config = DeployConfig {
root: dir.path().to_path_buf(),
out: out_dir.path().to_path_buf(),
format: OutputFormat::Json,
check: false,
max_pod_size: 20,
};
run_deploy(config).expect("Deploy failed");
let manifest: serde_json::Value = serde_json::from_str(
&fs::read_to_string(out_dir.path().join("metacall.pods.json")).unwrap(),
)
.unwrap();
let names: Vec<String> = manifest["deployments"]
.as_array()
.unwrap()
.iter()
.flat_map(|d| d["dependencies"].as_array().cloned().unwrap_or_default())
.filter_map(|dep| dep["name"].as_str().map(str::to_string))
.collect();
assert!(
!names.iter().any(|name| name.contains("console.log")),
"a memory load must not name a node after its inline code: {names:?}"
);
}
fn copy_dir_recursive(src: &std::path::Path, dst: &std::path::Path) -> std::io::Result<()> {
fs::create_dir_all(dst)?;
for entry in fs::read_dir(src)? {
let entry = entry?;
let file_type = entry.file_type()?;
if file_type.is_dir() {
copy_dir_recursive(&entry.path(), &dst.join(entry.file_name()))?;
} else {
fs::copy(entry.path(), dst.join(entry.file_name()))?;
}
}
Ok(())
}
}