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::*;
fn gated_variant(name: &str, cfg: Option<&str>) -> EnumVariant {
EnumVariant {
name: name.to_string(),
cfg: cfg.map(str::to_string),
..Default::default()
}
}
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 make_config_with_core_default(dir: &std::path::Path, core_features_body: &str) -> ResolvedCrateConfig {
let core_dir = dir.join("crates").join("test-lib");
std::fs::create_dir_all(&core_dir).expect("create core crate dir");
std::fs::write(
core_dir.join("Cargo.toml"),
format!("[package]\nname = \"test-lib\"\n\n[features]\n{core_features_body}"),
)
.expect("write core Cargo.toml");
ResolvedCrateConfig {
workspace_root: Some(dir.to_path_buf()),
name: "test-lib".to_string(),
sources: vec![std::path::PathBuf::from("crates/test-lib/src/lib.rs")],
r: Some(crate::core::config::RConfig {
package_name: Some("testlib".to_string()),
features: None,
default_features: None,
serde_rename_all: None,
exclude_functions: Vec::new(),
exclude_types: Vec::new(),
rename_fields: std::collections::HashMap::new(),
run_wrapper: None,
extra_lint_paths: Vec::new(),
extra_makevars_prelude: Vec::new(),
extra_pkg_libs: Vec::new(),
}),
..Default::default()
}
}
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 generate_r(api: &ApiSurface) -> String {
generate_r_with_config(api, &make_config())
}
fn generate_r_with_config(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")
}
#[test]
fn host_owned_cfg_gated_variant_keeps_arm_under_matching_cfg_guard() {
let api = ApiSurface {
enums: vec![EnumDef {
name: "Status".to_string(),
rust_path: "test_lib::Status".to_string(),
variants: vec![
gated_variant("Active", None),
gated_variant("Beta", Some(r#"feature = "beta""#)),
],
..Default::default()
}],
functions: vec![returning_function("get_status", "Status")],
..Default::default()
};
let out = generate_r(&api);
assert!(
out.contains("Status::Active => Self::Active,"),
"ungated variant's binding->core arm missing, fixture no longer exercises conversion:\n{out}"
);
assert!(
out.contains("test_lib::Status::Active => Self::Active,"),
"ungated variant's core->binding arm missing, fixture no longer exercises conversion:\n{out}"
);
assert!(
out.contains("#[cfg(feature = \"beta\")]\n Status::Beta => Self::Beta,"),
"host-owned cfg-gated variant must keep its binding->core arm under a matching #[cfg(...)] guard:\n{out}"
);
assert!(
out.contains("#[cfg(feature = \"beta\")]\n test_lib::Status::Beta => Self::Beta,"),
"host-owned cfg-gated variant must keep its core->binding arm under a matching #[cfg(...)] guard:\n{out}"
);
assert!(
!out.contains("_ => Self::default(),"),
"a host-owned cfg-gated variant alone must not trigger a catch-all (unreachable pattern \
under -D warnings):\n{out}"
);
}
#[test]
fn foreign_owned_cfg_gated_variant_drops_arm_and_cfg_forward_in_both_directions() {
let api = ApiSurface {
enums: vec![EnumDef {
name: "External".to_string(),
rust_path: "foreign_crate::External".to_string(),
variants: vec![
gated_variant("Foo", None),
gated_variant("Bar", Some(r#"feature = "extra""#)),
],
..Default::default()
}],
functions: vec![returning_function("get_external", "External")],
..Default::default()
};
let out = generate_r(&api);
assert!(
out.contains("External::Foo => Self::Foo,"),
"ungated variant's binding->core arm missing, fixture no longer exercises conversion:\n{out}"
);
assert!(
out.contains("foreign_crate::External::Foo => Self::Foo,"),
"ungated variant's core->binding arm missing, fixture no longer exercises conversion:\n{out}"
);
assert!(
!out.contains("External::Bar"),
"a foreign crate's cfg-gated variant must not be named anywhere in the conversion output:\n{out}"
);
assert!(
!out.contains(r#"#[cfg(feature = "extra")]"#),
"a foreign crate's cfg gate must never be forwarded into this generated crate:\n{out}"
);
}
#[test]
fn foreign_owned_cfg_gated_variant_proven_unreachable_drops_catch_all_in_both_directions() {
let enum_def = EnumDef {
name: "External".to_string(),
rust_path: "foreign_crate::External".to_string(),
variants: vec![
gated_variant("Foo", None),
gated_variant("Bar", Some(r#"feature = "extra""#)),
],
..Default::default()
};
let type_paths = std::collections::HashMap::new();
let configured_features: Option<&[String]> = Some(&[]);
let binding_to_core = super::super::enum_conversions::gen_from_binding_to_core(
&enum_def,
"test_lib",
&type_paths,
configured_features,
);
let core_to_binding = super::super::enum_conversions::gen_from_core_to_binding(
&enum_def,
"test_lib",
&type_paths,
configured_features,
);
assert!(
!binding_to_core.contains("Bar") && !core_to_binding.contains("Bar"),
"a foreign crate's cfg-gated variant must not be named anywhere in the conversion output:\n\
binding->core:\n{binding_to_core}\ncore->binding:\n{core_to_binding}"
);
assert!(
!binding_to_core.contains("_ => Self::default(),"),
"the binding->core match is over the BINDING enum extendr itself declares, which now \
drops a proven-unreachable foreign variant just like the declaration does -- keeping the \
catch-all here is an unreachable pattern under -D warnings, got:\n{binding_to_core}"
);
assert!(
!core_to_binding.contains("_ => Self::default(),"),
"the core->binding match is over the real core type, which this binding's own configured \
feature set proves lacks the variant -- a catch-all there is an unreachable pattern under \
-D warnings, got:\n{core_to_binding}"
);
}
#[test]
fn foreign_owned_cfg_gated_variant_not_proven_unreachable_keeps_catch_all_in_both_directions() {
let enum_def = EnumDef {
name: "External".to_string(),
rust_path: "foreign_crate::External".to_string(),
variants: vec![
gated_variant("Foo", None),
gated_variant("Bar", Some(r#"feature = "extra""#)),
],
..Default::default()
};
let type_paths = std::collections::HashMap::new();
let configured_features: Option<&[String]> = Some(&["extra".to_string()]);
let binding_to_core = super::super::enum_conversions::gen_from_binding_to_core(
&enum_def,
"test_lib",
&type_paths,
configured_features,
);
let core_to_binding = super::super::enum_conversions::gen_from_core_to_binding(
&enum_def,
"test_lib",
&type_paths,
configured_features,
);
assert!(
!binding_to_core.contains("Bar") && !core_to_binding.contains("Bar"),
"a foreign crate's cfg-gated variant must not be named anywhere in the conversion output:\n\
binding->core:\n{binding_to_core}\ncore->binding:\n{core_to_binding}"
);
assert!(
binding_to_core.contains("_ => Self::default(),") && core_to_binding.contains("_ => Self::default(),"),
"a foreign cfg-gated variant that is NOT proven unreachable must keep the catch-all in \
both directions, got:\nbinding->core:\n{binding_to_core}\ncore->binding:\n{core_to_binding}"
);
}
#[test]
fn foreign_owned_cfg_gated_variant_not_proven_unreachable_keeps_catch_all() {
let api = ApiSurface {
enums: vec![EnumDef {
name: "External".to_string(),
rust_path: "foreign_crate::External".to_string(),
variants: vec![
gated_variant("Foo", None),
gated_variant("Bar", Some(r#"feature = "extra""#)),
],
..Default::default()
}],
functions: vec![returning_function("get_external", "External")],
..Default::default()
};
let out = generate_r_with_config(&api, &make_config_with_feature("extra"));
assert!(
!out.contains("External::Bar"),
"a foreign crate's cfg-gated variant must not be named anywhere in the conversion output:\n{out}"
);
assert!(
out.contains("_ => Self::default(),"),
"a foreign cfg-gated variant that is NOT proven unreachable must keep the catch-all so the \
match stays exhaustive, got:\n{out}"
);
}
#[test]
fn foreign_owned_cfg_gated_variant_reachable_only_through_core_default_keeps_catch_all() {
let api = ApiSurface {
enums: vec![EnumDef {
name: "RetryPolicy".to_string(),
rust_path: "foreign_crate::RetryPolicy".to_string(),
variants: vec![
gated_variant("Standard", None),
gated_variant("Backoff", Some(r#"feature = "backoff""#)),
],
..Default::default()
}],
functions: vec![returning_function("get_retry_policy", "RetryPolicy")],
..Default::default()
};
let dir = tempfile::tempdir().expect("tempdir");
let config = make_config_with_core_default(dir.path(), "default = [\"backoff\"]\nbackoff = []\n");
let out = generate_r_with_config(&api, &config);
assert!(
!out.contains("RetryPolicy::Backoff"),
"a foreign crate's cfg-gated variant must not be named anywhere in the conversion output:\n{out}"
);
assert!(
out.contains("_ => Self::default(),"),
"a foreign cfg-gated variant reachable only through the core crate's own `default = [...]` \
(never named in this binding's own `alef.toml`) must still keep the catch-all so the \
generated match stays exhaustive, got:\n{out}"
);
}
#[test]
fn ungated_enum_emits_no_cfg_guard_and_no_catch_all() {
let api = ApiSurface {
enums: vec![EnumDef {
name: "Plain".to_string(),
rust_path: "test_lib::Plain".to_string(),
variants: vec![gated_variant("On", None), gated_variant("Off", None)],
..Default::default()
}],
functions: vec![returning_function("get_plain", "Plain")],
..Default::default()
};
let out = generate_r(&api);
assert!(
out.contains("Plain::On => Self::On,") && out.contains("Plain::Off => Self::Off,"),
"both ungated variants' binding->core arms must be present:\n{out}"
);
assert!(
!out.contains("#[cfg("),
"an ungated enum must not emit any #[cfg(...)] guard:\n{out}"
);
assert!(
!out.contains("_ => Self::default()"),
"an ungated enum with no data/excluded variants must not emit a catch-all fallback arm:\n{out}"
);
}