use super::{api_with_gated_functions, resolved_config};
use crate::codegen::cfg::warn_on_ffi_feature_drift;
use crate::core::config::Language;
use tracing_test::traced_test;
#[traced_test]
#[test]
fn warn_on_ffi_feature_drift_fires_when_effective_set_diverges_even_though_configured_sets_match() {
let config = resolved_config(
r#"
[workspace]
languages = ["ffi", "go"]
[[crates]]
name = "sample-core"
sources = []
[crates.ffi]
features = ["shared"]
[crates.go]
features = ["shared"]
"#,
);
let api = api_with_gated_functions(&[("discovered_gate", Some(r#"feature = "sample-gate""#))]);
warn_on_ffi_feature_drift(&api, &config, Language::Go);
assert!(
logs_contain("coverage gap"),
"the FFI crate's effective default set includes `sample-gate`, which Go's configured \
list does not -- this is precisely the drift the two CONFIGURED lists agree on and \
hide, so the warning must still fire"
);
assert!(
logs_contain(r#"unsatisfied_gates={"feature = \"sample-gate\""}"#),
"the warning must name the gate it evaluated, not merely a feature name, so a reader can \
check the claim against the source item"
);
assert!(
!logs_contain("unsafe and can produce glue"),
"Go's configured set is a subset of the effective set here, not a superset, so this \
must not be reported as the unsafe host-only direction"
);
}
#[traced_test]
#[test]
fn warn_on_ffi_feature_drift_silent_when_lang_features_equal_effective_set() {
let config = resolved_config(
r#"
[workspace]
languages = ["ffi", "go"]
[[crates]]
name = "sample-core"
sources = []
[crates.ffi]
features = ["shared"]
[crates.go]
features = ["shared"]
"#,
);
let api = api_with_gated_functions(&[("configured_only", None)]);
warn_on_ffi_feature_drift(&api, &config, Language::Go);
assert!(
!logs_contain("coverage gap") && !logs_contain("unsafe and can produce glue"),
"Go's configured set equals the FFI crate's effective default set, so neither warning \
must fire"
);
}
#[traced_test]
#[test]
fn warn_on_ffi_feature_drift_fires_when_the_binding_satisfies_a_gate_the_cdylib_lacks() {
let config = resolved_config(
r#"
[workspace]
languages = ["ffi", "go"]
[[crates]]
name = "sample-core"
sources = []
[crates.ffi]
features = ["shared"]
extra_features = ["alt-backend"]
[crates.go]
features = ["shared", "alt-backend"]
"#,
);
let api = api_with_gated_functions(&[("alt_backend_entry", Some(r#"feature = "alt-backend""#))]);
warn_on_ffi_feature_drift(&api, &config, Language::Go);
assert!(
logs_contain("unsafe and can produce glue"),
"`alt-backend` is declare-only on the FFI side, so the cdylib's default build omits \
`alt_backend_entry`; Go configures the feature and keeps the glue -- the unsafe \
direction must fire"
);
assert!(
!logs_contain("coverage gap"),
"no gate is satisfied by the cdylib and unsatisfied by Go here, so the safe \
coverage-gap warning must not also fire"
);
}
#[traced_test]
#[test]
fn warn_on_ffi_feature_drift_reports_nothing_when_full_is_configured_on_both_sides() {
let config = resolved_config(
r#"
[workspace]
languages = ["ffi", "go"]
[[crates]]
name = "sample-core"
sources = []
[crates.ffi]
features = ["full"]
[crates.go]
features = ["full"]
"#,
);
let api = api_with_gated_functions(&[
("ungated", None),
("alpha_entry", Some(r#"feature = "alpha""#)),
("beta_entry", Some(r#"feature = "beta""#)),
]);
warn_on_ffi_feature_drift(&api, &config, Language::Go);
assert!(
!logs_contain("coverage gap"),
"`full` satisfies every feature gate for both the binding filter and the cdylib, so no \
item is omitted from the Go surface and no coverage gap exists to report"
);
assert!(
!logs_contain("unsafe and can produce glue"),
"both sides satisfy every gate, so nothing is kept that the cdylib lacks either"
);
}
#[traced_test]
#[test]
fn warn_on_ffi_feature_drift_reports_nothing_when_an_any_gate_is_already_satisfied() {
let config = resolved_config(
r#"
[workspace]
languages = ["ffi", "go"]
[[crates]]
name = "sample-core"
sources = []
[crates.ffi]
features = ["alpha"]
[crates.go]
features = ["alpha"]
"#,
);
let api = api_with_gated_functions(&[("either_entry", Some(r#"any(feature = "alpha", feature = "beta")"#))]);
warn_on_ffi_feature_drift(&api, &config, Language::Go);
assert!(
!logs_contain("coverage gap"),
"`alpha` alone satisfies `any(alpha, beta)`, so `either_entry` survives the Go filter \
exactly as it survives into the cdylib -- `beta` is not a missing feature"
);
}
#[traced_test]
#[test]
fn warn_on_ffi_feature_drift_reports_a_configured_list_that_satisfies_no_gate() {
let config = resolved_config(
r#"
[workspace]
languages = ["ffi", "go"]
[[crates]]
name = "sample-core"
sources = []
[crates.ffi]
features = ["full"]
[crates.go]
features = ["unrelated"]
"#,
);
let api = api_with_gated_functions(&[
("ungated", None),
("alpha_entry", Some(r#"feature = "alpha""#)),
("beta_entry", Some(r#"feature = "beta""#)),
]);
warn_on_ffi_feature_drift(&api, &config, Language::Go);
assert!(
logs_contain("coverage gap"),
"`unrelated` satisfies neither gate, so both gated functions vanish from the Go surface \
while the cdylib exports them -- the underexposure must be reported"
);
assert!(
logs_contain(r#"missing_features={"alpha", "beta"}"#),
"the remedy must name the features to add, and both dropped gates must be accounted for"
);
}
struct AggregateDriftCase {
name: &'static str,
core_manifest: &'static str,
workspace_toml: &'static str,
gate: &'static str,
expect_coverage_gap: bool,
expect_unsafe: bool,
}
const AGGREGATE_DRIFT_CASES: &[AggregateDriftCase] = &[
AggregateDriftCase {
name: "fully_covered_aggregate",
core_manifest: "[package]\nname = \"sample-core\"\n\n[features]\ndefault = []\n\
mobile-target = [\"alt-backend\"]\nalt-backend = []\n",
workspace_toml: r#"
[workspace]
languages = ["ffi", "go"]
[[crates]]
name = "sample-core"
sources = []
[crates.ffi]
features = ["mobile-target"]
extra_features = ["alt-backend"]
[crates.go]
features = ["mobile-target"]
"#,
gate: r#"feature = "alt-backend""#,
expect_coverage_gap: false,
expect_unsafe: false,
},
AggregateDriftCase {
name: "aggregate_member_outside_ffi_reach",
core_manifest: "[package]\nname = \"sample-core\"\n\n[features]\ndefault = []\n\
mobile-target = [\"alt-backend\"]\nalt-backend = []\n",
workspace_toml: r#"
[workspace]
languages = ["ffi", "go"]
[[crates]]
name = "sample-core"
sources = []
[crates.ffi]
features = ["shared"]
extra_features = ["alt-backend"]
[crates.go]
features = ["mobile-target"]
"#,
gate: r#"feature = "alt-backend""#,
expect_coverage_gap: false,
expect_unsafe: true,
},
];
fn run_aggregate_drift_case(case: &AggregateDriftCase) -> tempfile::TempDir {
let dir = tempfile::tempdir().expect("tempdir");
let core_dir = dir.path().join("crates").join("sample-core");
std::fs::create_dir_all(&core_dir).expect("create core crate dir");
std::fs::write(core_dir.join("Cargo.toml"), case.core_manifest).expect("write core Cargo.toml");
let mut config = resolved_config(case.workspace_toml);
config.workspace_root = Some(dir.path().to_path_buf());
config.sources = vec![std::path::PathBuf::from("crates/sample-core/src/lib.rs")];
let api = api_with_gated_functions(&[("gated_entry", Some(case.gate))]);
warn_on_ffi_feature_drift(&api, &config, Language::Go);
dir
}
#[traced_test]
#[test]
fn warn_on_ffi_feature_drift_case_a_fully_covered_aggregate_stays_silent() {
let case = &AGGREGATE_DRIFT_CASES[0];
assert_eq!(case.name, "fully_covered_aggregate");
let _dir = run_aggregate_drift_case(case);
assert_eq!(
logs_contain("coverage gap"),
case.expect_coverage_gap,
"case `{}`: coverage-gap direction should not fire -- both sides expand `mobile-target` \
to the same `{{mobile-target, alt-backend}}` set",
case.name
);
assert_eq!(
logs_contain("unsafe and can produce glue"),
case.expect_unsafe,
"case `{}`: unsafe direction should not fire either -- nothing is kept that the cdylib \
lacks",
case.name
);
}
#[traced_test]
#[test]
fn warn_on_ffi_feature_drift_case_b_aggregate_member_outside_ffi_reach_is_unsafe() {
let case = &AGGREGATE_DRIFT_CASES[1];
assert_eq!(case.name, "aggregate_member_outside_ffi_reach");
let _dir = run_aggregate_drift_case(case);
assert_eq!(
logs_contain("unsafe and can produce glue"),
case.expect_unsafe,
"case `{}`: Go's expanded filter keeps `alt_backend_entry`, but no FFI-configured \
aggregate reaches `alt-backend`, so the cdylib never ships it -- this must fire unsafe",
case.name
);
assert_eq!(
logs_contain("coverage gap"),
case.expect_coverage_gap,
"case `{}`: the same gate cannot be both a coverage gap and unsafe drift",
case.name
);
}