use std::fs;
use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};
fn unique_temp_dir(name: &str) -> PathBuf {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_nanos();
let path = std::env::temp_dir().join(format!(
"xidl-parser-include-{name}-{}-{nanos}",
std::process::id()
));
fs::create_dir_all(&path).expect("create temp dir");
path
}
fn write_file(path: &Path, source: &str) {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).expect("create parent dir");
}
fs::write(path, source).expect("write fixture");
}
fn parse_hir(path: &Path) -> xidl_parser::error::ParserResult<xidl_parser::hir::Specification> {
let source = fs::read_to_string(path).expect("read fixture");
let typed = xidl_parser::parser::parser_text_with_resolver(
&source,
Some(path.to_str().unwrap()),
&mut xidl_parser::hir::FsIncludeResolver,
)?;
xidl_parser::hir::Specification::from_typed_ast_with_path(typed, path)
}
fn parse_hir_with_mounts(
path: &Path,
mounts: std::collections::BTreeMap<String, PathBuf>,
) -> xidl_parser::error::ParserResult<xidl_parser::hir::Specification> {
let source = fs::read_to_string(path).expect("read fixture");
let typed = xidl_parser::parser::parser_text_with_resolver(
&source,
Some(path.to_str().unwrap()),
&mut xidl_parser::hir::MountIncludeResolver::new(mounts),
)?;
xidl_parser::hir::Specification::from_typed_ast_with_path(typed, path)
}
#[test]
fn test_hir_include_inserts_definitions_in_order() {
let root = unique_temp_dir("ordered");
let main = root.join("main.idl");
let shared = root.join("shared.idl");
write_file(
&main,
r#"
struct Before {
int32 value;
};
#include "shared.idl"
struct After {
int32 value;
};
"#,
);
write_file(
&shared,
r#"
struct Shared {
int32 value;
};
"#,
);
let hir = parse_hir(&main).unwrap();
insta::assert_debug_snapshot!("hir__include_ordered", hir);
}
#[test]
fn test_hir_nested_include_preserves_depth_first_order() {
let root = unique_temp_dir("nested");
let main = root.join("main.idl");
let middle = root.join("parts/middle.idl");
let leaf = root.join("parts/leaf.idl");
write_file(
&main,
r#"
struct Start {
int32 value;
};
#include "parts/middle.idl"
struct End {
int32 value;
};
"#,
);
write_file(
&middle,
r#"
struct Middle {
int32 value;
};
#include "leaf.idl"
"#,
);
write_file(
&leaf,
r#"
struct Leaf {
int32 value;
};
"#,
);
let hir = parse_hir(&main).unwrap();
insta::assert_debug_snapshot!("hir__include_nested", hir);
}
#[test]
fn test_hir_include_missing_file_errors() {
let root = unique_temp_dir("missing");
let main = root.join("main.idl");
write_file(&main, "#include \"missing.idl\"\n");
let err = parse_hir(&main).expect_err("missing include should fail");
assert!(err.to_string().contains("does not exist"));
assert!(err.to_string().contains("missing.idl"));
}
#[test]
fn test_hir_include_invalid_ast_errors() {
let root = unique_temp_dir("invalid");
let main = root.join("main.idl");
let invalid = root.join("invalid.idl");
write_file(&main, "#include \"invalid.idl\"\n");
write_file(&invalid, "struct Broken { int32 value \n");
let err = parse_hir(&main).expect_err("invalid include should fail");
assert!(err.to_string().contains("failed to parse include"));
assert!(err.to_string().contains("invalid.idl"));
}
#[test]
fn test_hir_include_cycle_errors() {
let root = unique_temp_dir("cycle");
let a = root.join("a.idl");
let b = root.join("b.idl");
write_file(&a, "#include \"b.idl\"\n");
write_file(&b, "#include \"a.idl\"\n");
let err = parse_hir(&a).expect_err("cyclic include should fail");
assert!(err.to_string().contains("cyclic include detected"));
assert!(err.to_string().contains("a.idl"));
assert!(err.to_string().contains("b.idl"));
}
#[test]
fn http_includes_share_compiler_builtins_without_adding_runtime_models() {
let root = unique_temp_dir("http-builtins");
let main = root.join("main.idl");
write_file(&main, "#include \"first.idl\"\n#include \"second.idl\"\n");
write_file(
&root.join("first.idl"),
"@http union First switch(ContentType) { case Json: string value; };",
);
write_file(
&root.join("second.idl"),
"@http union Second switch(ContentType) { case Text: string value; };",
);
let spec = parse_hir(&main).expect("two included HTTP unions");
assert_eq!(spec.0.len(), 2, "only user declarations are retained");
let projected = xidl_parser::rest_hir::project(&spec).expect("HTTP projection");
let media_types = projected
.document
.http_unions
.iter()
.flat_map(|union| &union.cases)
.map(|case| case.content_type.as_str())
.collect::<Vec<_>>();
assert_eq!(media_types, ["application/json", "text/plain"]);
fs::remove_dir_all(root).expect("remove include fixtures");
}
#[test]
fn dependency_mount_resolves_nested_include() {
let root = unique_temp_dir("mount");
let main = root.join("main.idl");
let package = root.join("pkgs/timestamp-1.2.3");
write_file(
&main,
r#"
#include "timestamp/lib.idl"
"#,
);
write_file(
&package.join("lib.idl"),
r#"
struct Stamp {
int32 value;
};
"#,
);
let mounts = std::collections::BTreeMap::from([("timestamp".to_string(), package.clone())]);
let spec = parse_hir_with_mounts(&main, mounts).expect("mounted include resolves");
assert_eq!(spec.0.len(), 1, "the included declaration is available");
fs::remove_dir_all(root).expect("remove mount fixtures");
}
#[test]
fn dependency_mount_prefers_the_longest_matching_name() {
let root = unique_temp_dir("mount-longest");
let main = root.join("main.idl");
let outer = root.join("pkgs/xidl");
let inner = root.join("pkgs/xidl-std");
write_file(&main, "#include \"xidl/std/lib.idl\"\n");
write_file(&outer.join("std.idl"), "struct Wrong { int32 value; };\n");
write_file(
&outer.join("std/lib.idl"),
"struct AlsoWrong { int32 value; };\n",
);
write_file(&inner.join("lib.idl"), "struct Right { int32 value; };\n");
let mounts = std::collections::BTreeMap::from([
("xidl".to_string(), outer),
("xidl/std".to_string(), inner),
]);
let spec = parse_hir_with_mounts(&main, mounts).expect("mounted include resolves");
let ident = format!("{:?}", spec.0.first());
assert!(ident.contains("Right"), "expected {ident} to name Right");
fs::remove_dir_all(root).expect("remove mount fixtures");
}
#[test]
fn dependency_mount_falls_back_to_the_filesystem() {
let root = unique_temp_dir("mount-fallback");
let main = root.join("main.idl");
write_file(&main, "#include \"sibling.idl\"\n");
write_file(
&root.join("sibling.idl"),
"struct Sibling { int32 value; };\n",
);
let mounts =
std::collections::BTreeMap::from([("timestamp".to_string(), root.join("missing-package"))]);
let spec = parse_hir_with_mounts(&main, mounts).expect("sibling include still resolves");
assert_eq!(spec.0.len(), 1);
fs::remove_dir_all(root).expect("remove mount fixtures");
}
#[test]
fn dependency_mount_names_the_mount_when_the_file_is_missing() {
let root = unique_temp_dir("mount-missing");
let main = root.join("main.idl");
write_file(&main, "#include \"timestamp/lib.idl\"\n");
let mounts = std::collections::BTreeMap::from([(
"timestamp".to_string(),
root.join("pkgs/timestamp-1.2.3"),
)]);
let err = parse_hir_with_mounts(&main, mounts).expect_err("missing mount target should fail");
assert!(err.to_string().contains("timestamp/lib.idl"), "{err}");
assert!(err.to_string().contains("the 'timestamp' mount"), "{err}");
fs::remove_dir_all(root).expect("remove mount fixtures");
}