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 validate;
15
16use alef_core::backend::GeneratedFile;
17use alef_core::config::e2e::DependencyMode;
18use alef_core::config::{AlefConfig, Language};
19use anyhow::{Context, Result};
20use config::E2eConfig;
21use fixture::{group_fixtures, load_fixtures};
22use std::path::Path;
23use tracing::{info, warn};
24use validate::Severity;
25
26/// Map the top-level `[languages]` list (the scaffolded bindings) to the
27/// e2e generator names registered in [`codegen::all_generators`].
28///
29/// `Language::Ffi` maps to the `c` generator (the FFI binding's e2e harness
30/// is the C test runner). `Language::Rust` is always appended because rust is
31/// the source language and the rust e2e suite exercises the core crate.
32///
33/// Generators that don't have a corresponding `Language` variant (e.g.
34/// `brew`) are intentionally excluded — they require an explicit opt-in via
35/// `[e2e].languages` in alef.toml.
36pub fn default_e2e_languages(scaffolded: &[Language]) -> Vec<String> {
37    let mut names: Vec<String> = scaffolded
38        .iter()
39        .map(|l| match l {
40            Language::Ffi => "c".to_string(),
41            other => other.to_string(),
42        })
43        .collect();
44    if !names.iter().any(|n| n == "rust") {
45        names.push("rust".to_string());
46    }
47    names
48}
49
50/// Generate e2e test projects from fixtures.
51///
52/// Returns the list of generated files. The caller is responsible for writing
53/// them to disk.
54pub fn generate_e2e(
55    alef_config: &AlefConfig,
56    e2e_config: &E2eConfig,
57    languages: Option<&[String]>,
58) -> Result<Vec<GeneratedFile>> {
59    let fixtures_dir = Path::new(&e2e_config.fixtures);
60    let fixtures = load_fixtures(fixtures_dir)
61        .with_context(|| format!("failed to load fixtures from {}", fixtures_dir.display()))?;
62
63    info!("Loaded {} fixture(s) from {}", fixtures.len(), e2e_config.fixtures);
64
65    // Resolution order for which language generators to run:
66    //   1. Explicit `--lang` filter from the CLI (highest priority).
67    //   2. `[e2e].languages` from alef.toml when set.
68    //   3. The top-level `[languages]` list mapped to e2e generator names —
69    //      so e2e tests are only generated for actually scaffolded bindings,
70    //      never for backends the consumer hasn't opted into.
71    //
72    // The legacy `all_generators()` fallback is removed; emitting tests for
73    // languages without a matching binding produces broken e2e dirs that
74    // cannot compile.
75    let resolved_languages: Vec<String> = if let Some(langs) = languages {
76        langs.to_vec()
77    } else if !e2e_config.languages.is_empty() {
78        e2e_config.languages.clone()
79    } else {
80        default_e2e_languages(&alef_config.languages)
81    };
82
83    // Run semantic validation against the resolved language set so the
84    // empty-category check warns about the same languages we're about to
85    // generate for.
86    let diagnostics = validate::validate_fixtures_semantic(&fixtures, e2e_config, &resolved_languages);
87    for diag in &diagnostics {
88        match diag.severity {
89            Severity::Error => warn!("{}: {}", diag.file, diag.message),
90            Severity::Warning => warn!("{}: {}", diag.file, diag.message),
91        }
92    }
93
94    let all_groups = group_fixtures(&fixtures);
95
96    // In registry mode with a non-empty category filter, keep only the listed
97    // categories so the generated test apps contain a curated subset.
98    let groups: Vec<_> =
99        if e2e_config.dep_mode == DependencyMode::Registry && !e2e_config.registry.categories.is_empty() {
100            let allowed = &e2e_config.registry.categories;
101            all_groups
102                .into_iter()
103                .filter(|g| allowed.iter().any(|c| c == &g.category))
104                .collect()
105        } else {
106            all_groups
107        };
108
109    let generators = codegen::generators_for(&resolved_languages);
110
111    let mut all_files = Vec::new();
112    for generator in &generators {
113        let files = generator.generate(&groups, e2e_config, alef_config)?;
114        info!("  [{}] generated {} file(s)", generator.language_name(), files.len());
115        all_files.extend(files);
116    }
117
118    Ok(all_files)
119}