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.
55pub fn generate_e2e(
56    config: &ResolvedCrateConfig,
57    e2e_config: &E2eConfig,
58    languages: Option<&[String]>,
59) -> Result<Vec<GeneratedFile>> {
60    let fixtures_dir = Path::new(&e2e_config.fixtures);
61    let fixtures = load_fixtures(fixtures_dir)
62        .with_context(|| format!("failed to load fixtures from {}", fixtures_dir.display()))?;
63
64    info!("Loaded {} fixture(s) from {}", fixtures.len(), e2e_config.fixtures);
65
66    // Resolution order for which language generators to run:
67    //   1. Explicit `--lang` filter from the CLI (highest priority).
68    //   2. `[e2e].languages` from alef.toml when set.
69    //   3. The top-level `[languages]` list mapped to e2e generator names —
70    //      so e2e tests are only generated for actually scaffolded bindings,
71    //      never for backends the consumer hasn't opted into.
72    //
73    // The legacy `all_generators()` fallback is removed; emitting tests for
74    // languages without a matching binding produces broken e2e dirs that
75    // cannot compile.
76    let resolved_languages: Vec<String> = if let Some(langs) = languages {
77        langs.to_vec()
78    } else if !e2e_config.languages.is_empty() {
79        e2e_config.languages.clone()
80    } else {
81        default_e2e_languages(&config.languages)
82    };
83
84    // Run semantic validation against the resolved language set so the
85    // empty-category check warns about the same languages we're about to
86    // generate for.
87    let diagnostics = validate::validate_fixtures_semantic(&fixtures, e2e_config, &resolved_languages);
88    for diag in &diagnostics {
89        match diag.severity {
90            Severity::Error => warn!("{}: {}", diag.file, diag.message),
91            Severity::Warning => warn!("{}: {}", diag.file, diag.message),
92        }
93    }
94
95    let all_groups = group_fixtures(&fixtures);
96
97    // Drop categories that are explicitly excluded from cross-language e2e
98    // codegen. These fixtures stay on disk for Rust integration tests but
99    // never reach binding generators.
100    let all_groups: Vec<_> = if e2e_config.exclude_categories.is_empty() {
101        all_groups
102    } else {
103        all_groups
104            .into_iter()
105            .filter(|g| !e2e_config.exclude_categories.contains(&g.category))
106            .collect()
107    };
108
109    // In registry mode with a non-empty category filter, keep only the listed
110    // categories so the generated test apps contain a curated subset.
111    let groups: Vec<_> =
112        if e2e_config.dep_mode == DependencyMode::Registry && !e2e_config.registry.categories.is_empty() {
113            let allowed = &e2e_config.registry.categories;
114            all_groups
115                .into_iter()
116                .filter(|g| allowed.iter().any(|c| c == &g.category))
117                .collect()
118        } else {
119            all_groups
120        };
121
122    let generators = codegen::generators_for(&resolved_languages);
123
124    let mut all_files = Vec::new();
125    for generator in &generators {
126        let files = generator.generate(&groups, e2e_config, config)?;
127        info!("  [{}] generated {} file(s)", generator.language_name(), files.len());
128        all_files.extend(files);
129    }
130
131    Ok(all_files)
132}