Skip to main content

alef_e2e/
lib.rs

1//! Fixture-driven e2e test generation for alef.
2//!
3//! This crate generates complete, runnable e2e test projects for all supported
4//! languages from JSON fixture files. Each project is self-contained with
5//! build files, test files, and local package references.
6
7pub mod codegen;
8pub mod config;
9pub mod escape;
10pub mod field_access;
11pub mod fixture;
12pub mod format;
13pub mod scaffold;
14pub mod template_env;
15pub mod validate;
16
17use alef_core::backend::GeneratedFile;
18use alef_core::config::e2e::DependencyMode;
19use alef_core::config::{Language, ResolvedCrateConfig};
20use anyhow::{Context, Result};
21use config::E2eConfig;
22use fixture::{group_fixtures, load_fixtures};
23use std::path::Path;
24use tracing::{info, warn};
25use validate::Severity;
26
27/// Map the top-level `[languages]` list (the scaffolded bindings) to the
28/// e2e generator names registered in [`codegen::all_generators`].
29///
30/// `Language::Ffi` maps to the `c` generator (the FFI binding's e2e harness
31/// is the C test runner). `Language::Rust` is always appended because rust is
32/// the source language and the rust e2e suite exercises the core crate.
33///
34/// Generators that don't have a corresponding `Language` variant (e.g.
35/// `brew`) are intentionally excluded — they require an explicit opt-in via
36/// `[e2e].languages` in alef.toml.
37pub fn default_e2e_languages(scaffolded: &[Language]) -> Vec<String> {
38    let mut names: Vec<String> = scaffolded
39        .iter()
40        .map(|l| match l {
41            Language::Ffi => "c".to_string(),
42            other => other.to_string(),
43        })
44        .collect();
45    if !names.iter().any(|n| n == "rust") {
46        names.push("rust".to_string());
47    }
48    names
49}
50
51/// Generate e2e test projects from fixtures.
52///
53/// Returns the list of generated files. The caller is responsible for writing
54/// them to disk.
55///
56/// `type_defs` is the IR type registry for the source crate. Pass
57/// `&api.types` from the extracted [`alef_core::ir::ApiSurface`]. It is
58/// forwarded to generators that need to introspect struct field types (e.g.
59/// the TypeScript/WASM backend uses it to auto-derive `nested_types` for
60/// wasm-bindgen class wrapping). Pass an empty slice when the registry is not
61/// available; generators will fall back to explicit call-override mappings.
62pub fn generate_e2e(
63    config: &ResolvedCrateConfig,
64    e2e_config: &E2eConfig,
65    languages: Option<&[String]>,
66    type_defs: &[alef_core::ir::TypeDef],
67) -> Result<Vec<GeneratedFile>> {
68    let fixtures_dir = Path::new(&e2e_config.fixtures);
69    let fixtures = load_fixtures(fixtures_dir)
70        .with_context(|| format!("failed to load fixtures from {}", fixtures_dir.display()))?;
71
72    info!("Loaded {} fixture(s) from {}", fixtures.len(), e2e_config.fixtures);
73
74    // Resolution order for which language generators to run:
75    //   1. Explicit `--lang` filter from the CLI (highest priority).
76    //   2. `[e2e].languages` from alef.toml when set.
77    //   3. The top-level `[languages]` list mapped to e2e generator names —
78    //      so e2e tests are only generated for actually scaffolded bindings,
79    //      never for backends the consumer hasn't opted into.
80    //
81    // The legacy `all_generators()` fallback is removed; emitting tests for
82    // languages without a matching binding produces broken e2e dirs that
83    // cannot compile.
84    let resolved_languages: Vec<String> = if let Some(langs) = languages {
85        langs.to_vec()
86    } else if !e2e_config.languages.is_empty() {
87        e2e_config.languages.clone()
88    } else {
89        default_e2e_languages(&config.languages)
90    };
91
92    // Run semantic validation against the resolved language set so the
93    // empty-category check warns about the same languages we're about to
94    // generate for.
95    let diagnostics = validate::validate_fixtures_semantic(&fixtures, e2e_config, &resolved_languages);
96    for diag in &diagnostics {
97        match diag.severity {
98            Severity::Error => warn!("{}: {}", diag.file, diag.message),
99            Severity::Warning => warn!("{}: {}", diag.file, diag.message),
100        }
101    }
102
103    let all_groups = group_fixtures(&fixtures);
104
105    // Drop categories that are explicitly excluded from cross-language e2e
106    // codegen. These fixtures stay on disk for Rust integration tests but
107    // never reach binding generators.
108    let all_groups: Vec<_> = if e2e_config.exclude_categories.is_empty() {
109        all_groups
110    } else {
111        all_groups
112            .into_iter()
113            .filter(|g| !e2e_config.exclude_categories.contains(&g.category))
114            .collect()
115    };
116
117    // In registry mode with a non-empty category filter, keep only the listed
118    // categories so the generated test apps contain a curated subset.
119    let groups: Vec<_> =
120        if e2e_config.dep_mode == DependencyMode::Registry && !e2e_config.registry.categories.is_empty() {
121            let allowed = &e2e_config.registry.categories;
122            all_groups
123                .into_iter()
124                .filter(|g| allowed.iter().any(|c| c == &g.category))
125                .collect()
126        } else {
127            all_groups
128        };
129
130    let generators = codegen::generators_for(&resolved_languages);
131
132    let mut all_files = Vec::new();
133    for generator in &generators {
134        let files = generator.generate(&groups, e2e_config, config, type_defs)?;
135        info!("  [{}] generated {} file(s)", generator.language_name(), files.len());
136        all_files.extend(files);
137    }
138
139    Ok(all_files)
140}