use crate::core::backend::GeneratedFile;
use crate::core::config::ResolvedCrateConfig;
use crate::e2e::config::E2eConfig;
use crate::e2e::escape::sanitize_filename;
use crate::e2e::fixture::{Fixture, FixtureGroup};
use anyhow::Result;
use std::collections::HashMap;
use std::path::PathBuf;
use super::E2eCodegen;
pub struct ElixirCodegen;
impl E2eCodegen for ElixirCodegen {
fn generate(
&self,
groups: &[FixtureGroup],
e2e_config: &E2eConfig,
config: &ResolvedCrateConfig,
type_defs: &[crate::core::ir::TypeDef],
enums: &[crate::core::ir::EnumDef],
) -> Result<Vec<GeneratedFile>> {
let lang = self.language_name();
let output_base = PathBuf::from(e2e_config.effective_output()).join(lang);
let mut files = Vec::new();
let call = &e2e_config.call;
let overrides = call.overrides.get(lang);
let raw_module = overrides
.and_then(|o| o.module.as_ref())
.cloned()
.unwrap_or_else(|| call.module.clone());
let module_path = if raw_module.contains('.') || raw_module.chars().next().is_some_and(|c| c.is_uppercase()) {
raw_module.clone()
} else {
values::elixir_module_name(&raw_module)
};
let base_function_name = overrides
.and_then(|o| o.function.as_ref())
.cloned()
.unwrap_or_else(|| call.function.clone());
let function_name =
if call.r#async && !base_function_name.ends_with("_async") && !base_function_name.ends_with("_stream") {
format!("{base_function_name}_async")
} else {
base_function_name
};
let options_type = overrides.and_then(|o| o.options_type.clone());
let options_default_fn = overrides.and_then(|o| o.options_via.clone());
let empty_enum_fields = HashMap::new();
let enum_fields = overrides.map(|o| &o.enum_fields).unwrap_or(&empty_enum_fields);
let handle_struct_type = overrides.and_then(|o| o.handle_struct_type.clone());
let empty_atom_fields = std::collections::HashSet::new();
let handle_atom_list_fields = overrides
.map(|o| &o.handle_atom_list_fields)
.unwrap_or(&empty_atom_fields);
let result_var = &call.result_var;
let has_http_tests = groups.iter().any(|g| g.fixtures.iter().any(|f| f.is_http_test()));
let has_nif_tests = groups.iter().any(|g| g.fixtures.iter().any(|f| !f.is_http_test()));
let has_mock_server_tests = groups.iter().any(|g| {
g.fixtures.iter().any(|f| {
if f.needs_mock_server() {
return true;
}
let cc = e2e_config.resolve_call_for_fixture(
f.call.as_deref(),
&f.id,
&f.resolved_category(),
&f.tags,
&f.input,
);
let elixir_override = cc
.overrides
.get("elixir")
.or_else(|| e2e_config.call.overrides.get("elixir"));
elixir_override.and_then(|o| o.client_factory.as_deref()).is_some()
})
});
let pkg_ref = e2e_config.resolve_package(lang);
let pkg_dep_ref = if has_nif_tests {
match e2e_config.dep_mode {
crate::e2e::config::DependencyMode::Local => pkg_ref
.as_ref()
.and_then(|p| p.path.as_deref())
.unwrap_or("../../packages/elixir")
.to_string(),
crate::e2e::config::DependencyMode::Registry => pkg_ref
.as_ref()
.and_then(|p| p.version.clone())
.or_else(|| config.resolved_version())
.unwrap_or_else(|| "0.1.0".to_string()),
}
} else {
String::new()
};
let pkg_atom = config.elixir_app_name();
let has_http_fixtures = groups.iter().flat_map(|g| g.fixtures.iter()).any(|f| f.http.is_some());
let uses_harness = has_http_fixtures && !e2e_config.harness.imports.is_empty();
files.push(GeneratedFile {
path: output_base.join("mix.exs"),
content: project::render_mix_exs(
&pkg_atom,
&pkg_dep_ref,
e2e_config.dep_mode,
has_http_tests,
has_mock_server_tests,
has_nif_tests,
uses_harness,
),
generated_header: false,
});
files.push(GeneratedFile {
path: output_base.join("lib").join("e2e_elixir.ex"),
content: "defmodule E2eElixir do\n @moduledoc false\nend\n".to_string(),
generated_header: false,
});
if uses_harness {
files.push(GeneratedFile {
path: output_base.join("app_harness.exs"),
content: project::render_app_harness(e2e_config, groups, config),
generated_header: true,
});
}
files.push(GeneratedFile {
path: output_base.join("test").join("test_helper.exs"),
content: project::render_test_helper(has_http_tests || has_mock_server_tests, uses_harness, e2e_config),
generated_header: false,
});
for group in groups {
let active: Vec<&Fixture> = group
.fixtures
.iter()
.filter(|f| super::should_include_fixture(f, lang, e2e_config))
.collect();
if active.is_empty() {
continue;
}
let filename = format!("{}_test.exs", sanitize_filename(&group.category));
let content = test_file::render_test_file(
&group.category,
&active,
e2e_config,
&module_path,
&function_name,
result_var,
&e2e_config.call.args,
options_type.as_deref(),
options_default_fn.as_deref(),
enum_fields,
handle_struct_type.as_deref(),
handle_atom_list_fields,
&config.adapters,
enums,
config,
type_defs,
);
files.push(GeneratedFile {
path: output_base.join("test").join(filename),
content,
generated_header: true,
});
}
Ok(files)
}
fn language_name(&self) -> &'static str {
"elixir"
}
}
mod args;
mod assertions;
mod http;
mod project;
mod stubs;
mod test_case;
mod test_file;
mod values;
mod visitor;
pub use stubs::emit_test_backend;
#[cfg(test)]
mod tests;