use super::{
create_once_overwrite, handle, refused_snippet_dir_paths, report_deferred_formatting,
sync_registry_versions_before_all,
};
use crate::bin_cli::args::Commands;
use crate::bin_cli::dispatch::DispatchContext;
use crate::cli::cache;
use crate::core::backend::GeneratedFile;
use crate::core::config::NewAlefConfig;
const HAND_GROWN_COMPOSER_JSON: &str = concat!(
"{\n",
" \"name\": \"consumer/sample-lib\",\n",
" \"scripts\": { \"test\": \"vendor/bin/phpunit --testdox\" },\n",
" \"autoload\": { \"psr-4\": { \"Consumer\\\\\": \"src/\" } }\n",
"}\n",
);
const GENERATED_COMPOSER_PLACEHOLDER: &str = concat!(
"{\n",
" \"name\": \"alef/placeholder\",\n",
" \"require\": {}\n",
"}\n",
);
const CREATE_ONCE_SEED_PATH: &str = "packages/php/composer.json";
fn seed_hand_grown_create_once_file(base: &std::path::Path) -> std::path::PathBuf {
let relative = std::path::PathBuf::from(CREATE_ONCE_SEED_PATH);
let full = base.join(&relative);
std::fs::create_dir_all(full.parent().expect("seed path has a parent")).expect("create seed directory");
std::fs::write(&full, HAND_GROWN_COMPOSER_JSON).expect("write hand-grown seed");
cache::record_scaffold_owned_path(base, &full).expect("record alef ownership of the seed");
relative
}
fn generated_seed_placeholder(relative: std::path::PathBuf) -> GeneratedFile {
GeneratedFile {
path: relative,
content: GENERATED_COMPOSER_PLACEHOLDER.to_string(),
generated_header: false,
}
}
fn write_seed_with_overwrite(base: &std::path::Path, overwrite: bool) -> (String, usize) {
let relative = std::path::PathBuf::from(CREATE_ONCE_SEED_PATH);
let report = crate::cli::pipeline::write_scaffold_files_report(
&[generated_seed_placeholder(relative.clone())],
base,
overwrite,
)
.expect("write report");
assert!(
report.refused_paths.is_empty(),
"the ownership guard must not fire on a recorded path -- a refusal here would make this \
test blind to what `overwrite` does: {:?}",
report.refused_paths
);
let on_disk = std::fs::read_to_string(base.join(&relative)).expect("read seed after write");
(on_disk, report.changed_count())
}
#[test]
fn clean_alone_leaves_a_pre_existing_create_once_seed_untouched() {
for (clean, clobber) in [(true, false), (false, false)] {
let dir = tempfile::tempdir().expect("tempdir");
let base = dir.path();
seed_hand_grown_create_once_file(base);
let (on_disk, changed) = write_seed_with_overwrite(base, create_once_overwrite(clean, clobber));
assert_eq!(
on_disk, HAND_GROWN_COMPOSER_JSON,
"--clean must not disable the create-only skip (clean={clean}, clobber={clobber})"
);
assert_eq!(
changed, 0,
"a skipped seed is not a change (clean={clean}, clobber={clobber})"
);
}
}
#[test]
fn clobber_create_once_seeds_replaces_a_pre_existing_create_once_seed() {
let dir = tempfile::tempdir().expect("tempdir");
let base = dir.path();
seed_hand_grown_create_once_file(base);
let (on_disk, changed) = write_seed_with_overwrite(base, create_once_overwrite(false, true));
assert_eq!(
on_disk, GENERATED_COMPOSER_PLACEHOLDER,
"--clobber-create-once-seeds must overwrite a pre-existing seed alef is recorded as owning"
);
assert_eq!(changed, 1, "an overwritten seed must be counted as a change");
}
#[test]
fn clean_and_clobber_together_reproduce_the_pre_separation_clean_behaviour() {
let separated_dir = tempfile::tempdir().expect("tempdir");
let separated_base = separated_dir.path();
seed_hand_grown_create_once_file(separated_base);
let separated = write_seed_with_overwrite(separated_base, create_once_overwrite(true, true));
let legacy_dir = tempfile::tempdir().expect("tempdir");
let legacy_base = legacy_dir.path();
seed_hand_grown_create_once_file(legacy_base);
let legacy = write_seed_with_overwrite(legacy_base, true);
assert_eq!(
separated, legacy,
"`--clean --clobber-create-once-seeds` must leave the tree in exactly the state the old \
coupled `--clean` left it in -- same bytes, same changed count"
);
assert_eq!(
separated.0, GENERATED_COMPOSER_PLACEHOLDER,
"both flags together must reach the overwriting branch, not agree on doing nothing"
);
}
#[test]
fn all_generates_snippets_before_readmes_consume_them() {
let source = include_str!("all_commands.rs");
let e2e = source.find("e2e_stage::run(").expect("e2e stage call site");
let readmes = source.find("Generating READMEs...").expect("README stage");
assert!(
e2e < readmes,
"README generation must observe snippets produced by the same run"
);
}
#[test]
fn all_runs_its_only_build_step_before_the_docs_stage_that_validates_snippets() {
let source = include_str!("all_commands.rs");
let post_build = source
.find("Running post-build processing...")
.expect("post-build stage");
let docs = source.find("Generating docs...").expect("docs stage");
assert!(
post_build < docs,
"the only build `all` performs (FFI cdylib + per-backend post-build hooks, via \
complete_generated_artifacts) runs before the docs stage that triggers snippet validation -- \
but that build is FFI-only and does not satisfy typecheck/compile/run snippet validation for \
languages needing a full per-language build (typescript, java, kotlin, swift, zig, ...)"
);
}
#[test]
fn all_never_calls_the_general_per_language_build_stage() {
let source = include_str!("all_commands.rs");
assert!(
!source.contains("pipeline::build("),
"`alef all`'s documented scope (\"generate + stubs + scaffold + readme + docs + sync + e2e\") \
excludes building native artifacts; the only build all_commands.rs may trigger is the narrow \
FFI-only one inside `complete_generated_artifacts`. If this now calls `pipeline::build` \
directly, `docs::enforce_snippet_summary`'s unresolved-dependency reporting (see its doc \
comment in `docs/mod.rs`) is stale and must be revisited alongside this test."
);
}
#[test]
fn all_never_drops_refusals_through_the_count_only_write_wrapper() {
let source = include_str!("all_commands.rs");
assert!(
!source.contains("write_scaffold_files_with_overwrite"),
"`alef all` must write through `write_scaffold_files_report` and fold every result into \
`refusals` via `absorb_refusals` -- the count-only wrapper silently drops refused writes, \
which is what let a run with thousands of refusals report success"
);
}
#[test]
fn all_correlates_a_docs_stage_failure_with_pending_write_refusals() {
let source = include_str!("all_commands.rs");
let doc_write = source
.find("write_scaffold_files_report(&doc_files")
.expect("docs write must go through the refusal-tracking writer");
let correlation = source
.find("Docs/snippet validation reads content from")
.expect("doc-result error path must explain a possible refusal/stale-content correlation");
assert!(
doc_write < correlation,
"the docs write must be folded into `refusals` before its failure path can correlate a \
validation error with pending write refusals"
);
}
#[test]
fn all_scopes_the_docs_stage_failure_blame_to_snippet_dir_refusals_not_the_run_wide_count() {
let source = include_str!("all_commands.rs");
let err_arm_start = source
.find("Err(error) => {")
.expect("the docs-stage match must have an Err arm");
let err_arm = &source[err_arm_start..];
assert!(
!err_arm[..2000.min(err_arm.len())].contains("refusals.refused_count() > 0"),
"the docs-stage Err arm must not gate its ownership-guard blame on the run-wide \
`refusals.refused_count()` -- that blames refusals with no relationship to the snippet \
tree that actually failed validation"
);
assert!(
err_arm[..2000.min(err_arm.len())].contains("!snippet_refusals.is_empty()"),
"the docs-stage Err arm must gate its ownership-guard blame on `snippet_refusals` -- the \
same `docs.snippets`-scoped set the Ok arm above it already consults via \
`refused_snippet_dir_paths` -- so a validation failure and a validation pass attribute \
refusals identically"
);
}
fn write_neutral_config(root: &std::path::Path, cargo_toml: &str, hash: &str) -> std::path::PathBuf {
let cargo_path = root.join("Cargo.toml");
std::fs::write(&cargo_path, cargo_toml).expect("write Cargo.toml");
let config_path = root.join("alef.toml");
let config = format!(
concat!(
"[workspace]\nlanguages = [\"zig\"]\n\n",
"[[crates]]\nname = \"sample-core\"\nsources = []\nversion_from = {:?}\n\n",
"[crates.e2e.call]\nfunction = \"sample_call\"\n\n",
"[crates.e2e.registry.packages.zig]\n",
"name = \"sample_pkg\"\nversion = \"0.8.0\"\nhash = {:?}\n"
),
cargo_path.to_string_lossy(),
hash
);
std::fs::write(&config_path, config).expect("write alef.toml");
config_path
}
fn resolve(config_path: &std::path::Path) -> Vec<crate::core::config::ResolvedCrateConfig> {
let raw = std::fs::read_to_string(config_path).expect("read alef.toml");
toml::from_str::<NewAlefConfig>(&raw)
.expect("parse alef.toml")
.resolve()
.expect("resolve alef.toml")
}
#[test]
fn all_preflight_repairs_stale_zig_registry_hash_version() {
let temp = tempfile::tempdir().expect("tempdir");
let stale_hash = "sample_pkg-0.8.0-AbCd_XyZ123456789";
let config_path = write_neutral_config(
temp.path(),
"[package]\nname = \"sample-core\"\nversion = \"0.9.0\"\n",
stale_hash,
);
let configs = resolve(&config_path);
let selected = configs.iter().collect::<Vec<_>>();
let changed = sync_registry_versions_before_all(&config_path, &selected).expect("repair stale hash");
assert!(changed);
let repaired = std::fs::read_to_string(config_path).expect("read repaired config");
assert!(repaired.contains("version = \"0.9.0\""));
assert!(repaired.contains("hash = \"sample_pkg-0.9.0-AbCd_XyZ123456789\""));
}
#[test]
fn all_preflight_rejects_unreadable_version_source_without_mutating_hash() {
let temp = tempfile::tempdir().expect("tempdir");
let stale_hash = "sample_pkg-0.8.0-AbCd_XyZ123456789";
let config_path = write_neutral_config(temp.path(), "not valid TOML", stale_hash);
let configs = resolve(&config_path);
let selected = configs.iter().collect::<Vec<_>>();
let error = sync_registry_versions_before_all(&config_path, &selected).expect_err("invalid version must fail");
assert!(
error
.to_string()
.contains("could not resolve version for crate `sample-core`")
);
let unchanged = std::fs::read_to_string(config_path).expect("read unchanged config");
assert!(unchanged.contains(stale_hash));
}
fn write_config_with_snippet_roots(root: &std::path::Path, dirs: &[&str], exclude: &[&str]) -> std::path::PathBuf {
let cargo_path = root.join("Cargo.toml");
std::fs::write(&cargo_path, "[package]\nname = \"sample-core\"\nversion = \"0.1.0\"\n").expect("write Cargo.toml");
let config_path = root.join("alef.toml");
let dirs_toml = dirs.iter().map(|dir| format!("{dir:?}")).collect::<Vec<_>>().join(", ");
let exclude_toml = exclude
.iter()
.map(|dir| format!("{dir:?}"))
.collect::<Vec<_>>()
.join(", ");
let config = format!(
concat!(
"[workspace]\nlanguages = [\"zig\"]\n\n",
"[workspace.docs.snippets]\ndirs = [{}]\nexclude = [{}]\n\n",
"[[crates]]\nname = \"sample-core\"\nsources = []\nversion_from = {:?}\n"
),
dirs_toml,
exclude_toml,
cargo_path.to_string_lossy(),
);
std::fs::write(&config_path, config).expect("write alef.toml");
config_path
}
#[test]
fn refused_snippet_dir_paths_flags_a_refusal_inside_configured_snippet_dirs() {
let temp = tempfile::tempdir().expect("tempdir");
let config_path = write_config_with_snippet_roots(temp.path(), &["docs/snippets"], &[]);
let configs = resolve(&config_path);
let config = configs.into_iter().next().expect("one crate");
let refused_snippet = temp.path().join("docs/snippets/python/example.md");
let refused_unrelated = temp.path().join("bindings/python/example.py");
let refused_paths = std::collections::BTreeSet::from([refused_snippet.clone(), refused_unrelated]);
let flagged = refused_snippet_dir_paths(&refused_paths, &config, temp.path());
assert_eq!(
flagged,
vec![refused_snippet],
"only the refusal inside docs.snippets.dirs must be flagged, not every refusal in the run"
);
}
#[test]
fn refused_snippet_dir_paths_is_empty_when_no_refusal_touches_the_snippet_roots() {
let temp = tempfile::tempdir().expect("tempdir");
let config_path = write_config_with_snippet_roots(temp.path(), &["docs/snippets"], &[]);
let configs = resolve(&config_path);
let config = configs.into_iter().next().expect("one crate");
let refused_unrelated = temp.path().join("bindings/python/example.py");
let refused_paths = std::collections::BTreeSet::from([refused_unrelated]);
assert!(refused_snippet_dir_paths(&refused_paths, &config, temp.path()).is_empty());
assert!(
refused_snippet_dir_paths(&std::collections::BTreeSet::new(), &config, temp.path()).is_empty(),
"no refusals at all must never be flagged"
);
}
#[test]
fn refused_snippet_dir_paths_respects_configured_exclude_prefixes() {
let temp = tempfile::tempdir().expect("tempdir");
let config_path = write_config_with_snippet_roots(temp.path(), &["docs/snippets"], &["docs/snippets/generated"]);
let configs = resolve(&config_path);
let config = configs.into_iter().next().expect("one crate");
let refused_excluded = temp.path().join("docs/snippets/generated/example.md");
let refused_paths = std::collections::BTreeSet::from([refused_excluded]);
assert!(refused_snippet_dir_paths(&refused_paths, &config, temp.path()).is_empty());
}
#[test]
fn all_checks_for_refused_snippet_writes_on_the_docs_stage_success_path() {
let source = include_str!("all_commands.rs");
let ok_arm = source.find("Ok(()) => {").expect("docs stage Ok arm");
let success_check = source
.find("refused_snippet_dir_paths(&refusals.refused_paths")
.expect("docs stage success path must consult refused_snippet_dir_paths");
let err_arm = source.find("Err(error) => {").expect("docs stage Err arm");
assert!(
ok_arm < success_check,
"the success-path refusal check must live inside `match doc_result`'s `Ok` arm"
);
assert!(
success_check < err_arm,
"the success-path refusal check must run before the `Err` arm begins, not inside it"
);
}
#[test]
fn all_docs_stage_failure_does_not_return_before_formatting_and_hash_stamping() {
let source = include_str!("all_commands.rs");
let err_arm_start = source.find("Err(error) => {").expect("docs stage Err arm");
let err_arm_end = source
.find("docs_stage_error.get_or_insert(error);")
.expect("the docs stage Err arm must defer via docs_stage_error");
let err_arm_body = &source[err_arm_start..err_arm_end];
assert!(
!err_arm_body.contains("return"),
"the docs-stage `Err` arm must defer the failure via `docs_stage_error`, not `return` -- a \
`return` here exits `handle` immediately, skipping formatting, orphan sweeping, hash \
finalisation, deferred-formatting reporting and hook installation for this crate and every \
later crate in this loop. Arm body was: {err_arm_body:?}"
);
let format_generated = source
.find("pipeline::format_generated_reporting(resolved_cfg, &base_dir, None, strict)?")
.expect("the converging whole-tree formatting pass must still run after the docs stage");
let finalize_hashes_sweeping = source
.find("pipeline::finalize_hashes_sweeping(")
.expect("hash stamping must still run after the docs stage");
let sweep_manifest_orphans = source
.find("pipeline::sweep_manifest_orphans(&previous_paths, ¤t_gen_paths, &cleanup_roots, &cleanup_roots)")
.expect("orphan sweeping must still run after the docs stage");
assert!(
err_arm_end < sweep_manifest_orphans,
"orphan sweeping must be reachable after the docs-stage `Err` arm completes"
);
assert!(
err_arm_end < format_generated,
"formatting must be reachable after the docs-stage `Err` arm completes"
);
assert!(
err_arm_end < finalize_hashes_sweeping,
"hash stamping must be reachable after the docs-stage `Err` arm completes"
);
assert!(
sweep_manifest_orphans < finalize_hashes_sweeping,
"sweep_manifest_orphans must still run before finalize_hashes_sweeping"
);
}
#[test]
fn all_propagates_the_deferred_docs_error_only_after_hook_installation() {
let source = include_str!("all_commands.rs");
let install_hooks = source
.find("pipeline::install_poly_hooks(&base_dir);")
.expect("hook installation stage");
let propagate = source
.find("if let Some(error) = docs_stage_error {")
.expect("the deferred docs error must be propagated once, after the loop");
assert!(
install_hooks < propagate,
"the deferred docs-stage error must be returned only after hook installation (and every \
other must-always-run step) has completed for every crate, not before"
);
let tail = &source[propagate..];
assert!(
tail.contains("stage_failures.record(\"docs/snippet validation\", error);"),
"the deferred error must be handed to `stage_failures` as-is -- it already carries \
whatever `.context(...)` the `Err` arm applied (the refusal-count wrapping), so this \
must not rebuild or discard it before recording it: {tail}"
);
assert!(
!source[..propagate].contains("return Err(error.context"),
"the docs-stage `Err` arm must not return directly -- `.context(...)` is applied while \
building the deferred `error` binding, not at a `return` site"
);
}
#[cfg(test)]
#[path = "all_commands_e2e_defer_tests.rs"]
mod e2e_defer_tests;
use crate::test_support::CwdGuard as E2eDeferCwdGuard;
const LANG_MANIFEST_FIXTURE_SOURCE: &str = "pub fn greet(name: String) -> String {\n name\n}\n";
const LANG_MANIFEST_FIXTURE_CARGO_TOML: &str =
"[package]\nname = \"test-lib\"\nversion = \"0.1.0\"\nedition = \"2024\"\n";
const LANG_MANIFEST_FIXTURE_ALEF_TOML: &str = r#"
[workspace]
languages = ["python"]
[[crates]]
name = "test-lib"
sources = ["src/lib.rs"]
version_from = "Cargo.toml"
[crates.python]
module_name = "test_lib"
[crates.python.stubs]
output = "packages/python/test_lib"
"#;
fn write_lang_manifest_fixture_workspace(root: &std::path::Path) {
std::fs::create_dir_all(root.join("src")).expect("create fixture src directory");
std::fs::write(root.join("src/lib.rs"), LANG_MANIFEST_FIXTURE_SOURCE).expect("write fixture source");
std::fs::write(root.join("Cargo.toml"), LANG_MANIFEST_FIXTURE_CARGO_TOML).expect("write fixture Cargo.toml");
std::fs::write(root.join("alef.toml"), LANG_MANIFEST_FIXTURE_ALEF_TOML).expect("write fixture alef.toml");
}
fn lang_manifest_all_command() -> Commands {
Commands::All {
clean: false,
clobber_create_once_seeds: false,
strict: false,
skip_frb: false,
skip_snippet_validation: false,
}
}
#[test]
fn all_writes_the_full_cross_phase_union_into_the_language_manifest() {
let temp = tempfile::tempdir().expect("tempdir");
let root = temp.path().canonicalize().unwrap_or_else(|_| temp.path().to_path_buf());
write_lang_manifest_fixture_workspace(&root);
let _cwd = E2eDeferCwdGuard::enter(&root);
let context = DispatchContext {
config_path: root.join("alef.toml"),
crate_filter: Vec::new(),
};
handle(lang_manifest_all_command(), &context).expect("all must succeed against a plain python fixture");
let mut manifest = cache::read_lang_manifest("test-lib", "python");
manifest.sort();
let mut expected = vec![
root.join("crates/test-lib-py/src/lib.rs"),
root.join("packages/python/test_lib/test_lib.pyi"),
root.join("packages/python/test_lib/options.py"),
root.join("packages/python/test_lib/api.py"),
root.join("packages/python/test_lib/exceptions.py"),
root.join("packages/python/test_lib/__init__.py"),
];
expected.sort();
assert_eq!(
manifest, expected,
"python.manifest must hold the union of every phase's alef-marked output -- bindings, \
stubs, and public API -- not just generate_bindings' own single file. Got: {manifest:?}"
);
}
#[test]
fn all_a_cache_hit_run_does_not_delete_its_own_manifested_binding_output() {
let temp = tempfile::tempdir().expect("tempdir");
let root = temp.path().canonicalize().unwrap_or_else(|_| temp.path().to_path_buf());
write_lang_manifest_fixture_workspace(&root);
let _cwd = E2eDeferCwdGuard::enter(&root);
let context = DispatchContext {
config_path: root.join("alef.toml"),
crate_filter: Vec::new(),
};
handle(lang_manifest_all_command(), &context).expect("first `all` run must succeed against a plain fixture");
let mut expected = vec![
root.join("crates/test-lib-py/src/lib.rs"),
root.join("packages/python/test_lib/test_lib.pyi"),
root.join("packages/python/test_lib/options.py"),
root.join("packages/python/test_lib/api.py"),
root.join("packages/python/test_lib/exceptions.py"),
root.join("packages/python/test_lib/__init__.py"),
];
expected.sort();
let mut manifest_after_first_run = cache::read_lang_manifest("test-lib", "python");
manifest_after_first_run.sort();
assert_eq!(
manifest_after_first_run, expected,
"the first (real generation) run must record the full cross-phase union"
);
let binding_source = root.join("crates/test-lib-py/src/lib.rs");
assert!(
binding_source.exists(),
"the binding source the first run generated must exist before the second run"
);
handle(lang_manifest_all_command(), &context)
.expect("second `all` run over an unchanged fixture must also succeed");
assert!(
binding_source.exists(),
"a cache-hit run must not delete a manifested binding output as a false orphan -- \
the file existed and was still named by python.manifest, so nothing about this run \
justified removing it"
);
let mut manifest_after_second_run = cache::read_lang_manifest("test-lib", "python");
manifest_after_second_run.sort();
assert_eq!(
manifest_after_second_run, expected,
"a cache-hit run must leave the existing manifest intact -- only a real generation \
may rewrite it"
);
}
#[test]
#[tracing_test::traced_test]
fn a_missing_formatter_executable_is_not_reported_as_a_publish_deferral() {
let dir = tempfile::tempdir().expect("tempdir");
let out = dir.path().join("e2e-out");
std::fs::create_dir_all(out.join("php")).expect("create php output dir");
let script = out.join("php").join("run_tests.php");
std::fs::write(&script, "<?php\n").expect("write php file");
let mut e2e_config = crate::core::config::e2e::E2eConfig {
output: out.to_string_lossy().into_owned(),
..Default::default()
};
e2e_config.format.insert(
"php".to_owned(),
"(cd {dir} && ../../vendor/bin/alef-absent-php-cs-fixer fix .)".to_owned(),
);
let files = vec![GeneratedFile {
path: script,
content: "<?php\n".to_owned(),
generated_header: false,
}];
let deferred = crate::e2e::format::run_formatters(&files, &e2e_config, false)
.expect("a missing formatter executable must not abort the run");
assert_eq!(deferred.len(), 1, "the skip must be recorded, got: {deferred:?}");
report_deferred_formatting("sample-crate", &deferred);
assert!(
!logs_contain("deferred until the pinned version is published"),
"a missing formatter executable must not be reported as waiting on a publish"
);
assert!(
logs_contain("is NOT formatted"),
"the heading must say the output was left unformatted"
);
assert!(
logs_contain("executable is not installed"),
"the report must still carry the entry's own reason"
);
}
#[test]
fn all_records_the_full_cross_phase_union_into_the_binding_ownership_stage_manifest() {
let temp = tempfile::tempdir().expect("tempdir");
let root = temp.path().canonicalize().unwrap_or_else(|_| temp.path().to_path_buf());
write_lang_manifest_fixture_workspace(&root);
let _cwd = E2eDeferCwdGuard::enter(&root);
let context = DispatchContext {
config_path: root.join("alef.toml"),
crate_filter: Vec::new(),
};
handle(lang_manifest_all_command(), &context).expect("all must succeed against a plain python fixture");
let mut ownership = cache::read_stage_paths("test-lib", "all-bindings-python-ownership");
ownership.sort();
let mut expected = vec![
root.join("crates/test-lib-py/src/lib.rs"),
root.join("packages/python/test_lib/test_lib.pyi"),
root.join("packages/python/test_lib/options.py"),
root.join("packages/python/test_lib/api.py"),
root.join("packages/python/test_lib/exceptions.py"),
root.join("packages/python/test_lib/__init__.py"),
];
expected.sort();
assert_eq!(
ownership, expected,
"the all-bindings-python-ownership stage manifest must hold the union of every phase's \
output -- bindings, stubs, and public API -- not just generate_bindings' single \
crates/test-lib-py/src/lib.rs entry. Got: {ownership:?}"
);
}