use super::{
create_once_overwrite, handle, refused_snippet_dir_paths, snippet_validation_needs_build_artifacts,
sync_registry_versions_before_all, warn_if_snippet_validation_needs_build,
};
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("Generating e2e test suites...").expect("e2e stage");
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, `warn_if_snippet_validation_needs_build`'s precondition warning (and its doc \
comment) 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"
);
}
#[test]
fn snippet_validation_needs_build_artifacts_is_true_only_for_toolchain_levels() {
assert!(snippet_validation_needs_build_artifacts(Some("typecheck")));
assert!(snippet_validation_needs_build_artifacts(Some("compile")));
assert!(snippet_validation_needs_build_artifacts(Some("run")));
assert!(
snippet_validation_needs_build_artifacts(Some("Compile")),
"the check must be case-insensitive since config values are user-authored TOML strings"
);
assert!(!snippet_validation_needs_build_artifacts(Some("syntax")));
assert!(!snippet_validation_needs_build_artifacts(None));
assert!(!snippet_validation_needs_build_artifacts(Some("bogus")));
}
fn write_config_with_snippet_validation_level(root: &std::path::Path, validation_level: &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 config = format!(
concat!(
"[workspace]\nlanguages = [\"zig\"]\n\n",
"[workspace.docs.snippets]\nvalidation_level = {:?}\n\n",
"[[crates]]\nname = \"sample-core\"\nsources = []\nversion_from = {:?}\n"
),
validation_level,
cargo_path.to_string_lossy(),
);
std::fs::write(&config_path, config).expect("write alef.toml");
config_path
}
#[test]
fn warn_if_snippet_validation_needs_build_reads_the_merged_validation_level_without_panicking() {
let temp = tempfile::tempdir().expect("tempdir");
let config_path = write_config_with_snippet_validation_level(temp.path(), "compile");
let configs = resolve(&config_path);
let config = configs.into_iter().next().expect("one crate");
let merged_level = config
.docs
.as_ref()
.and_then(|docs| docs.snippets.as_ref())
.and_then(|snippets| snippets.validation_level.as_deref());
assert_eq!(merged_level, Some("compile"));
warn_if_snippet_validation_needs_build(&config);
}
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(&files_to_format, resolved_cfg, &base_dir, None)")
.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("return Err(error);"),
"the deferred error must be returned as-is -- it already carries whatever `.context(...)` \
the `Err` arm applied (the refusal-count wrapping), so this must not rebuild or discard it"
);
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"
);
}
use crate::test_support::CwdGuard as E2eDeferCwdGuard;
const E2E_DEFER_FIXTURE_SOURCE: &str = r#"
pub struct Metadata {
pub document_title: String,
}
pub struct CompletionResult {
pub id: String,
pub metadata: Metadata,
}
pub fn complete(prompt: String) -> Result<CompletionResult, String> {
let _ = prompt;
Err("unimplemented".to_string())
}
"#;
const E2E_DEFER_FIXTURE_CARGO_TOML: &str = "[package]\nname = \"deferlib\"\nversion = \"0.1.0\"\nedition = \"2024\"\n";
const E2E_DEFER_FIXTURE_ALEF_TOML: &str = r#"
[workspace]
languages = ["python"]
[[crates]]
name = "deferlib"
sources = ["src/lib.rs"]
version_from = "Cargo.toml"
[crates.e2e]
fixtures = "fixtures"
output = "e2e"
languages = ["c", "rust"]
[crates.e2e.call]
function = "complete"
module = "deferlib"
result_var = "result"
[[crates.e2e.call.args]]
name = "prompt"
field = "input.prompt"
type = "string"
"#;
fn e2e_defer_fixture_json(field: &str) -> String {
format!(
"{{\n \"id\": \"complete_basic\",\n \"description\": \"a completion asserting a field on \
the nested Metadata type\",\n \"category\": \"smoke\",\n \"tags\": [\"smoke\"],\n \
\"call\": \"_default\",\n \"input\": {{ \"prompt\": \"hello\" }},\n \"assertions\": [\n \
{{ \"type\": \"not_error\" }},\n {{ \"type\": \"equals\", \"field\": \"{field}\", \"value\": \"irrelevant\" }}\n \
]\n}}\n"
)
}
fn write_e2e_defer_fixture_workspace(root: &std::path::Path, field: &str) {
std::fs::create_dir_all(root.join("src")).expect("create fixture src directory");
std::fs::create_dir_all(root.join("fixtures")).expect("create fixture fixtures directory");
std::fs::write(root.join("src/lib.rs"), E2E_DEFER_FIXTURE_SOURCE).expect("write fixture source");
std::fs::write(root.join("Cargo.toml"), E2E_DEFER_FIXTURE_CARGO_TOML).expect("write fixture Cargo.toml");
std::fs::write(root.join("fixtures/complete_basic.json"), e2e_defer_fixture_json(field))
.expect("write fixture json");
std::fs::write(root.join("alef.toml"), E2E_DEFER_FIXTURE_ALEF_TOML).expect("write fixture alef.toml");
}
fn e2e_defer_all_command() -> Commands {
Commands::All {
clean: false,
clobber_create_once_seeds: false,
strict: false,
skip_frb: false,
}
}
fn expect_e2e_defer_err(result: anyhow::Result<Option<Commands>>, message: &str) -> anyhow::Error {
match result {
Err(error) => error,
Ok(_) => panic!("{message}"),
}
}
#[test]
fn all_defers_an_e2e_generator_failure_so_sibling_backends_still_write_and_the_run_fails() {
let temp = tempfile::tempdir().expect("tempdir");
let root = temp.path().canonicalize().unwrap_or_else(|_| temp.path().to_path_buf());
write_e2e_defer_fixture_workspace(&root, "metadata.title");
let _cwd = E2eDeferCwdGuard::enter(&root);
let context = DispatchContext {
config_path: root.join("alef.toml"),
crate_filter: Vec::new(),
};
let error = expect_e2e_defer_err(
handle(e2e_defer_all_command(), &context),
"a C-backend e2e codegen failure must still fail the run -- writing sibling files must \
not silently turn this into a healthy exit code",
);
let message = format!("{error:#}");
assert!(
message.contains("e2e codegen failed") && message.contains("[c]"),
"the propagated error must be the deferred e2e generator failure naming the `c` backend, \
not something else: {message}"
);
assert!(
message.contains("Metadata") && message.contains("title"),
"the failure must carry `ensure_leaf_field_exists`'s own diagnostic verbatim: {message}"
);
let rust_cargo_toml = root.join("e2e").join("rust").join("Cargo.toml");
assert!(
rust_cargo_toml.is_file(),
"the `rust` sibling backend must still have written its e2e suite even though the `c` \
backend's codegen failed: {} is missing",
rust_cargo_toml.display()
);
let c_makefile = root.join("e2e").join("c").join("Makefile");
assert!(
!c_makefile.is_file(),
"the failing `c` backend itself must not have produced output -- `run_generators` \
treats a backend's `Err` as zero files for that backend, not a partial write: {}",
c_makefile.display()
);
}
#[test]
fn all_gates_e2e_stage_hash_and_orphan_sweep_on_a_deferred_generator_failure() {
let temp = tempfile::tempdir().expect("tempdir");
let root = temp.path().canonicalize().unwrap_or_else(|_| temp.path().to_path_buf());
write_e2e_defer_fixture_workspace(&root, "metadata.document_title");
let _cwd = E2eDeferCwdGuard::enter(&root);
let context = DispatchContext {
config_path: root.join("alef.toml"),
crate_filter: Vec::new(),
};
handle(e2e_defer_all_command(), &context).expect("the baseline run with a valid field path must succeed");
let c_makefile = root.join("e2e").join("c").join("Makefile");
assert!(
c_makefile.is_file(),
"the baseline run must have produced `c` e2e output at {}",
c_makefile.display()
);
std::fs::write(
root.join("fixtures/complete_basic.json"),
e2e_defer_fixture_json("metadata.title"),
)
.expect("rewrite fixture with a field Metadata does not have");
expect_e2e_defer_err(
handle(e2e_defer_all_command(), &context),
"a run whose fixture trips ensure_leaf_field_exists must still fail",
);
assert!(
c_makefile.is_file(),
"the previously-good `c` output must survive a run whose `c` backend failed -- \
sweep_manifest_orphans must not run on a deferred generator failure, or it deletes the \
last known-good backend output: {}",
c_makefile.display()
);
expect_e2e_defer_err(
handle(e2e_defer_all_command(), &context),
"a repeat run over the same broken fixture must still fail -- a stage hash written on \
the previous failed attempt would silently cache the failure away",
);
}
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,
}
}
#[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:?}"
);
}