use super::*;
fn config_with_extra_deps() -> ResolvedCrateConfig {
let mut config = test_config();
config
.extra_dependencies
.insert("anyhow".to_string(), toml::Value::String("1.0".to_string()));
config.extra_dependencies.insert(
"tracing".to_string(),
toml::Value::Table({
let mut t = toml::map::Map::new();
t.insert("version".to_string(), toml::Value::String("0.1".to_string()));
t.insert(
"features".to_string(),
toml::Value::Array(vec![toml::Value::String("log".to_string())]),
);
t
}),
);
config
}
#[test]
fn test_render_extra_deps_empty() {
let config = test_config();
assert_eq!(render_extra_deps(&config, Language::Python), "");
}
#[test]
fn test_render_extra_deps_string_version() {
let config = config_with_extra_deps();
let rendered = render_extra_deps(&config, Language::Python);
assert!(rendered.contains("anyhow = \"1.0\""), "got: {rendered}");
}
#[test]
fn test_render_extra_deps_table_value() {
let config = config_with_extra_deps();
let rendered = render_extra_deps(&config, Language::Python);
assert!(rendered.contains("tracing = "), "got: {rendered}");
assert!(rendered.contains("\"log\""), "got: {rendered}");
}
#[test]
fn test_render_extra_deps_sorted() {
let config = config_with_extra_deps();
let rendered = render_extra_deps(&config, Language::Python);
let anyhow_pos = rendered.find("anyhow").expect("anyhow missing");
let tracing_pos = rendered.find("tracing").expect("tracing missing");
assert!(anyhow_pos < tracing_pos, "deps should be sorted alphabetically");
}
#[test]
fn test_scaffold_python_cargo_extra_deps() {
let config = config_with_extra_deps();
let api = test_api();
let all_files = scaffold(&api, &config, &[Language::Python]).unwrap();
let files = language_files(&all_files);
let cargo_toml = files.iter().find(|f| f.path.ends_with("Cargo.toml")).unwrap();
assert!(
cargo_toml.content.contains("anyhow = \"1.0\""),
"content: {}",
cargo_toml.content
);
assert!(
cargo_toml.content.contains("tracing"),
"content: {}",
cargo_toml.content
);
let deps_pos = cargo_toml.content.find("[dependencies]").unwrap();
let anyhow_pos = cargo_toml.content.find("anyhow").unwrap();
assert!(anyhow_pos > deps_pos, "anyhow should appear inside [dependencies]");
}
#[test]
fn test_scaffold_node_cargo_extra_deps() {
let config = config_with_extra_deps();
let api = test_api();
let all_files = scaffold(&api, &config, &[Language::Node]).unwrap();
let files = language_files(&all_files);
let cargo_toml = files.iter().find(|f| f.path.ends_with("Cargo.toml")).unwrap();
assert!(
cargo_toml.content.contains("anyhow = \"1.0\""),
"content: {}",
cargo_toml.content
);
}
#[test]
fn test_scaffold_ruby_cargo_extra_deps() {
let config = config_with_extra_deps();
let api = test_api();
let all_files = scaffold(&api, &config, &[Language::Ruby]).unwrap();
let files = language_files(&all_files);
let cargo_toml = files.iter().find(|f| f.path.ends_with("Cargo.toml")).unwrap();
assert!(
cargo_toml.content.contains("anyhow = \"1.0\""),
"content: {}",
cargo_toml.content
);
}
#[test]
fn test_scaffold_php_cargo_extra_deps() {
let config = config_with_extra_deps();
let api = test_api();
let all_files = scaffold(&api, &config, &[Language::Php]).unwrap();
let files = language_files(&all_files);
let cargo_toml = files.iter().find(|f| f.path.ends_with("Cargo.toml")).unwrap();
assert!(
cargo_toml.content.contains("anyhow = \"1.0\""),
"content: {}",
cargo_toml.content
);
}
#[test]
fn test_scaffold_elixir_cargo_extra_deps() {
let config = config_with_extra_deps();
let api = test_api();
let all_files = scaffold(&api, &config, &[Language::Elixir]).unwrap();
let files = language_files(&all_files);
let cargo_toml = files.iter().find(|f| f.path.ends_with("Cargo.toml")).unwrap();
assert!(
cargo_toml.content.contains("anyhow = \"1.0\""),
"content: {}",
cargo_toml.content
);
}
#[test]
fn test_scaffold_language_level_extra_deps_override_crate_level() {
let mut config = test_config();
config
.extra_dependencies
.insert("shared-dep".to_string(), toml::Value::String("1.0".to_string()));
let mut python_extra: std::collections::HashMap<String, toml::Value> = std::collections::HashMap::new();
python_extra.insert("shared-dep".to_string(), toml::Value::String("2.0".to_string()));
config.python = Some(PythonConfig {
module_name: None,
async_runtime: None,
stubs: None,
pip_name: None,
features: None,
serde_rename_all: None,
capsule_types: std::collections::HashMap::new(),
release_gil: false,
exclude_functions: vec![],
exclude_types: vec![],
extra_dependencies: python_extra,
pip_dependencies: Vec::new(),
sdist_include: Vec::new(),
scaffold_output: None,
rename_fields: Default::default(),
run_wrapper: None,
extra_lint_paths: Vec::new(),
extra_init_imports: std::collections::BTreeMap::new(),
reexported_types: Vec::new(),
});
let rendered = render_extra_deps(&config, Language::Python);
assert!(rendered.contains("shared-dep = \"2.0\""), "got: {rendered}");
assert!(
!rendered.contains("1.0"),
"crate-level version should be overridden, got: {rendered}"
);
}
fn dep_keys_in_order(cargo_toml: &str) -> Vec<&str> {
let mut in_deps = false;
let mut keys = Vec::new();
for line in cargo_toml.lines() {
if line.trim_start().starts_with('[') {
in_deps = line.trim() == "[dependencies]";
continue;
}
if in_deps {
if let Some(key) = line.split('=').next() {
let key = key.trim();
if !key.is_empty() && !key.starts_with('#') {
keys.push(key);
}
}
}
}
keys
}
#[test]
fn test_scaffold_elixir_cargo_deps_are_alphabetically_sorted() {
use crate::core::config::TraitBridgeConfig;
let mut config = test_config();
config.languages = vec![Language::Elixir];
config.trait_bridges = vec![TraitBridgeConfig {
trait_name: "HtmlVisitor".to_string(),
super_trait: None,
registry_getter: None,
register_fn: None,
unregister_fn: None,
clear_fn: None,
type_alias: None,
param_name: None,
register_extra_args: None,
exclude_languages: vec![],
ffi_skip_methods: Vec::new(),
bind_via: crate::core::config::BridgeBinding::OptionsField,
options_type: Some("ConversionOptions".to_string()),
options_field: None,
context_type: None,
result_type: None,
}];
let api = test_api();
let all_files = scaffold(&api, &config, &[Language::Elixir]).unwrap();
let files = language_files(&all_files);
let cargo_toml = files.iter().find(|f| f.path.ends_with("Cargo.toml")).unwrap();
let keys = dep_keys_in_order(&cargo_toml.content);
assert!(
keys.contains(&"async-trait"),
"async-trait must appear when trait bridges are configured; keys: {keys:?}"
);
assert!(
keys.contains(&"tokio"),
"tokio must appear when trait bridges are configured; keys: {keys:?}"
);
let mut sorted = keys.clone();
sorted.sort();
assert_eq!(
keys, sorted,
"elixir Cargo.toml [dependencies] must be alphabetically sorted; got: {keys:?}"
);
}
#[test]
fn test_scaffold_ruby_cargo_deps_are_alphabetically_sorted() {
use crate::core::config::TraitBridgeConfig;
let mut config = test_config();
config.languages = vec![Language::Ruby];
config.trait_bridges = vec![TraitBridgeConfig {
trait_name: "HtmlVisitor".to_string(),
super_trait: None,
registry_getter: None,
register_fn: None,
unregister_fn: None,
clear_fn: None,
type_alias: None,
param_name: None,
register_extra_args: None,
exclude_languages: vec![],
ffi_skip_methods: Vec::new(),
bind_via: crate::core::config::BridgeBinding::OptionsField,
options_type: Some("ConversionOptions".to_string()),
options_field: None,
context_type: None,
result_type: None,
}];
let api = test_api();
let all_files = scaffold(&api, &config, &[Language::Ruby]).unwrap();
let files = language_files(&all_files);
let cargo_toml = files.iter().find(|f| f.path.ends_with("Cargo.toml")).unwrap();
let keys = dep_keys_in_order(&cargo_toml.content);
assert!(
keys.contains(&"async-trait"),
"async-trait must appear when trait bridges are configured; keys: {keys:?}"
);
assert!(
keys.contains(&"tokio"),
"tokio must appear when trait bridges are configured; keys: {keys:?}"
);
let mut sorted = keys.clone();
sorted.sort();
assert_eq!(
keys, sorted,
"ruby Cargo.toml [dependencies] must be alphabetically sorted; got: {keys:?}"
);
}
#[test]
fn test_scaffold_r_cargo_deps_are_alphabetically_sorted() {
use crate::core::config::TraitBridgeConfig;
let mut config = test_config();
config.languages = vec![Language::R];
config.trait_bridges = vec![TraitBridgeConfig {
trait_name: "HtmlVisitor".to_string(),
super_trait: None,
registry_getter: None,
register_fn: None,
unregister_fn: None,
clear_fn: None,
type_alias: None,
param_name: None,
register_extra_args: None,
exclude_languages: vec![],
ffi_skip_methods: Vec::new(),
bind_via: crate::core::config::BridgeBinding::OptionsField,
options_type: Some("ConversionOptions".to_string()),
options_field: None,
context_type: None,
result_type: None,
}];
let api = test_api();
let all_files = scaffold(&api, &config, &[Language::R]).unwrap();
let files = language_files(&all_files);
let cargo_toml = files.iter().find(|f| f.path.ends_with("Cargo.toml")).unwrap();
let keys = dep_keys_in_order(&cargo_toml.content);
assert!(
keys.contains(&"async-trait"),
"async-trait must appear when trait bridges are configured; keys: {keys:?}"
);
let mut sorted = keys.clone();
sorted.sort();
assert_eq!(
keys, sorted,
"r Cargo.toml [dependencies] must be alphabetically sorted; got: {keys:?}"
);
}
#[test]
fn test_scaffold_elixir_cargo_deps_sorted_no_trait_bridges() {
let mut config = test_config();
config.languages = vec![Language::Elixir];
let api = test_api();
let all_files = scaffold(&api, &config, &[Language::Elixir]).unwrap();
let files = language_files(&all_files);
let cargo_toml = files.iter().find(|f| f.path.ends_with("Cargo.toml")).unwrap();
let keys = dep_keys_in_order(&cargo_toml.content);
let mut sorted = keys.clone();
sorted.sort();
assert_eq!(
keys, sorted,
"elixir Cargo.toml [dependencies] must be alphabetically sorted (sync-only); got: {keys:?}"
);
}
#[test]
fn test_scaffold_r_cargo_deps_sorted_no_trait_bridges() {
let mut config = test_config();
config.languages = vec![Language::R];
let api = test_api();
let all_files = scaffold(&api, &config, &[Language::R]).unwrap();
let files = language_files(&all_files);
let cargo_toml = files.iter().find(|f| f.path.ends_with("Cargo.toml")).unwrap();
let keys = dep_keys_in_order(&cargo_toml.content);
let mut sorted = keys.clone();
sorted.sort();
assert_eq!(
keys, sorted,
"r Cargo.toml [dependencies] must be alphabetically sorted (no trait bridges); got: {keys:?}"
);
}
fn section_headers_in_order(cargo_toml: &str) -> Vec<&str> {
cargo_toml
.lines()
.filter_map(|line| {
let t = line.trim();
if t.starts_with('[') && !t.starts_with("[[") {
Some(t)
} else {
None
}
})
.collect()
}
#[test]
fn test_scaffold_elixir_cargo_section_order_is_cargo_sort_canonical() {
let config = test_config();
let api = test_api();
let all_files = scaffold(&api, &config, &[Language::Elixir]).unwrap();
let files = language_files(&all_files);
let cargo_toml = files.iter().find(|f| f.path.ends_with("Cargo.toml")).unwrap();
let headers = section_headers_in_order(&cargo_toml.content);
let workspace_pos = headers.iter().position(|h| *h == "[workspace]");
let lib_pos = headers.iter().position(|h| *h == "[lib]");
let deps_pos = headers.iter().position(|h| *h == "[dependencies]");
assert!(
workspace_pos.is_some(),
"Elixir NIF Cargo.toml must contain a [workspace] section; headers: {headers:?}"
);
assert!(
lib_pos.is_some(),
"Elixir NIF Cargo.toml must contain a [lib] section; headers: {headers:?}"
);
assert!(
deps_pos.is_some(),
"Elixir NIF Cargo.toml must contain a [dependencies] section; headers: {headers:?}"
);
assert!(
workspace_pos < lib_pos,
"[workspace] must come before [lib] (cargo-sort canonical); headers: {headers:?}"
);
assert!(
lib_pos < deps_pos,
"[lib] must come before [dependencies] (cargo-sort canonical); headers: {headers:?}"
);
}
#[test]
fn test_render_extra_deps_injects_version_for_workspace_member() {
use std::fs;
use tempfile::TempDir;
let tmp = TempDir::new().unwrap();
let root = tmp.path();
fs::write(
root.join("Cargo.toml"),
r#"
[workspace]
resolver = "2"
members = ["crates/my-lib-http"]
[workspace.package]
version = "2.5.0"
"#,
)
.unwrap();
fs::create_dir_all(root.join("crates/my-lib-http/src")).unwrap();
fs::write(root.join("crates/my-lib-http/src/lib.rs"), "pub fn f() {}").unwrap();
fs::write(
root.join("crates/my-lib-http/Cargo.toml"),
"[package]\nname = \"my-lib-http\"\nversion.workspace = true\n",
)
.unwrap();
let mut config = test_config();
config.workspace_root = Some(root.to_path_buf());
config.extra_dependencies.insert(
"my-lib-http".to_string(),
toml::Value::Table(toml::map::Map::from_iter([(
"path".to_string(),
toml::Value::String("../my-lib-http".to_string()),
)])),
);
config
.extra_dependencies
.insert("anyhow".to_string(), toml::Value::String("1.0".to_string()));
let rendered = render_extra_deps(&config, Language::Python);
assert!(
rendered.contains(r#"my-lib-http = { path = "../my-lib-http", version = "2.5.0" }"#),
"workspace member should get the resolved workspace version injected; got:\n{rendered}"
);
assert!(
rendered.contains(r#"anyhow = "1.0""#),
"non-member external dep must be emitted unchanged; got:\n{rendered}"
);
}
#[test]
fn test_render_extra_deps_leaves_non_member_path_dep_unchanged() {
use std::fs;
use tempfile::TempDir;
let tmp = TempDir::new().unwrap();
let root = tmp.path();
fs::write(
root.join("Cargo.toml"),
"[workspace]\nmembers = []\n[workspace.package]\nversion = \"2.5.0\"\n",
)
.unwrap();
let mut config = test_config();
config.workspace_root = Some(root.to_path_buf());
config.extra_dependencies.insert(
"vendored-thing".to_string(),
toml::Value::Table(toml::map::Map::from_iter([(
"path".to_string(),
toml::Value::String("../../vendor/thing".to_string()),
)])),
);
let rendered = render_extra_deps(&config, Language::Python);
assert!(
rendered.contains(r#"vendored-thing = { path = "../../vendor/thing" }"#),
"non-member path dep must remain path-only; got:\n{rendered}"
);
assert!(
!rendered.contains("version ="),
"no version should be injected for a non-member; got:\n{rendered}"
);
}
#[test]
fn test_render_extra_deps_does_not_double_inject_version() {
use std::fs;
use tempfile::TempDir;
let tmp = TempDir::new().unwrap();
let root = tmp.path();
fs::write(
root.join("Cargo.toml"),
"[workspace]\nmembers = [\"crates/my-lib-http\"]\n[workspace.package]\nversion = \"2.5.0\"\n",
)
.unwrap();
fs::create_dir_all(root.join("crates/my-lib-http/src")).unwrap();
fs::write(root.join("crates/my-lib-http/src/lib.rs"), "pub fn f() {}").unwrap();
fs::write(
root.join("crates/my-lib-http/Cargo.toml"),
"[package]\nname = \"my-lib-http\"\nversion = \"9.9.9\"\n",
)
.unwrap();
let mut config = test_config();
config.workspace_root = Some(root.to_path_buf());
config.extra_dependencies.insert(
"my-lib-http".to_string(),
toml::Value::Table(toml::map::Map::from_iter([
("path".to_string(), toml::Value::String("../my-lib-http".to_string())),
("version".to_string(), toml::Value::String("1.0".to_string())),
])),
);
let rendered = render_extra_deps(&config, Language::Python);
assert!(
rendered.contains(r#"version = "1.0""#),
"pre-existing version must be preserved; got:\n{rendered}"
);
assert!(
!rendered.contains("9.9.9") && !rendered.contains("2.5.0"),
"must not overwrite or append a second version; got:\n{rendered}"
);
}
#[test]
fn test_render_extra_deps_swift_injects_version_for_workspace_member() {
use std::fs;
use tempfile::TempDir;
let tmp = TempDir::new().unwrap();
let root = tmp.path();
fs::write(
root.join("Cargo.toml"),
r#"
[workspace]
resolver = "2"
members = ["crates/my-lib-http"]
[workspace.package]
version = "3.1.0"
"#,
)
.unwrap();
fs::create_dir_all(root.join("crates/my-lib-http/src")).unwrap();
fs::write(root.join("crates/my-lib-http/src/lib.rs"), "pub fn f() {}").unwrap();
fs::write(
root.join("crates/my-lib-http/Cargo.toml"),
"[package]\nname = \"my-lib-http\"\nversion.workspace = true\n",
)
.unwrap();
let mut config = test_config();
config.workspace_root = Some(root.to_path_buf());
config.extra_dependencies.insert(
"my-lib-http".to_string(),
toml::Value::Table(toml::map::Map::from_iter([(
"path".to_string(),
toml::Value::String("../my-lib-http".to_string()),
)])),
);
let rendered = render_extra_deps(&config, Language::Swift);
assert!(
rendered.contains(r#"version = "3.1.0""#),
"swift backend: workspace member must get version injected; got:\n{rendered}"
);
assert!(
rendered.contains(r#"path = "../my-lib-http""#),
"swift backend: path must be preserved alongside injected version; got:\n{rendered}"
);
}