pub mod assertions;
pub mod cargo_config;
pub mod cargo_toml;
pub mod http;
pub mod mock_server;
pub mod test_file;
mod args;
mod assertion_helpers;
mod assertion_synthetic;
pub use cargo_config::render_cargo_config;
pub use cargo_toml::render_cargo_toml;
pub use mock_server::{render_common_module, render_mock_server_binary, render_mock_server_module};
use crate::core::backend::GeneratedFile;
use crate::core::config::ResolvedCrateConfig;
use anyhow::Result;
use std::path::PathBuf;
use crate::e2e::config::E2eConfig;
use crate::e2e::escape::sanitize_filename;
use crate::e2e::fixture::{Fixture, FixtureGroup};
use super::E2eCodegen;
use test_file::{is_skipped, render_test_file};
pub struct RustE2eCodegen;
impl E2eCodegen for RustE2eCodegen {
fn generate(
&self,
groups: &[FixtureGroup],
e2e_config: &E2eConfig,
config: &ResolvedCrateConfig,
_type_defs: &[crate::core::ir::TypeDef],
_enums: &[crate::core::ir::EnumDef],
_functions: &[crate::core::ir::FunctionDef],
) -> Result<Vec<GeneratedFile>> {
let mut files = Vec::new();
let output_base = PathBuf::from(e2e_config.effective_output()).join("rust");
let crate_name = resolve_crate_name(e2e_config, config);
let crate_path = resolve_crate_path(e2e_config, &crate_name);
let dep_name = crate_name.replace('-', "_");
let all_call_configs = std::iter::once(&e2e_config.call).chain(e2e_config.calls.values());
let needs_serde_json = all_call_configs
.flat_map(|c| c.args.iter())
.any(|a| a.arg_type == "json_object" || a.arg_type == "handle");
let needs_mock_server = groups
.iter()
.flat_map(|g| g.fixtures.iter())
.any(|f| !is_skipped(f, "rust") && f.needs_mock_server());
let needs_http_tests = groups
.iter()
.flat_map(|g| g.fixtures.iter())
.any(|f| !is_skipped(f, "rust") && f.http.is_some());
let needs_tower_http = groups
.iter()
.flat_map(|g| g.fixtures.iter())
.filter(|f| !is_skipped(f, "rust"))
.filter_map(|f| f.http.as_ref())
.filter_map(|h| h.handler.middleware.as_ref())
.any(|m| m.cors.is_some() || m.static_files.is_some());
let any_async_call = std::iter::once(&e2e_config.call)
.chain(e2e_config.calls.values())
.any(|c| c.r#async);
let needs_tokio = needs_mock_server || needs_http_tests || any_async_call;
let all_call_args_for_anyhow = std::iter::once(&e2e_config.call)
.chain(e2e_config.calls.values())
.flat_map(|c| c.args.iter())
.any(|a| a.arg_type == "test_backend");
let any_fixture_test_backend = groups
.iter()
.flat_map(|g| g.fixtures.iter())
.filter(|f| !is_skipped(f, "rust"))
.any(|f| f.args.iter().any(|a| a.arg_type == "test_backend"));
let needs_anyhow = all_call_args_for_anyhow || any_fixture_test_backend;
if let Some(content) = render_cargo_config(&e2e_config.env) {
files.push(GeneratedFile {
path: output_base.join(".cargo").join("config.toml"),
content,
generated_header: false,
});
}
let crate_version = resolve_crate_version(e2e_config).or_else(|| config.resolved_version());
files.push(GeneratedFile {
path: output_base.join("Cargo.toml"),
content: render_cargo_toml(
&crate_name,
&dep_name,
&crate_path,
needs_serde_json,
needs_mock_server,
needs_http_tests,
needs_tokio,
needs_tower_http,
needs_anyhow,
e2e_config.dep_mode,
crate_version.as_deref(),
&config.features,
),
generated_header: true,
});
if needs_mock_server {
files.push(GeneratedFile {
path: output_base.join("tests").join("mock_server.rs"),
content: render_mock_server_module(),
generated_header: true,
});
files.push(GeneratedFile {
path: output_base.join("tests").join("common.rs"),
content: render_common_module(),
generated_header: true,
});
}
if needs_mock_server || needs_http_tests {
files.push(GeneratedFile {
path: output_base.join("src").join("main.rs"),
content: render_mock_server_binary(),
generated_header: true,
});
}
for group in groups {
let fixtures: Vec<&Fixture> = group.fixtures.iter().filter(|f| !is_skipped(f, "rust")).collect();
if fixtures.is_empty() {
continue;
}
let filename = format!("{}_test.rs", sanitize_filename(&group.category));
let content = render_test_file(
&group.category,
&fixtures,
e2e_config,
config,
_type_defs,
&dep_name,
needs_mock_server,
None,
false,
);
files.push(GeneratedFile {
path: output_base.join("tests").join(filename),
content,
generated_header: true,
});
}
Ok(files)
}
fn render_snippet_body(
&self,
fixture: &Fixture,
e2e_config: &E2eConfig,
config: &ResolvedCrateConfig,
type_defs: &[crate::core::ir::TypeDef],
_enums: &[crate::core::ir::EnumDef],
) -> Result<String> {
let dep_name = resolve_crate_name(e2e_config, config).replace('-', "_");
let mut call_fixture = fixture.docs_call_fixture();
let expects_error = fixture
.assertions
.iter()
.any(|assertion| assertion.assertion_type == "error");
call_fixture.assertions.clear();
call_fixture.mock_response = None;
let test_file = render_test_file(
&fixture.resolved_category(),
&[&call_fixture],
e2e_config,
config,
type_defs,
&dep_name,
call_fixture.needs_mock_server(),
fixture.docs_client(),
expects_error,
);
let (imports, body, is_async) = extract_rust_snippet(&test_file)?;
let api_key_var = crate::e2e::fixture::FixtureEnv::api_key_var_or_default(fixture.env.as_ref());
let body = body
.into_iter()
.map(|line| {
line.replace(
"\"test-key\".to_string()",
&format!("std::env::var(\"{api_key_var}\").expect(\"{api_key_var} must be set\")"),
)
})
.collect::<Vec<_>>();
let presentation = super::presentation::resolve(&call_fixture, e2e_config, "rust");
let call = e2e_config.resolve_call_for_fixture(
call_fixture.call.as_deref(),
&call_fixture.id,
&call_fixture.resolved_category(),
&call_fixture.tags,
&call_fixture.input,
);
let display_result = !expects_error && presentation.is_empty() && !call.returns_void;
let body = body
.into_iter()
.map(|line| {
if display_result {
line.replacen("let _ =", &format!("let {} =", call.effective_result_var()), 1)
} else {
line.to_string()
}
})
.collect::<Vec<_>>();
Ok(crate::e2e::template_env::render(
"rust/snippet_body.rs.jinja",
minijinja::context! {
imports => imports, body => body, is_async => is_async, presentation => presentation,
display_result => display_result, result_var => call.effective_result_var(),
expects_error => expects_error, returns_void => call.returns_void,
},
))
}
fn language_name(&self) -> &'static str {
"rust"
}
}
fn extract_rust_snippet(rendered: &str) -> Result<(Vec<&str>, Vec<&str>, bool)> {
let lines = rendered.lines().collect::<Vec<_>>();
let signature = lines
.iter()
.position(|line| line.starts_with("async fn test_") || line.starts_with("fn test_"))
.ok_or_else(|| anyhow::anyhow!("generated Rust test did not contain a fixture function"))?;
let imports = lines[..signature]
.iter()
.copied()
.filter(|line| line.starts_with("use ") && !line.contains("common::"))
.collect();
let function_end = find_function_end(&lines, signature + 1)
.ok_or_else(|| anyhow::anyhow!("generated Rust fixture function was not closed"))?;
let body = lines[signature + 1..function_end]
.iter()
.copied()
.filter(|line| !line.trim_start().starts_with("//"))
.collect();
Ok((imports, body, lines[signature].starts_with("async fn ")))
}
fn find_function_end(lines: &[&str], start: usize) -> Option<usize> {
#[derive(Clone, Copy)]
enum State {
Code,
Str,
RawStr(usize),
}
let mut depth: i32 = 1;
let mut state = State::Code;
for (offset, line) in lines[start..].iter().enumerate() {
let chars: Vec<char> = line.chars().collect();
let mut i = 0;
while i < chars.len() {
match state {
State::Code => {
if chars[i] == '/' && chars.get(i + 1) == Some(&'/') {
break; } else if chars[i] == '"' {
state = State::Str;
} else if let Some(hashes) = raw_string_open(&chars, i) {
state = State::RawStr(hashes);
i += 1 + hashes; } else if chars[i] == '{' {
depth += 1;
} else if chars[i] == '}' {
depth -= 1;
if depth == 0 {
return Some(start + offset);
}
}
}
State::Str => {
if chars[i] == '\\' {
i += 1; } else if chars[i] == '"' {
state = State::Code;
}
}
State::RawStr(hashes) => {
if chars[i] == '"' && raw_string_close_matches(&chars, i + 1, hashes) {
i += hashes; state = State::Code;
}
}
}
i += 1;
}
}
None
}
fn raw_string_open(chars: &[char], i: usize) -> Option<usize> {
if chars.get(i) != Some(&'r') {
return None;
}
let mut hashes = 0;
while chars.get(i + 1 + hashes) == Some(&'#') {
hashes += 1;
}
if chars.get(i + 1 + hashes) == Some(&'"') {
Some(hashes)
} else {
None
}
}
fn raw_string_close_matches(chars: &[char], from: usize, hashes: usize) -> bool {
(0..hashes).all(|offset| chars.get(from + offset) == Some(&'#'))
}
fn resolve_crate_name(_e2e_config: &E2eConfig, config: &ResolvedCrateConfig) -> String {
config.name.clone()
}
fn resolve_crate_path(e2e_config: &E2eConfig, crate_name: &str) -> String {
e2e_config
.resolve_package("rust")
.and_then(|p| p.path.clone())
.unwrap_or_else(|| format!("../../crates/{crate_name}"))
}
fn resolve_crate_version(e2e_config: &E2eConfig) -> Option<String> {
e2e_config.resolve_package("rust").and_then(|p| p.version.clone())
}
pub fn emit_test_backend(
trait_bridge: &crate::core::config::TraitBridgeConfig,
methods: &[&crate::core::ir::MethodDef],
fixture: &Fixture,
) -> super::TestBackendEmission {
use crate::codegen::defaults::language_defaults;
use std::fmt::Write as FmtWrite;
let stub_name = format!("TestStub{}", fixture_id_to_pascal_case(&fixture.id));
let trait_name = &trait_bridge.trait_name;
let backend_name = extract_backend_name_from_input(&fixture.input, &fixture.id);
let defaults = language_defaults("rust");
let mut type_imports: Vec<String> = Vec::new();
type_imports.push(trait_name.clone());
let mut setup = String::new();
let crate_module: Option<&str> = trait_bridge
.super_trait
.as_deref()
.and_then(|s| s.split("::").next())
.filter(|s| !s.is_empty());
let _ = writeln!(setup, "struct {stub_name} {{ _name: &'static str }}");
if let Some(super_trait) = &trait_bridge.super_trait {
let super_short = super_trait.split("::").last().unwrap_or(super_trait.as_str());
let _ = writeln!(setup, "impl {super_trait} for {stub_name} {{");
let _ = writeln!(setup, " fn name(&self) -> &str {{ self._name }}");
let _ = writeln!(setup, "}}");
if !super_short.is_empty() && !super_trait.contains("::") {
type_imports.push(super_short.to_string());
}
}
let has_async_methods = methods
.iter()
.any(|m| !(m.has_default_impl || trait_bridge.super_trait.is_some() && m.name == "name") && m.is_async);
if has_async_methods {
let _ = writeln!(setup, "#[async_trait::async_trait]");
}
let _ = writeln!(setup, "impl {trait_name} for {stub_name} {{");
for method in methods {
if method.has_default_impl {
continue;
}
if trait_bridge.super_trait.is_some() && method.name == "name" {
continue;
}
emit_rust_stub_method(&mut setup, method, &*defaults, &mut type_imports, crate_module);
}
let _ = writeln!(setup, "}}");
type_imports.sort();
type_imports.dedup();
let arg_expr = format!("std::sync::Arc::new({stub_name} {{ _name: \"{backend_name}\" }})");
let type_imports = type_imports
.into_iter()
.filter(|s| {
!matches!(
s.as_str(),
"bool"
| "u8"
| "u16"
| "u32"
| "u64"
| "i8"
| "i16"
| "i32"
| "i64"
| "f32"
| "f64"
| "usize"
| "isize"
| "String"
| "str"
| "Vec"
| "Option"
| "Result"
| "()"
)
})
.collect();
super::TestBackendEmission {
setup_block: setup,
arg_expr,
type_imports,
teardown_block: String::new(),
}
}
fn collect_named_types(ty: &crate::core::ir::TypeRef, out: &mut Vec<String>) {
use crate::core::ir::TypeRef;
match ty {
TypeRef::Named(name) => out.push(name.clone()),
TypeRef::Optional(inner) | TypeRef::Vec(inner) => collect_named_types(inner, out),
TypeRef::Map(k, v) => {
collect_named_types(k, out);
collect_named_types(v, out);
}
_ => {}
}
}
fn rust_type_name(ty: &crate::core::ir::TypeRef) -> String {
use crate::codegen::type_mapper::{IdentityMapper, TypeMapper};
IdentityMapper.map_type(ty)
}
fn emit_rust_stub_method(
out: &mut String,
method: &crate::core::ir::MethodDef,
defaults: &dyn crate::codegen::defaults::LanguageDefaults,
type_imports: &mut Vec<String>,
crate_module: Option<&str>,
) {
use crate::core::ir::TypeRef;
use std::fmt::Write as FmtWrite;
let params_typed: Vec<String> = method
.params
.iter()
.enumerate()
.map(|(i, param)| {
collect_named_types(¶m.ty, type_imports);
if param.is_ref {
use crate::core::ir::TypeRef;
let mut_kw = if param.is_mut { "mut " } else { "" };
let ref_str = match ¶m.ty {
TypeRef::Bytes => format!("&{mut_kw}[u8]"),
TypeRef::String => format!("&{mut_kw}str"),
other => format!("&{}{}", mut_kw, rust_type_name(other)),
};
format!("_p{i}: {ref_str}")
} else {
let ty_str = rust_type_name(¶m.ty);
format!("_p{i}: {ty_str}")
}
})
.collect();
let params_str = if params_typed.is_empty() {
String::new()
} else {
format!(", {}", params_typed.join(", "))
};
let return_type_str = if method.returns_ref {
use crate::core::ir::TypeRef;
let ref_type = match &method.return_type {
TypeRef::String => "&str".to_string(),
TypeRef::Bytes => "&[u8]".to_string(),
TypeRef::Vec(inner) => match inner.as_ref() {
TypeRef::String => "&[&str]".to_string(),
TypeRef::Bytes => "&[u8]".to_string(),
other => format!("&[{}]", rust_type_name(other)),
},
other => format!("&{}", rust_type_name(other)),
};
Some(ref_type)
} else {
match &method.return_type {
TypeRef::Unit if method.error_type.is_none() => None,
_ => {
let base = rust_type_name(&method.return_type);
collect_named_types(&method.return_type, type_imports);
let full = if let Some(err) = &method.error_type {
if err == "anyhow::Error" {
if let Some(module) = crate_module {
format!("{module}::Result<{base}>")
} else {
format!("Result<{base}, {err}>")
}
} else {
if !err.contains("::") {
type_imports.push(err.clone());
}
format!("Result<{base}, {err}>")
}
} else {
base
};
Some(full)
}
}
};
let body = if method.returns_ref {
use crate::core::ir::TypeRef;
match &method.return_type {
TypeRef::String => "\"\"".to_string(),
TypeRef::Bytes | TypeRef::Vec(_) => "&[]".to_string(),
_ => format!(
"compile_error!(\"alef cannot generate Rust e2e test_backend method `{}` because it returns an \
unsupported reference type\")",
method.name
),
}
} else {
let raw = match &method.return_type {
TypeRef::Unit => "()".to_string(),
_ => defaults.emit_default(&method.return_type),
};
if method.error_type.is_some() {
format!("Ok({raw})")
} else {
raw
}
};
let async_kw = if method.is_async { "async " } else { "" };
let return_annotation = match &return_type_str {
Some(rt) => format!(" -> {rt}"),
None => String::new(),
};
let _ = writeln!(
out,
" {async_kw}fn {name}(&self{params_str}){return_annotation} {{ {body} }}",
name = method.name
);
}
fn fixture_id_to_pascal_case(id: &str) -> String {
id.split('_')
.map(|word| {
let mut chars = word.chars();
match chars.next() {
None => String::new(),
Some(first) => first.to_uppercase().to_string() + chars.as_str(),
}
})
.collect()
}
fn extract_backend_name_from_input(input: &serde_json::Value, fallback: &str) -> String {
if let Some(obj) = input.as_object() {
if let Some(s) = obj.get("name").and_then(|v| v.as_str()) {
return s.to_string();
}
for v in obj.values() {
if let Some(inner) = v.as_object()
&& let Some(s) = inner.get("name").and_then(|v| v.as_str())
{
return s.to_string();
}
}
for v in obj.values() {
if let Some(s) = v.as_str() {
return s.to_string();
}
}
}
fallback.to_string()
}
#[cfg(test)]
fn test_method(
name: &str,
return_type: crate::core::ir::TypeRef,
is_async: bool,
error_type: Option<&str>,
) -> crate::core::ir::MethodDef {
crate::core::ir::MethodDef {
name: name.to_string(),
params: Vec::new(),
return_type,
is_async,
is_static: false,
error_type: error_type.map(str::to_string),
doc: String::new(),
receiver: Some(crate::core::ir::ReceiverKind::Ref),
cfg: None,
sanitized: false,
trait_source: None,
returns_ref: false,
returns_cow: false,
return_newtype_wrapper: None,
has_default_impl: false,
binding_excluded: false,
binding_exclusion_reason: None,
version: Default::default(),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_fixture(id: &str, input: serde_json::Value) -> crate::e2e::fixture::Fixture {
serde_json::from_value(serde_json::json!({
"id": id,
"description": "test fixture",
"input": input,
"assertions": []
}))
.expect("minimal fixture JSON must parse")
}
#[test]
fn resolve_crate_name_uses_config_name() {
use crate::core::config::NewAlefConfig;
let cfg: NewAlefConfig = toml::from_str(
r#"
[workspace]
languages = ["rust"]
[[crates]]
name = "my-lib"
sources = ["src/lib.rs"]
[crates.e2e]
fixtures = "fixtures"
output = "e2e"
[crates.e2e.call]
function = "process"
module = "my_lib"
result_var = "result"
"#,
)
.unwrap();
let e2e = cfg.crates[0].e2e.clone().unwrap();
let resolved = cfg.resolve().unwrap().remove(0);
let name = resolve_crate_name(&e2e, &resolved);
assert_eq!(name, "my-lib");
}
#[test]
fn snippet_body_matches_rust_client_json_and_async_rendering() {
use crate::core::config::NewAlefConfig;
use crate::e2e::codegen::E2eCodegen;
let cfg: NewAlefConfig = toml::from_str(
r#"
[workspace]
languages = ["rust"]
[[crates]]
name = "example-core"
sources = ["src/lib.rs"]
[crates.e2e]
fixtures = "fixtures"
[crates.e2e.call]
function = "chat"
module = "example_core"
async = true
args = [{ name = "request", field = "input", type = "json_object", owned = true }]
[crates.e2e.call.overrides.rust]
client_factory = "create_client"
options_type = "ChatRequest"
"#,
)
.expect("snippet config must parse");
let e2e = cfg.crates[0].e2e.clone().expect("e2e config");
let resolved = cfg.resolve().expect("config resolves").remove(0);
let fixture: Fixture = serde_json::from_value(serde_json::json!({
"id": "client_chat",
"description": "send a request",
"input": {"model": "example-model", "url": "$mock_url/guide", "messages": []},
"mock_response": {"status": 200},
"docs": {
"topic": "guides",
"presentation": {
"input": {"model": "example-model", "url": "https://api.example.com/guide", "messages": []},
"operations": [{"op": "show", "path": "message"}]
}
},
"assertions": [{"type": "not_error"}]
}))
.expect("fixture must parse");
let rendered = RustE2eCodegen
.render_snippet_body(&fixture, &e2e, &resolved, &[], &[])
.expect("snippet renders");
assert!(rendered.contains("let request: ChatRequest"), "{rendered}");
assert!(rendered.contains("example_core::create_client"), "{rendered}");
assert!(rendered.contains("client.chat(request).await"), "{rendered}");
assert!(rendered.contains(".await.expect(\"call failed\")"), "{rendered}");
assert!(rendered.contains("println!(\"{:?}\", result.message);"), "{rendered}");
assert!(rendered.contains("#[tokio::main]"), "{rendered}");
assert!(!rendered.contains("#[tokio::test]"), "{rendered}");
assert!(!rendered.contains("fn test_"), "{rendered}");
assert!(rendered.contains("https://api.example.com/guide"), "{rendered}");
assert!(!rendered.contains("MOCK_SERVER"), "{rendered}");
assert!(!rendered.contains("E2E_ALLOW_PRIVATE_NETWORK"), "{rendered}");
assert!(!rendered.contains("$mock_url"), "{rendered}");
}
#[test]
fn successful_snippet_binds_and_displays_the_call_result() {
use crate::e2e::codegen::E2eCodegen;
let fixture = make_fixture("list_widgets", serde_json::Value::Null);
let mut e2e = crate::e2e::config::E2eConfig::default();
e2e.call.function = "list_widgets".into();
e2e.call.result_var = "widgets".into();
let rendered = RustE2eCodegen
.render_snippet_body(&fixture, &e2e, &ResolvedCrateConfig::default(), &[], &[])
.expect("Rust snippet renders");
assert!(rendered.contains("let widgets = list_widgets()"), "{rendered}");
assert!(rendered.contains("println!(\"{:?}\", widgets)"), "{rendered}");
assert!(!rendered.contains("let _ = list_widgets()"), "{rendered}");
assert!(
!rendered.contains("match widgets {"),
"a fixture with no error assertion must not get the error branch:\n{rendered}"
);
}
#[test]
fn error_fixture_snippet_matches_the_result_instead_of_panicking() {
use crate::e2e::codegen::E2eCodegen;
let fixture: Fixture = serde_json::from_value(serde_json::json!({
"id": "rate_limit_429",
"description": "Surface a rate-limit failure",
"input": null,
"assertions": [{"type": "error"}]
}))
.expect("fixture must parse");
let mut e2e = crate::e2e::config::E2eConfig::default();
e2e.call.function = "chat".into();
e2e.call.result_var = "result".into();
let rendered = RustE2eCodegen
.render_snippet_body(&fixture, &e2e, &ResolvedCrateConfig::default(), &[], &[])
.expect("Rust snippet renders");
assert!(rendered.contains("let result = chat()"), "{rendered}");
assert!(rendered.contains("match result {"), "{rendered}");
assert!(
rendered.contains("Ok(value) => println!(\"{:?}\", value),"),
"{rendered}"
);
assert!(rendered.contains("Err(error) => println!(\"{error}\"),"), "{rendered}");
assert!(
!rendered.contains(".expect(\"call failed\")"),
"the error branch must not panic on the failure it documents:\n{rendered}"
);
assert!(
!rendered.contains("println!(\"{:?}\", result);"),
"the moved result must not be printed after the match:\n{rendered}"
);
}
#[test]
fn client_factory_snippet_never_points_the_reader_at_the_mock_server() {
use crate::core::config::NewAlefConfig;
use crate::e2e::codegen::E2eCodegen;
let cfg: NewAlefConfig = toml::from_str(
r#"
[workspace]
languages = ["rust"]
[[crates]]
name = "sample-core"
sources = ["src/lib.rs"]
[crates.e2e]
fixtures = "fixtures"
[crates.e2e.call]
function = "chat"
result_var = "result"
[crates.e2e.call.overrides.rust]
client_factory = "create_client"
"#,
)
.expect("snippet config must parse");
let e2e = cfg.crates[0].e2e.clone().expect("e2e config");
let resolved = cfg.resolve().expect("config resolves").remove(0);
let fixture: Fixture = serde_json::from_value(serde_json::json!({
"id": "rate_limit_429",
"description": "Rate limited",
"input": null,
"mock_response": {"status": 429}
}))
.expect("fixture must parse");
let rendered = RustE2eCodegen
.render_snippet_body(&fixture, &e2e, &resolved, &[], &[])
.expect("Rust snippet renders");
assert!(
!rendered.contains("MOCK_SERVER"),
"mock-server env var leaked:\n{rendered}"
);
assert!(
!rendered.contains("/fixtures/rate_limit_429"),
"mock-server fixture route leaked:\n{rendered}"
);
assert!(
!rendered.contains("\"test-key\""),
"literal credential leaked:\n{rendered}"
);
assert!(
rendered.contains("std::env::var(\"API_KEY\").expect(\"API_KEY must be set\")"),
"credential is not read from the environment:\n{rendered}"
);
assert!(
rendered.contains(
"sample_core::create_client(std::env::var(\"API_KEY\").expect(\"API_KEY must be set\"), \
None, None, None, None).unwrap();"
),
"client is not constructed the way a reader would:\n{rendered}"
);
}
#[test]
fn raw_literal_handles_backticks_and_blank_line_after_fence() {
let input = "<pre><code>```rust\nlet value = r#\"sample\"#;\n```\n\nnext</code></pre>";
let literal = crate::e2e::escape::rust_raw_string(input);
let expression = syn::parse_str::<syn::Expr>(&literal).expect("generated raw literal parses");
assert!(matches!(expression, syn::Expr::Lit(_)), "{literal}");
assert!(literal.starts_with("r##\""), "{literal}");
}
#[test]
fn snippet_extraction_preserves_multiline_raw_literal_contents() {
let rendered = concat!(
"use sample::process;\n",
"\n",
"fn test_multiline() {\n",
" let source = r#\"# A comment\n",
"def greet(name):\n",
" return name\n",
"\n",
"import os\n",
"\"#;\n",
" let _ = process(source);\n",
"}\n",
"}\n",
);
let (_, body, _) = extract_rust_snippet(rendered).expect("snippet extracts");
let body = body.join("\n");
assert!(body.contains("def greet(name):"), "{body}");
assert!(body.contains("import os"), "{body}");
assert!(body.contains("\ndef greet(name):"), "{body}");
assert!(!body.lines().any(|line| line == "}"), "{body}");
syn::parse_file(&format!("fn main() {{\n{body}\n}}")).expect("generated snippet body parses");
}
#[test]
fn snippet_extraction_survives_a_bare_closing_brace_inside_a_raw_string() {
let rendered = concat!(
"use sample::process;\n",
"\n",
"fn test_brace_in_literal() {\n",
" let source = r#\"fn example() {\n",
"}\n",
"let after = 1;\n",
"\"#;\n",
" let _ = process(source);\n",
"}\n",
);
let (_, body, _) = extract_rust_snippet(rendered).expect("snippet extracts");
let body = body.join("\n");
assert!(body.contains("fn example() {"), "{body}");
assert!(body.contains("let after = 1;"), "{body}");
assert!(body.contains("let _ = process(source);"), "{body}");
}
#[test]
fn emit_test_backend_rust_generates_struct_and_arc_expr() {
use crate::core::config::TraitBridgeConfig;
use crate::core::ir::TypeRef;
let bridge = TraitBridgeConfig {
trait_name: "TestTrait".to_string(),
super_trait: Some("Plugin".to_string()),
register_fn: Some("register_test_trait".to_string()),
..Default::default()
};
let m1 = test_method("do_work", TypeRef::String, false, None);
let m2 = test_method(
"process_async",
TypeRef::Named("WorkResult".to_string()),
true,
Some("WorkError"),
);
let methods = [&m1, &m2];
let fixture = make_fixture("my_test_fixture", serde_json::json!({ "name": "my-test-backend" }));
let emission = emit_test_backend(&bridge, &methods, &fixture);
assert!(
emission.setup_block.contains("TestStubMyTestFixture"),
"setup_block should contain stub name, got: {}",
emission.setup_block
);
assert!(
emission.setup_block.contains("TestTrait"),
"setup_block should reference trait by name, got: {}",
emission.setup_block
);
assert!(
!emission.setup_block.contains("OcrBackend"),
"setup_block must not hardcode OcrBackend"
);
assert!(
!emission.setup_block.contains("DocumentExtractor"),
"setup_block must not hardcode DocumentExtractor"
);
assert!(
emission.setup_block.contains("fn name("),
"setup_block should emit name() when super_trait is set"
);
assert!(
emission.setup_block.contains("fn do_work("),
"required method do_work should be in setup_block"
);
assert!(
emission.setup_block.contains("fn process_async("),
"required async method process_async should be in setup_block"
);
assert!(
emission.arg_expr.contains("Arc::new"),
"arg_expr should use Arc::new, got: {}",
emission.arg_expr
);
assert!(
emission.arg_expr.contains("TestStubMyTestFixture"),
"arg_expr should reference stub struct, got: {}",
emission.arg_expr
);
}
#[test]
fn emit_test_backend_rust_skips_default_impl_methods() {
use crate::core::config::TraitBridgeConfig;
use crate::core::ir::TypeRef;
let bridge = TraitBridgeConfig {
trait_name: "TestTrait".to_string(),
..Default::default()
};
let required = test_method("required_method", TypeRef::String, false, None);
let mut optional = test_method("optional_method", TypeRef::String, false, None);
optional.has_default_impl = true;
let methods = [&required, &optional];
let fixture = make_fixture("skip_defaults_fixture", serde_json::json!({}));
let emission = emit_test_backend(&bridge, &methods, &fixture);
assert!(
emission.setup_block.contains("fn required_method("),
"required method should be emitted"
);
assert!(
!emission.setup_block.contains("fn optional_method("),
"method with default impl should be skipped"
);
}
#[test]
fn emit_test_backend_rust_name_extracted_from_input() {
use crate::core::config::TraitBridgeConfig;
let bridge = TraitBridgeConfig {
trait_name: "TestTrait".to_string(),
super_trait: Some("Plugin".to_string()),
..Default::default()
};
let fixture = make_fixture(
"name_extraction_fixture",
serde_json::json!({ "backend": { "name": "extracted-name" } }),
);
let emission = emit_test_backend(&bridge, &[], &fixture);
assert!(
emission.arg_expr.contains("extracted-name"),
"arg_expr should contain the name from input.backend.name, got: {}",
emission.arg_expr
);
}
#[test]
fn emit_test_backend_rust_pins_error_type_to_the_method_signature() {
use crate::core::config::TraitBridgeConfig;
use crate::core::ir::TypeRef;
let bridge = TraitBridgeConfig {
trait_name: "TestTrait".to_string(),
super_trait: Some("Plugin".to_string()),
..Default::default()
};
let method = test_method(
"embed",
TypeRef::Vec(Box::new(TypeRef::String)),
true,
Some("SampleCrateError"),
);
let methods = [&method];
let fixture = make_fixture("error_type_fixture", serde_json::json!({ "name": "backend" }));
let emission = emit_test_backend(&bridge, &methods, &fixture);
assert!(
emission
.setup_block
.contains("async fn embed(&self) -> Result<Vec<String>, SampleCrateError>"),
"stub signature must use the method's declared error type, got: {}",
emission.setup_block
);
assert!(
emission.type_imports.contains(&"SampleCrateError".to_string()),
"the declared error type must be imported, got: {:?}",
emission.type_imports
);
assert_eq!(
emission
.type_imports
.iter()
.filter(|import| import.ends_with("Error"))
.collect::<Vec<_>>(),
vec![&"SampleCrateError".to_string()],
"no error type other than the declared one may be imported, got: {:?}",
emission.type_imports
);
}
#[test]
fn emit_test_backend_rust_renders_alias_result_through_the_crate_module() {
use crate::core::config::TraitBridgeConfig;
use crate::core::ir::TypeRef;
let bridge = TraitBridgeConfig {
trait_name: "TestTrait".to_string(),
super_trait: Some("sample_core::plugins::Plugin".to_string()),
..Default::default()
};
let method = test_method("validate", TypeRef::Unit, true, Some("anyhow::Error"));
let methods = [&method];
let fixture = make_fixture("alias_result_fixture", serde_json::json!({ "name": "backend" }));
let emission = emit_test_backend(&bridge, &methods, &fixture);
assert!(
emission
.setup_block
.contains("async fn validate(&self) -> sample_core::Result<()>"),
"single-arg Result alias must render through the crate module, got: {}",
emission.setup_block
);
assert!(
!emission.type_imports.iter().any(|import| import.contains("Error")),
"the alias sentinel must not become an error-type import, got: {:?}",
emission.type_imports
);
}
}