alef-e2e 0.13.10

Fixture-driven e2e test generator for alef
Documentation
//! Post-generation formatter support for e2e test projects.
//!
//! Reads formatter commands from `E2eConfig.format` and runs them for each
//! language that had files generated. The `{dir}` placeholder in the command
//! is replaced with the actual output directory.

use crate::config::E2eConfig;
use alef_core::backend::GeneratedFile;
use std::collections::HashSet;
use std::path::Path;
use tracing::warn;

/// Default per-language formatter commands for e2e directories.
///
/// These are run automatically when the language's directory was generated and
/// no custom override is present in `e2e_config.format`. The `{dir}` placeholder
/// is replaced with the actual output directory path before execution.
///
/// * `rust` — `(cd {dir} && cargo fmt --all)` formats the standalone e2e crate
///   from inside its own directory. `cargo fmt --manifest-path` is *not* a
///   global cargo flag (it's an unstable cargo-fmt-only flag in nightly), so
///   running cargo fmt from the e2e crate's own directory is the portable way
///   to format a non-workspace-member crate.
/// * `python` — `ruff format {dir}` normalises whitespace/newlines in the
///   generated test files so prek's ruff hook is a no-op.
fn default_formatter(lang: &str) -> Option<&'static str> {
    match lang {
        "rust" => Some("(cd {dir} && cargo fmt --all)"),
        "python" => Some("ruff format {dir}"),
        _ => None,
    }
}

/// Run per-language formatters for all languages that had files generated.
///
/// For each language present in `files`, picks the command from
/// `e2e_config.format[lang]` when available, then falls back to
/// [`default_formatter`] for languages that have a built-in default (rust,
/// python). The `{dir}` placeholder is replaced with `{output}/{lang}`.
/// Failures are logged as warnings and do not abort the process.
pub fn run_formatters(files: &[GeneratedFile], e2e_config: &E2eConfig) {
    // Collect the set of languages that had files generated by inspecting
    // file paths. E2e files are written to `{output}/{lang}/...`, so the
    // first path component after the output prefix is the language name.
    let output_prefix = Path::new(e2e_config.effective_output());
    let languages: HashSet<String> = files
        .iter()
        .filter_map(|f| {
            let remainder = f.path.strip_prefix(output_prefix).ok()?;
            let first = remainder.components().next()?;
            Some(first.as_os_str().to_string_lossy().into_owned())
        })
        .collect();

    for lang in &languages {
        // User override takes precedence; then built-in default.
        let cmd_template: &str = if let Some(custom) = e2e_config.format.get(lang.as_str()) {
            custom.as_str()
        } else if let Some(builtin) = default_formatter(lang.as_str()) {
            builtin
        } else {
            continue;
        };

        let dir = format!("{}/{}", e2e_config.effective_output(), lang);
        let cmd = cmd_template.replace("{dir}", &dir);

        eprintln!("  Formatting {lang}: {cmd}");
        let status = std::process::Command::new("sh").args(["-c", &cmd]).status();

        match status {
            Ok(s) if s.success() => {}
            Ok(s) => {
                warn!("Formatter for {lang} exited with {s}: {cmd}");
            }
            Err(e) => {
                warn!("Failed to run formatter for {lang}: {e}");
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_default_formatter_rust_uses_cd_into_dir() {
        let cmd = default_formatter("rust").expect("rust must have a default formatter");
        assert!(
            cmd.contains("cd {dir}") && cmd.contains("cargo fmt"),
            "rust formatter must cd into {{dir}} before invoking cargo fmt so it works on \
             standalone (non-workspace-member) e2e crates: {cmd}"
        );
        assert!(
            !cmd.contains("--manifest-path"),
            "rust formatter must not use --manifest-path; cargo-fmt does not accept it as a \
             global flag: {cmd}"
        );
        assert!(
            cmd.contains("{dir}"),
            "rust formatter must include {{dir}} placeholder: {cmd}"
        );
    }

    #[test]
    fn test_default_formatter_python_uses_ruff_format() {
        let cmd = default_formatter("python").expect("python must have a default formatter");
        assert!(cmd.contains("ruff"), "python formatter must use ruff: {cmd}");
        assert!(cmd.contains("format"), "python formatter must run ruff format: {cmd}");
        assert!(
            cmd.contains("{dir}"),
            "python formatter must include {{dir}} placeholder: {cmd}"
        );
    }

    #[test]
    fn test_default_formatter_unknown_lang_returns_none() {
        assert!(default_formatter("gleam").is_none());
        assert!(default_formatter("zig").is_none());
        assert!(default_formatter("java").is_none());
    }
}