use mitts::adapter::rust::features::{
resolve_package_features_with_resolver, PackageFeatureContext,
};
use mitts::adapter::rust::manifest::CargoToml;
use mitts::adapter::{BuildMode, RustAdapter, RustBuildOptions};
use std::fs;
use std::path::Path;
use tempfile::TempDir;
fn write_crate(dir: &Path, manifest: &str, lib_src: &str) {
fs::create_dir_all(dir.join("src")).unwrap();
fs::write(dir.join("Cargo.toml"), manifest).unwrap();
fs::write(dir.join("src/lib.rs"), lib_src).unwrap();
}
#[test]
fn test_standalone_root_proc_macro_host_semantics_and_feature_survival() {
let isolated_cache = TempDir::new().unwrap();
let root = TempDir::new().unwrap();
write_crate(
&root.path().join("helper"),
"[package]\nname = \"helper\"\nversion = \"0.1.0\"\nedition = \"2021\"\n",
"pub fn helper_fn() -> i32 { 10 }\n",
);
write_crate(
&root.path().join("test_helper"),
"[package]\nname = \"test_helper\"\nversion = \"0.1.0\"\nedition = \"2021\"\n",
"pub fn test_val() -> i32 { 20 }\n",
);
let macro_dir = root.path().join("my_macro");
write_crate(
¯o_dir,
"[package]\nname = \"my_macro\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n\
[lib]\nproc-macro = true\n\n\
[features]\ndefault = [\"std\"]\nstd = [\"dep:helper\"]\nextra = []\n\n\
[dependencies]\nhelper = { path = \"../helper\", optional = true }\n\n\
[dev-dependencies]\ntest_helper = { path = \"../test_helper\" }\n",
"extern crate proc_macro;\nuse proc_macro::TokenStream;\n\
#[proc_macro]\npub fn identity(item: TokenStream) -> TokenStream { item }\n\
#[cfg(feature = \"std\")]\nconst _: fn() -> i32 = helper::helper_fn;\n\
#[test]\nfn test_in_macro() { assert_eq!(test_helper::test_val(), 20); }\n",
);
let manifest_path = macro_dir.join("Cargo.toml");
let target_dir = macro_dir.join("target");
let opt_build = RustBuildOptions {
manifest_path: Some(manifest_path.clone()),
cache_dir: Some(isolated_cache.path().to_path_buf()),
target_dir: Some(target_dir.clone()),
..Default::default()
};
let plan_build = RustAdapter::explain_plan(&opt_build).expect("explain plan build");
let macro_unit = plan_build
.units
.iter()
.find(|u| u.package_name == "my_macro" && u.kind == "proc-macro")
.expect("my_macro proc-macro unit");
assert!(macro_unit.for_host, "root proc-macro must be for_host");
assert!(
macro_unit.features.contains(&"default".to_string()),
"default feature must survive on host proc-macro: {:?}",
macro_unit.features
);
assert!(
macro_unit.features.contains(&"std".to_string()),
"std feature must survive on host proc-macro: {:?}",
macro_unit.features
);
let helper_unit = plan_build
.units
.iter()
.find(|u| u.package_name == "helper")
.expect("helper unit");
assert!(
helper_unit.for_host,
"proc-macro dependency must be for_host"
);
assert!(
!plan_build
.units
.iter()
.any(|u| u.package_name == "test_helper"),
"test_helper must not be in build mode plan"
);
let opt_test = RustBuildOptions {
manifest_path: Some(manifest_path.clone()),
cache_dir: Some(isolated_cache.path().to_path_buf()),
target_dir: Some(target_dir.clone()),
mode: BuildMode::Test,
all_tests: true,
..Default::default()
};
let plan_test = RustAdapter::explain_plan(&opt_test).expect("explain plan test");
let test_unit = plan_test
.units
.iter()
.find(|u| u.package_name == "my_macro" && u.kind == "test")
.expect("my_macro test unit");
assert!(test_unit.for_host, "proc-macro test unit must be for_host");
assert!(
test_unit.features.contains(&"default".to_string()),
"default feature must survive on proc-macro test unit: {:?}",
test_unit.features
);
let test_helper_unit = plan_test
.units
.iter()
.find(|u| u.package_name == "test_helper")
.expect("test_helper unit in test mode");
assert!(
test_helper_unit.for_host,
"dev-dependency of proc-macro must be for_host"
);
let opt_no_def = RustBuildOptions {
manifest_path: Some(manifest_path.clone()),
cache_dir: Some(isolated_cache.path().to_path_buf()),
target_dir: Some(target_dir.clone()),
no_default_features: true,
features: vec!["extra".to_string()],
..Default::default()
};
let plan_no_def = RustAdapter::explain_plan(&opt_no_def).expect("explain plan no_def");
let macro_no_def = plan_no_def
.units
.iter()
.find(|u| u.package_name == "my_macro" && u.kind == "proc-macro")
.expect("my_macro unit");
assert!(
!macro_no_def.features.contains(&"default".to_string()),
"default feature must be suppressed"
);
assert!(
macro_no_def.features.contains(&"extra".to_string()),
"explicit extra feature must be present"
);
let opt_explicit_test = RustBuildOptions {
mode: BuildMode::Test,
all_tests: true,
..opt_no_def
};
let explicit_test_plan = RustAdapter::explain_plan(&opt_explicit_test).unwrap();
let explicit_test = explicit_test_plan
.units
.iter()
.find(|unit| unit.package_name == "my_macro" && unit.kind == "test")
.unwrap();
assert!(explicit_test.for_host);
assert!(explicit_test.features.contains(&"extra".to_string()));
assert!(!explicit_test.features.contains(&"default".to_string()));
let (_, res_build) = RustAdapter::build(&opt_build).expect("build proc macro");
assert!(
res_build.success,
"Standalone proc-macro build failed: {}",
res_build.stderr
);
let (_, res_test) = RustAdapter::build(&opt_test).expect("build test proc macro");
assert!(
res_test.success,
"Standalone proc-macro test build failed: {}",
res_test.stderr
);
}
#[test]
fn test_workspace_macro_selection_without_active_consumer() {
let isolated_cache = TempDir::new().unwrap();
let root = TempDir::new().unwrap();
fs::write(
root.path().join("Cargo.toml"),
"[workspace]\nmembers = [\"my_macro\", \"unused_app\"]\nresolver = \"2\"\n",
)
.unwrap();
let macro_dir = root.path().join("my_macro");
write_crate(
¯o_dir,
"[package]\nname = \"my_macro\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[lib]\nproc-macro = true\n",
"extern crate proc_macro;\nuse proc_macro::TokenStream;\n#[proc_macro]\npub fn noop(item: TokenStream) -> TokenStream { item }\n",
);
let app_dir = root.path().join("unused_app");
fs::create_dir_all(app_dir.join("src")).unwrap();
fs::write(
app_dir.join("Cargo.toml"),
"[package]\nname = \"unused_app\"\nversion = \"0.1.0\"\nedition = \"2021\"\n",
)
.unwrap();
fs::write(app_dir.join("src/main.rs"), "fn main() {}\n").unwrap();
let opt = RustBuildOptions {
manifest_path: Some(root.path().join("Cargo.toml")),
cache_dir: Some(isolated_cache.path().to_path_buf()),
target_dir: Some(root.path().join("target")),
package: Some("my_macro".to_string()),
..Default::default()
};
let plan = RustAdapter::explain_plan(&opt).expect("explain plan");
let macro_unit = plan
.units
.iter()
.find(|u| u.package_name == "my_macro")
.expect("my_macro unit");
assert!(
macro_unit.for_host,
"selected workspace proc-macro without consumer must be for_host"
);
assert!(
!plan.units.iter().any(|u| u.package_name == "unused_app"),
"unused_app must be pruned from plan"
);
let (_, res) = RustAdapter::build(&opt).expect("build");
assert!(
res.success,
"Workspace -p proc-macro build failed: {}",
res.stderr
);
}
#[test]
fn test_proc_macro_resolver_2_decoupling_and_resolver_1_unification() {
let macro_toml = CargoToml::parse(
r#"
[package]
name = "pm_crate"
version = "0.1.0"
edition = "2021"
[lib]
proc-macro = true
[dependencies]
shared = { version = "1.0.0", features = ["host_feat"] }
"#,
)
.unwrap();
let app_toml = CargoToml::parse(
r#"
[package]
name = "app_crate"
version = "0.1.0"
edition = "2021"
[dependencies]
pm_crate = { version = "0.1.0" }
shared = { version = "1.0.0", features = ["target_feat"] }
"#,
)
.unwrap();
let shared_toml = CargoToml::parse(
r#"
[package]
name = "shared"
version = "1.0.0"
edition = "2021"
[features]
host_feat = []
target_feat = []
"#,
)
.unwrap();
let shared_deps = Vec::new();
let pm_deps = vec!["shared 1.0.0".to_string()];
let app_deps = vec!["pm_crate 0.1.0".to_string(), "shared 1.0.0".to_string()];
let pkgs = vec![
PackageFeatureContext {
name: "app_crate",
version: "0.1.0",
source: None,
is_workspace: true,
is_root: true,
toml: &app_toml,
dependencies: &app_deps,
},
PackageFeatureContext {
name: "pm_crate",
version: "0.1.0",
source: None,
is_workspace: true,
is_root: false,
toml: ¯o_toml,
dependencies: &pm_deps,
},
PackageFeatureContext {
name: "shared",
version: "1.0.0",
source: None,
is_workspace: false,
is_root: false,
toml: &shared_toml,
dependencies: &shared_deps,
},
];
let opt = RustBuildOptions::default();
let res2 = resolve_package_features_with_resolver(&pkgs, &opt, Some("2")).unwrap();
let shared_target_feats = res2.target.get("shared@1.0.0").cloned().unwrap_or_default();
let shared_host_feats = res2.host.get("shared@1.0.0").cloned().unwrap_or_default();
assert!(
shared_target_feats.contains(&"target_feat".to_string()),
"target shared must have target_feat"
);
assert!(
!shared_target_feats.contains(&"host_feat".to_string()),
"Resolver 2: target shared must not have host_feat"
);
assert!(
shared_host_feats.contains(&"host_feat".to_string()),
"host shared must have host_feat"
);
assert!(
!shared_host_feats.contains(&"target_feat".to_string()),
"Resolver 2: host shared must not have target_feat"
);
let res1 = resolve_package_features_with_resolver(&pkgs, &opt, Some("1")).unwrap();
let r1_target = res1.target.get("shared@1.0.0").cloned().unwrap_or_default();
let r1_host = res1.host.get("shared@1.0.0").cloned().unwrap_or_default();
assert!(
r1_target.contains(&"target_feat".to_string())
&& r1_target.contains(&"host_feat".to_string()),
"Resolver 1: target must unify host and target features"
);
assert_eq!(
r1_target, r1_host,
"Resolver 1: target and host features must be identical"
);
}