use crate::core::ir::{
ApiSurface, EnumDef, EnumVariant, ErrorDef, FieldDef, FunctionDef, MethodDef, ServiceDef, TypeDef,
};
use std::collections::BTreeSet;
struct Position {
label: &'static str,
feature: &'static str,
in_collector: bool,
in_header_defines: bool,
}
const HOST_CRATE: &str = "hostlib";
const POSITIONS: &[Position] = &[
Position {
label: "types[].cfg (host)",
feature: "host-type",
in_collector: true,
in_header_defines: true,
},
Position {
label: "types[].fields[].cfg (host)",
feature: "host-field",
in_collector: true,
in_header_defines: false,
},
Position {
label: "types[].methods[].cfg (host)",
feature: "host-type-method",
in_collector: true,
in_header_defines: true,
},
Position {
label: "enums[].cfg (host)",
feature: "host-enum",
in_collector: true,
in_header_defines: true,
},
Position {
label: "enums[].variants[].cfg (host)",
feature: "host-variant",
in_collector: true,
in_header_defines: false,
},
Position {
label: "enums[].methods[].cfg (host)",
feature: "host-enum-method",
in_collector: true,
in_header_defines: true,
},
Position {
label: "functions[].cfg (host)",
feature: "host-function",
in_collector: true,
in_header_defines: true,
},
Position {
label: "services[].cfg (host)",
feature: "host-service",
in_collector: true,
in_header_defines: true,
},
Position {
label: "services[].constructor.cfg (host)",
feature: "host-service-constructor",
in_collector: true,
in_header_defines: false,
},
Position {
label: "services[].configurators[].cfg (host)",
feature: "host-service-configurator",
in_collector: true,
in_header_defines: false,
},
Position {
label: "errors[].methods[].cfg",
feature: "error-method",
in_collector: false,
in_header_defines: true,
},
Position {
label: "types[].cfg (foreign)",
feature: "foreign-type",
in_collector: false,
in_header_defines: true,
},
Position {
label: "types[].fields[].cfg (foreign)",
feature: "foreign-field",
in_collector: false,
in_header_defines: false,
},
Position {
label: "types[].methods[].cfg (foreign)",
feature: "foreign-type-method",
in_collector: false,
in_header_defines: true,
},
Position {
label: "enums[].cfg (foreign)",
feature: "foreign-enum",
in_collector: false,
in_header_defines: true,
},
Position {
label: "enums[].variants[].cfg (foreign)",
feature: "foreign-variant",
in_collector: false,
in_header_defines: false,
},
Position {
label: "enums[].methods[].cfg (foreign)",
feature: "foreign-enum-method",
in_collector: false,
in_header_defines: true,
},
Position {
label: "functions[].cfg (foreign)",
feature: "foreign-function",
in_collector: true,
in_header_defines: true,
},
Position {
label: "services[].cfg (foreign)",
feature: "foreign-service",
in_collector: false,
in_header_defines: true,
},
Position {
label: "services[].constructor.cfg (foreign)",
feature: "foreign-service-constructor",
in_collector: false,
in_header_defines: false,
},
Position {
label: "services[].configurators[].cfg (foreign)",
feature: "foreign-service-configurator",
in_collector: false,
in_header_defines: false,
},
];
fn gate(feature: &str) -> Option<String> {
Some(format!("feature = {feature:?}"))
}
fn method(name: &str, feature: &str) -> MethodDef {
MethodDef {
name: name.to_string(),
cfg: gate(feature),
..MethodDef::default()
}
}
fn typ(name: &str, rust_path: &str, type_feature: &str, field_feature: &str, method_feature: &str) -> TypeDef {
TypeDef {
name: name.to_string(),
rust_path: rust_path.to_string(),
cfg: gate(type_feature),
fields: vec![FieldDef {
name: "gated_field".to_string(),
cfg: gate(field_feature),
..FieldDef::default()
}],
methods: vec![method("gated_method", method_feature)],
..TypeDef::default()
}
}
fn enumeration(
name: &str,
rust_path: &str,
enum_feature: &str,
variant_feature: &str,
method_feature: &str,
) -> EnumDef {
EnumDef {
name: name.to_string(),
rust_path: rust_path.to_string(),
cfg: gate(enum_feature),
variants: vec![EnumVariant {
name: "GatedVariant".to_string(),
cfg: gate(variant_feature),
..EnumVariant::default()
}],
methods: vec![method("gated_method", method_feature)],
..EnumDef::default()
}
}
fn service(
name: &str,
rust_path: &str,
service_feature: &str,
constructor_feature: &str,
configurator_feature: &str,
) -> ServiceDef {
ServiceDef {
name: name.to_string(),
rust_path: rust_path.to_string(),
constructor: method("new", constructor_feature),
configurators: vec![method("with_gated_option", configurator_feature)],
registrations: vec![],
entrypoints: vec![],
doc: String::new(),
cfg: gate(service_feature),
}
}
fn fixture_api() -> ApiSurface {
ApiSurface {
crate_name: HOST_CRATE.to_string(),
version: "0.1.0".to_string(),
types: vec![
typ(
"HostType",
"hostlib::HostType",
"host-type",
"host-field",
"host-type-method",
),
typ(
"ForeignType",
"foreignlib::ForeignType",
"foreign-type",
"foreign-field",
"foreign-type-method",
),
],
enums: vec![
enumeration(
"HostEnum",
"hostlib::HostEnum",
"host-enum",
"host-variant",
"host-enum-method",
),
enumeration(
"ForeignEnum",
"foreignlib::ForeignEnum",
"foreign-enum",
"foreign-variant",
"foreign-enum-method",
),
],
functions: vec![
FunctionDef {
name: "host_function".to_string(),
rust_path: "hostlib::host_function".to_string(),
cfg: gate("host-function"),
..FunctionDef::default()
},
FunctionDef {
name: "foreign_function".to_string(),
rust_path: "foreignlib::foreign_function".to_string(),
cfg: gate("foreign-function"),
..FunctionDef::default()
},
],
errors: vec![ErrorDef {
name: "HostError".to_string(),
rust_path: "hostlib::HostError".to_string(),
original_rust_path: String::new(),
variants: vec![],
doc: String::new(),
methods: vec![method("status_code", "error-method")],
binding_excluded: false,
binding_exclusion_reason: None,
version: Default::default(),
}],
services: vec![
service(
"HostService",
"hostlib::HostService",
"host-service",
"host-service-constructor",
"host-service-configurator",
),
service(
"ForeignService",
"foreignlib::ForeignService",
"foreign-service",
"foreign-service-constructor",
"foreign-service-configurator",
),
],
..ApiSurface::default()
}
}
fn header_define_features(api: &ApiSurface) -> BTreeSet<String> {
super::super::helpers::cbindgen_feature_defines(api, "AL")
.into_iter()
.map(|(key, _macro_name)| {
key.strip_prefix("feature = ")
.unwrap_or_else(|| panic!("cbindgen [defines] key must be `feature = <name>`, got `{key}`"))
.to_string()
})
.collect()
}
#[test]
fn fixture_carries_a_distinct_gate_at_every_cfg_bearing_position() {
let api = fixture_api();
let mut found: Vec<&str> = Vec::new();
for item in &api.types {
found.push(item.cfg.as_deref().expect("type gate"));
found.push(item.fields[0].cfg.as_deref().expect("field gate"));
found.push(item.methods[0].cfg.as_deref().expect("type method gate"));
}
for item in &api.enums {
found.push(item.cfg.as_deref().expect("enum gate"));
found.push(item.variants[0].cfg.as_deref().expect("variant gate"));
found.push(item.methods[0].cfg.as_deref().expect("enum method gate"));
}
for item in &api.functions {
found.push(item.cfg.as_deref().expect("function gate"));
}
for item in &api.errors {
found.push(item.methods[0].cfg.as_deref().expect("error method gate"));
}
for item in &api.services {
found.push(item.cfg.as_deref().expect("service gate"));
found.push(item.constructor.cfg.as_deref().expect("service constructor gate"));
found.push(item.configurators[0].cfg.as_deref().expect("service configurator gate"));
}
let placed: BTreeSet<String> = found.iter().map(|cfg| cfg.to_string()).collect();
assert_eq!(
placed.len(),
found.len(),
"each position must use a feature name no other position uses, or a set difference \
cannot be attributed to one position; got {found:?}"
);
for position in POSITIONS {
let expected = format!("feature = {:?}", position.feature);
assert!(
placed.contains(&expected),
"fixture does not carry a gate at position `{}` (expected `{expected}`); every \
coverage assertion for that position would be vacuous",
position.label
);
}
assert_eq!(
placed.len(),
POSITIONS.len(),
"the fixture and the POSITIONS table must describe the same set of positions"
);
}
#[test]
fn the_two_feature_walks_cover_exactly_the_documented_positions() {
let api = fixture_api();
let collector = crate::codegen::cfg::collect_cfg_features(&api);
let header = header_define_features(&api);
assert!(
collector.contains("host-type"),
"control: collect_cfg_features must reach types[].cfg, got {collector:?}"
);
assert!(
header.contains("host-type"),
"control: cbindgen_feature_defines must reach types[].cfg, got {header:?}"
);
for position in POSITIONS {
assert_eq!(
collector.contains(position.feature),
position.in_collector,
"codegen::cfg::collect_cfg_features coverage of `{}` changed: the table says \
visited={}, the walk says visited={}. Update the walk or, if the change is \
intended, the POSITIONS row — and check whether cbindgen_feature_defines \
(backends/ffi/gen_bindings/helpers.rs) needs the same edit.",
position.label,
position.in_collector,
collector.contains(position.feature)
);
assert_eq!(
header.contains(position.feature),
position.in_header_defines,
"backends::ffi::gen_bindings::helpers::cbindgen_feature_defines coverage of `{}` \
changed: the table says visited={}, the walk says visited={}. Update the walk or, \
if the change is intended, the POSITIONS row — and check whether \
collect_cfg_features (codegen/cfg.rs) needs the same edit.",
position.label,
position.in_header_defines,
header.contains(position.feature)
);
}
let modeled: BTreeSet<&str> = POSITIONS.iter().map(|position| position.feature).collect();
for feature in collector.iter().chain(header.iter()) {
assert!(
modeled.contains(feature.as_str()),
"a walk emitted `{feature}`, which the POSITIONS table does not model; a new \
cfg-bearing IR position needs a row here and a decision about the other walk"
);
}
}
#[test]
fn the_header_only_features_are_the_documented_ones() {
let api = fixture_api();
let collector = crate::codegen::cfg::collect_cfg_features(&api);
let header = header_define_features(&api);
let header_only: BTreeSet<&str> = header
.iter()
.map(String::as_str)
.filter(|feature| !collector.contains(*feature))
.collect();
let expected: BTreeSet<&str> = POSITIONS
.iter()
.filter(|position| position.in_header_defines && !position.in_collector)
.map(|position| position.feature)
.collect();
assert!(
!expected.is_empty(),
"control: the table must model at least one header-only position, or this test asserts \
nothing"
);
assert_eq!(
header_only, expected,
"the set of features guarding the C header but absent from every binding crate's \
[features] table (and so from backends::go::cgo_features' -D list) changed"
);
}