use anyhow::Context as _;
use std::path::Path;
pub(super) fn verify(facade_file: &Path, bridge_file: &Path, exclude_functions: &[String]) -> anyhow::Result<()> {
if !facade_file.exists() || !bridge_file.exists() {
tracing::debug!(
"VerifyFrbBridgeCoverage: facade or bridge not found ({} / {})",
facade_file.display(),
bridge_file.display()
);
return Ok(());
}
let facade_source =
std::fs::read_to_string(facade_file).with_context(|| format!("failed to read FRB facade {}", facade_file.display()))?;
let bridge_source =
std::fs::read_to_string(bridge_file).with_context(|| format!("failed to read FRB bridge {}", bridge_file.display()))?;
let missing = crate::backends::dart::missing_bridge_functions(&facade_source, &bridge_source, exclude_functions);
if missing.is_empty() {
return Ok(());
}
anyhow::bail!(
"flutter_rust_bridge bridge {} is missing {} function(s) declared in facade {}: {}. \
flutter_rust_bridge_codegen did not (re)generate this bridge against the current facade \
-- install/enable `flutter_rust_bridge_codegen` (or update the committed bridge source) \
and rerun, since alef's post-build patches must not be applied to a stale bridge.",
bridge_file.display(),
missing.len(),
facade_file.display(),
missing.join(", "),
);
}
#[cfg(test)]
mod tests {
use super::*;
const FACADE_ONE_FUNCTION: &str = "pub fn count_widgets(collection: String) -> Result<i64, String> {\n Ok(0)\n}\n";
const BRIDGE_COVERING_IT: &str =
"Future<int> countWidgets({required String collection}) => RustLib.instance.api.crateCountWidgets(collection: collection);\n";
#[test]
fn verify_passes_when_bridge_covers_every_facade_function() {
let dir = tempfile::tempdir().expect("temp dir");
let facade = dir.path().join("lib.rs");
let bridge = dir.path().join("lib.dart");
std::fs::write(&facade, FACADE_ONE_FUNCTION).unwrap();
std::fs::write(&bridge, BRIDGE_COVERING_IT).unwrap();
assert!(verify(&facade, &bridge, &[]).is_ok());
}
#[test]
fn verify_fails_when_the_bridge_is_stale_relative_to_the_facade() {
let dir = tempfile::tempdir().expect("temp dir");
let facade = dir.path().join("lib.rs");
let bridge = dir.path().join("lib.dart");
std::fs::write(
&facade,
"pub fn count_widgets(collection: String) -> Result<i64, String> {\n Ok(0)\n}\n\
pub fn record_price(id: String, price_cents: i64) -> Result<(), String> {\n Ok(())\n}\n",
)
.unwrap();
std::fs::write(&bridge, BRIDGE_COVERING_IT).unwrap();
let error = verify(&facade, &bridge, &[]).expect_err("stale bridge must fail the check");
let message = format!("{error:#}");
assert!(
message.contains("record_price"),
"error must name the missing function: {message}"
);
}
#[test]
fn verify_ignores_configured_exclusions() {
let dir = tempfile::tempdir().expect("temp dir");
let facade = dir.path().join("lib.rs");
let bridge = dir.path().join("lib.dart");
std::fs::write(
&facade,
"pub fn count_widgets(collection: String) -> Result<i64, String> {\n Ok(0)\n}\n\
pub fn internal_only(id: String) -> Result<(), String> {\n Ok(())\n}\n",
)
.unwrap();
std::fs::write(&bridge, BRIDGE_COVERING_IT).unwrap();
assert!(verify(&facade, &bridge, &["internal_only".to_string()]).is_ok());
}
#[test]
fn verify_is_a_no_op_when_the_bridge_does_not_exist_yet() {
let dir = tempfile::tempdir().expect("temp dir");
let facade = dir.path().join("lib.rs");
let bridge = dir.path().join("lib.dart");
std::fs::write(&facade, FACADE_ONE_FUNCTION).unwrap();
assert!(verify(&facade, &bridge, &[]).is_ok());
}
#[test]
fn run_post_build_aborts_before_patching_a_stale_bridge_when_frb_is_skipped() {
use crate::core::backend::{BuildConfig, BuildDependency, PostBuildStep, PostProcessor};
use crate::core::config::{Language, ResolvedCrateConfig};
static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
let _guard = ENV_LOCK.lock().expect("env lock poisoned");
let previous = std::env::var("ALEF_SKIP_COMMANDS").ok();
unsafe {
std::env::set_var("ALEF_SKIP_COMMANDS", "flutter_rust_bridge_codegen");
}
let dir = tempfile::tempdir().expect("temp dir");
let facade_rel = std::path::PathBuf::from("lib.rs");
let bridge_rel = std::path::PathBuf::from("lib.dart");
std::fs::write(
dir.path().join(&facade_rel),
"pub fn count_widgets(collection: String) -> Result<i64, String> {\n Ok(0)\n}\n\
pub fn record_price(id: String, price_cents: i64) -> Result<(), String> {\n Ok(())\n}\n",
)
.unwrap();
let stale_bridge_with_trailing_whitespace = "Future<int> countWidgets({required String collection}) => \nRustLib.instance.api.crateCountWidgets(collection: collection); \n";
std::fs::write(dir.path().join(&bridge_rel), stale_bridge_with_trailing_whitespace).unwrap();
let build_config = BuildConfig {
tool: "cargo",
crate_suffix: "-dart",
build_dep: BuildDependency::None,
post_build: vec![
PostBuildStep::RunCommand {
cmd: "flutter_rust_bridge_codegen",
args: vec!["generate"],
},
PostBuildStep::VerifyFrbBridgeCoverage {
facade_path: facade_rel.clone(),
bridge_path: bridge_rel.clone(),
exclude_functions: vec![],
},
PostBuildStep::PostProcessFile {
path: bridge_rel.clone(),
processor: PostProcessor::DartStripTrailingWhitespace,
},
],
};
let result = crate::cli::pipeline::run_post_build(
Language::Dart,
&build_config,
&ResolvedCrateConfig::default(),
dir.path(),
);
match previous {
Some(value) => unsafe { std::env::set_var("ALEF_SKIP_COMMANDS", value) },
None => unsafe { std::env::remove_var("ALEF_SKIP_COMMANDS") },
}
let error = result.expect_err("a stale bridge behind a skipped frb run must fail the build");
assert!(
format!("{error:#}").contains("record_price"),
"error must name the missing function: {error:#}"
);
let bridge_after = std::fs::read_to_string(dir.path().join(&bridge_rel)).unwrap();
assert_eq!(
bridge_after, stale_bridge_with_trailing_whitespace,
"the PostProcessFile step after VerifyFrbBridgeCoverage must never run against the stale bridge"
);
}
}