use super::super::ExtendrBackend;
use super::make_config;
use crate::core::backend::Backend;
use crate::core::config::ResolvedCrateConfig;
use crate::core::config::new_config::NewAlefConfig;
use crate::core::ir::*;
use std::collections::BTreeSet;
fn make_config_with_feature(configured_feature: &str) -> ResolvedCrateConfig {
let toml_src = format!(
"[workspace]\nlanguages = [\"r\"]\n[[crates]]\nname = \"test-lib\"\nsources = [\"src/lib.rs\"]\n\
[crates.r]\npackage_name = \"testlib\"\nfeatures = [\"{configured_feature}\"]\n"
);
let cfg: NewAlefConfig = toml::from_str(&toml_src).unwrap();
cfg.resolve().unwrap().remove(0)
}
fn generate(api: &ApiSurface, config: &ResolvedCrateConfig) -> String {
ExtendrBackend
.generate_bindings(api, config)
.expect("extendr generation")
.iter()
.map(|f| format!("// ==== {} ====\n{}", f.path.display(), f.content))
.collect::<Vec<_>>()
.join("\n")
}
fn returning_function(name: &str, enum_name: &str) -> FunctionDef {
FunctionDef {
name: name.to_string(),
rust_path: format!("test_lib::{name}"),
return_type: TypeRef::Named(enum_name.to_string()),
..Default::default()
}
}
fn unit_variant(name: &str, cfg: Option<&str>) -> EnumVariant {
EnumVariant {
name: name.to_string(),
cfg: cfg.map(str::to_string),
..Default::default()
}
}
fn wrapper_enum_declaration(out: &str) -> &str {
let start = out
.find("pub enum RoutingStrategy {")
.expect("generated crate must declare the RoutingStrategy wrapper enum");
let end = out[start..]
.find("\n}")
.map(|i| start + i + 2)
.expect("enum declaration must close");
&out[start..end]
}
fn declared_variant_names(rendered: &str) -> BTreeSet<String> {
rendered
.split(',')
.filter_map(|fragment| {
let fragment = fragment.rsplit(']').next()?;
let (name, rest) = fragment.trim().split_once(" = ")?;
let name = name.trim();
let rest: String = rest.chars().take_while(char::is_ascii_digit).collect();
if name.is_empty() || rest.is_empty() {
return None;
}
Some(name.to_string())
})
.collect()
}
fn names(values: &[&str]) -> BTreeSet<String> {
values.iter().map(|s| s.to_string()).collect()
}
#[test]
fn declares_exact_retained_variant_set_for_foreign_variant_proven_unreachable() {
let api = ApiSurface {
enums: vec![EnumDef {
name: "RoutingStrategy".to_string(),
rust_path: "dep_crate::RoutingStrategy".to_string(),
variants: vec![
unit_variant("Primary", None),
unit_variant("Secondary", None),
unit_variant("Extra", Some(r#"feature = "extra-tier""#)),
],
..Default::default()
}],
functions: vec![returning_function("get_strategy", "RoutingStrategy")],
..Default::default()
};
let excluded = generate(&api, &make_config());
let excluded_decl = wrapper_enum_declaration(&excluded);
assert_eq!(
declared_variant_names(excluded_decl),
names(&["Primary", "Secondary"]),
"the declared set must be exactly the two retained variants, got:\n{excluded_decl}"
);
let active = generate(&api, &make_config_with_feature("extra-tier"));
let active_decl = wrapper_enum_declaration(&active);
assert_eq!(
declared_variant_names(active_decl),
names(&["Primary", "Secondary", "Extra"]),
"with \"extra-tier\" configured, the declared set must include the retained foreign \
variant, got:\n{active_decl}"
);
}
#[test]
fn never_drops_host_owned_cfg_variant_from_declaration() {
let api = ApiSurface {
enums: vec![EnumDef {
name: "RoutingStrategy".to_string(),
rust_path: "test_lib::RoutingStrategy".to_string(),
variants: vec![
unit_variant("Primary", None),
unit_variant("Secondary", None),
unit_variant("Extra", Some(r#"feature = "extra-tier""#)),
],
..Default::default()
}],
functions: vec![returning_function("get_strategy", "RoutingStrategy")],
..Default::default()
};
let out = generate(&api, &make_config());
let decl = wrapper_enum_declaration(&out);
assert_eq!(
declared_variant_names(decl),
names(&["Primary", "Secondary", "Extra"]),
"a host-owned cfg-gated variant must stay declared even with no features configured, \
got:\n{decl}"
);
}
fn tuple_variant(name: &str, cfg: Option<&str>) -> EnumVariant {
EnumVariant {
name: name.to_string(),
fields: vec![FieldDef {
name: "_0".to_string(),
ty: TypeRef::String,
..Default::default()
}],
is_tuple: true,
cfg: cfg.map(str::to_string),
..Default::default()
}
}
fn output_format_api(rust_path: &str) -> ApiSurface {
ApiSurface {
enums: vec![EnumDef {
name: "OutputFormat".to_string(),
rust_path: rust_path.to_string(),
variants: vec![
unit_variant("Standard", None),
tuple_variant("Custom", None),
tuple_variant("Testkit", Some(r#"feature = "testkit""#)),
],
..Default::default()
}],
functions: vec![returning_function("get_format", "OutputFormat")],
..Default::default()
}
}
#[test]
fn flat_data_enum_struct_omits_field_for_foreign_variant_proven_unreachable_control_kept_when_active() {
let api = output_format_api("dep_crate::OutputFormat");
let excluded = generate(&api, &make_config());
assert!(
excluded.contains("pub struct OutputFormat {"),
"this fixture must exercise the flat struct path, got:\n{excluded}"
);
assert!(
excluded.contains("pub custom: Option<String>"),
"the always-present variant's field must still be declared, got:\n{excluded}"
);
assert!(
!excluded.contains("testkit"),
"the excluded variant's field must not appear anywhere on the flat struct, got:\n{excluded}"
);
let active = generate(&api, &make_config_with_feature("testkit"));
assert!(
active.contains("pub testkit: Option<String>"),
"with \"testkit\" configured, the field must be declared, got:\n{active}"
);
}
#[test]
fn flat_data_enum_conversions_drop_arm_for_foreign_variant() {
let api = output_format_api("dep_crate::OutputFormat");
let out = generate(&api, &make_config());
assert!(
out.contains("impl From<dep_crate::OutputFormat> for OutputFormat {"),
"the core->binding flat conversion must be emitted, got:\n{out}"
);
assert!(
out.contains("dep_crate::OutputFormat::Custom(_0) => Self {"),
"the always-present tuple variant's arm must still be emitted, got:\n{out}"
);
assert!(
!out.contains("Testkit"),
"the excluded variant must not be named anywhere in the flat conversions, got:\n{out}"
);
}
#[test]
fn flat_data_enum_never_drops_host_owned_cfg_variant() {
let api = output_format_api("test_lib::OutputFormat");
let out = generate(&api, &make_config());
assert!(
out.contains("pub testkit: Option<String>"),
"a host-owned cfg-gated variant's field must stay declared even with no features \
configured, got:\n{out}"
);
assert!(
out.contains("#[cfg(feature = \"testkit\")]"),
"a host-owned cfg-gated variant's conversion arm must carry its matching #[cfg(...)] \
guard, got:\n{out}"
);
}