use std::path::Path;
use std::process::{Command, Output};
fn build_workspace(root: &Path) {
std::fs::create_dir_all(root.join("graphs")).unwrap();
std::fs::create_dir_all(root.join("schemas")).unwrap();
std::fs::write(
root.join("Cargo.toml"),
"[workspace]\nresolver = \"2\"\nmembers = []\n",
)
.unwrap();
}
fn run_node(root: &Path, args: &[&str]) -> Output {
Command::new(env!("CARGO_BIN_EXE_cerulion"))
.arg("node")
.args(args)
.current_dir(root)
.output()
.expect("failed to spawn cerulion binary")
}
fn stderr_of(out: &Output) -> String {
String::from_utf8_lossy(&out.stderr).into_owned()
}
fn read_prod_lib(root: &Path) -> String {
std::fs::read_to_string(root.join("nodes/prod/src/lib.rs")).unwrap()
}
fn create_prod(root: &Path) {
let out = run_node(root, &["create", "prod", "--policy", "period_ms=10"]);
assert_eq!(
out.status.code(),
Some(0),
"node create must succeed; stderr: {}",
stderr_of(&out)
);
}
#[test]
fn bare_builtin_modify_resolves_end_to_end_with_note() {
let tmp = tempfile::tempdir().unwrap();
build_workspace(tmp.path());
create_prod(tmp.path());
let out = run_node(tmp.path(), &["modify", "prod", "-o", "Vector3", "out"]);
assert_eq!(out.status.code(), Some(0), "stderr: {}", stderr_of(&out));
let stderr = stderr_of(&out);
assert!(
stderr.contains(
"note: resolved bare schema 'Vector3' to built-in 'geometry_msgs/Vector3' \
(qualify it to silence this note)"
),
"stderr must carry the exact resolution note: {stderr}"
);
let src = read_prod_lib(tmp.path());
assert!(
src.contains("use native_ros2_messages::geometry_msgs::Vector3;"),
"lib.rs must carry the qualified import: {src}"
);
assert!(
!src.contains("use Vector3;"),
"the pre-fix E0432 bare import must NOT be emitted: {src}"
);
}
#[test]
fn bare_builtin_create_resolves_end_to_end_with_note() {
let tmp = tempfile::tempdir().unwrap();
build_workspace(tmp.path());
let out = run_node(
tmp.path(),
&[
"create",
"prod",
"-o",
"Vector3",
"out",
"--policy",
"period_ms=10",
],
);
assert_eq!(out.status.code(), Some(0), "stderr: {}", stderr_of(&out));
let stderr = stderr_of(&out);
assert!(
stderr.contains(
"note: resolved bare schema 'Vector3' to built-in 'geometry_msgs/Vector3' \
(qualify it to silence this note)"
),
"stderr must carry the exact resolution note: {stderr}"
);
let src = read_prod_lib(tmp.path());
assert!(
src.contains("use native_ros2_messages::geometry_msgs::Vector3;"),
"lib.rs must carry the qualified import: {src}"
);
assert!(
!src.contains("use Vector3;"),
"the pre-fix E0432 bare import must NOT be emitted: {src}"
);
}
#[test]
fn bare_builtin_modify_input_resolves_with_note() {
let tmp = tempfile::tempdir().unwrap();
build_workspace(tmp.path());
create_prod(tmp.path());
let out = run_node(tmp.path(), &["modify", "prod", "-i", "Vector3", "vin"]);
assert_eq!(out.status.code(), Some(0), "stderr: {}", stderr_of(&out));
let stderr = stderr_of(&out);
assert!(
stderr.contains(
"note: resolved bare schema 'Vector3' to built-in 'geometry_msgs/Vector3' \
(qualify it to silence this note)"
),
"stderr must carry the exact resolution note: {stderr}"
);
let src = read_prod_lib(tmp.path());
assert!(
src.contains("use native_ros2_messages::geometry_msgs::Vector3;"),
"lib.rs must carry the qualified import: {src}"
);
assert!(
src.contains("#[input]"),
"lib.rs must carry a regular #[input] field: {src}"
);
assert!(
src.contains("vin: Vector3,"),
"lib.rs must carry the typed input field: {src}"
);
assert!(
!src.contains("use Vector3;"),
"the pre-fix E0432 bare import must NOT be emitted: {src}"
);
}
#[test]
fn bare_builtin_trigger_input_resolves_with_note() {
let tmp = tempfile::tempdir().unwrap();
build_workspace(tmp.path());
create_prod(tmp.path());
let out = run_node(tmp.path(), &["modify", "prod", "-T", "Vector3", "trig"]);
assert_eq!(out.status.code(), Some(0), "stderr: {}", stderr_of(&out));
let stderr = stderr_of(&out);
assert!(
stderr.contains(
"note: resolved bare schema 'Vector3' to built-in 'geometry_msgs/Vector3' \
(qualify it to silence this note)"
),
"stderr must carry the exact resolution note: {stderr}"
);
let src = read_prod_lib(tmp.path());
assert!(
src.contains("use native_ros2_messages::geometry_msgs::Vector3;"),
"lib.rs must carry the qualified import: {src}"
);
assert!(
src.contains("#[input(trigger)]"),
"lib.rs must mark the new field as the trigger: {src}"
);
assert!(
src.contains("trig: Vector3,"),
"lib.rs must carry the typed trigger field: {src}"
);
assert!(
!src.contains("use Vector3;"),
"the pre-fix E0432 bare import must NOT be emitted: {src}"
);
}
#[test]
fn ambiguous_bare_name_exits_nonzero_and_leaves_source_untouched() {
let tmp = tempfile::tempdir().unwrap();
build_workspace(tmp.path());
create_prod(tmp.path());
let before = read_prod_lib(tmp.path());
let out = run_node(tmp.path(), &["modify", "prod", "-o", "Pose2D", "p"]);
assert_ne!(out.status.code(), Some(0), "must exit nonzero");
let stderr = stderr_of(&out);
assert!(
stderr.contains("ambiguous"),
"stderr must state the ambiguity: {stderr}"
);
assert!(
stderr.contains("geometry_msgs/Pose2D") && stderr.contains("vision_msgs/Pose2D"),
"stderr must list BOTH candidate packages: {stderr}"
);
assert_eq!(
before,
read_prod_lib(tmp.path()),
"lib.rs must be byte-unchanged after the failed modify"
);
}
#[test]
fn unknown_bare_name_exits_nonzero_with_schema_list_remedy() {
let tmp = tempfile::tempdir().unwrap();
build_workspace(tmp.path());
create_prod(tmp.path());
let out = run_node(tmp.path(), &["modify", "prod", "-o", "Vectorr3", "v"]);
assert_ne!(out.status.code(), Some(0), "must exit nonzero");
let stderr = stderr_of(&out);
assert!(
stderr.contains("Schema 'Vectorr3' not found"),
"stderr must name the missing schema: {stderr}"
);
assert!(
stderr.contains(
"Run 'cerulion schema list' to see every available schema, or qualify as 'pkg/Type'"
),
"stderr must carry the schema-list remedy: {stderr}"
);
}
#[test]
fn workspace_schema_wins_bare_preserved_with_shadow_warning() {
let tmp = tempfile::tempdir().unwrap();
build_workspace(tmp.path());
create_prod(tmp.path());
std::fs::write(
tmp.path().join("schemas/Vector3.yaml"),
"schemas:\n Vector3:\n fields:\n float64 x:\n",
)
.unwrap();
let out = run_node(tmp.path(), &["modify", "prod", "-o", "Vector3", "w"]);
assert_eq!(out.status.code(), Some(0), "stderr: {}", stderr_of(&out));
let stderr = stderr_of(&out);
assert!(
stderr.contains(
"WARNING: workspace schema 'Vector3' shadows built-in 'geometry_msgs/Vector3' \
— workspace wins for node ports"
),
"stderr must carry the exact shadow warning: {stderr}"
);
let src = read_prod_lib(tmp.path());
assert!(
src.contains("use Vector3;"),
"workspace bare name must pass through unchanged: {src}"
);
assert!(
!src.contains("use native_ros2_messages::geometry_msgs::Vector3;"),
"workspace name must NOT be hijacked to the built-in path: {src}"
);
}
#[test]
fn qualified_name_is_silent_on_stderr() {
let tmp = tempfile::tempdir().unwrap();
build_workspace(tmp.path());
create_prod(tmp.path());
let out = run_node(
tmp.path(),
&["modify", "prod", "-o", "geometry_msgs::Vector3", "q"],
);
assert_eq!(out.status.code(), Some(0), "stderr: {}", stderr_of(&out));
let stderr = stderr_of(&out);
assert!(
!stderr.contains("note: resolved bare schema"),
"a qualified name must not print the resolution note: {stderr}"
);
assert!(
!stderr.contains("WARNING: workspace schema"),
"a qualified name must not print the shadow warning: {stderr}"
);
let src = read_prod_lib(tmp.path());
assert!(
src.contains("use native_ros2_messages::geometry_msgs::Vector3;"),
"the qualified import must be spliced as before: {src}"
);
}