use crate::e2e::codegen::TestBackendEmission;
use crate::e2e::escape::sanitize_ident;
use std::fmt::Write as FmtWrite;
pub fn emit_test_backend(
trait_bridge: &crate::core::config::TraitBridgeConfig,
methods: &[&crate::core::ir::MethodDef],
fixture: &crate::e2e::fixture::Fixture,
) -> TestBackendEmission {
use crate::codegen::defaults::language_defaults;
use crate::e2e::escape::escape_r;
let defaults = language_defaults("r");
let backend_name = extract_backend_name_from_input(&fixture.input, &fixture.id);
let var_name = format!("r_backend_{}", sanitize_ident(&fixture.id));
let mut setup = String::new();
let _ = writeln!(setup, " {var_name} <- list(");
let required: Vec<_> = methods.iter().filter(|m| !m.has_default_impl).collect();
let super_trait_entries: Vec<String> = if trait_bridge.super_trait.is_some() {
let escaped_name = escape_r(&backend_name);
vec![
format!(" name = \"{escaped_name}\""),
" initialize = function() invisible(NULL)".to_string(),
" shutdown = function() invisible(NULL)".to_string(),
]
} else {
vec![]
};
let total_entries = super_trait_entries.len() + required.len();
let mut emitted = 0usize;
for entry in &super_trait_entries {
emitted += 1;
let trailing = if emitted < total_entries { "," } else { "" };
let _ = writeln!(setup, "{entry}{trailing}");
}
for method in required.iter() {
let method_name = &method.name;
let method_val = if let Some(backend_obj) = fixture.input.get("backend") {
if let Some(val) = backend_obj.get(method_name) {
match val {
serde_json::Value::Number(n) => n.to_string(),
serde_json::Value::String(s) => format!("\"{}\"", escape_r(s)),
serde_json::Value::Bool(b) => {
if *b {
"TRUE".to_string()
} else {
"FALSE".to_string()
}
}
serde_json::Value::Array(_) => "c()".to_string(), serde_json::Value::Null | serde_json::Value::Object(_) => {
defaults.emit_default(&method.return_type)
}
}
} else {
defaults.emit_default(&method.return_type)
}
} else {
defaults.emit_default(&method.return_type)
};
let params: Vec<&str> = method.params.iter().map(|p| p.name.as_str()).collect();
let param_list = params.join(", ");
emitted += 1;
let trailing = if emitted < total_entries { "," } else { "" };
let _ = writeln!(
setup,
" {method_name} = function({param_list}) {method_val}{trailing}"
);
}
let _ = writeln!(setup, " )");
let teardown_block = trait_bridge
.unregister_fn
.as_deref()
.map(|unregister_fn| {
let escaped = escape_r(&backend_name);
format!(" {unregister_fn}(\"{escaped}\")\n")
})
.unwrap_or_default();
TestBackendEmission {
setup_block: setup,
arg_expr: var_name,
type_imports: Vec::new(),
teardown_block,
}
}
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() {
if 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)]
mod tests {
#[test]
fn test_emit_test_backend_is_generic_no_domain_names() {
use crate::core::config::TraitBridgeConfig;
use crate::core::ir::{MethodDef, ParamDef, ReceiverKind, TypeRef};
use crate::e2e::fixture::Fixture;
let method = MethodDef {
name: "do_work".to_string(),
params: vec![ParamDef {
name: "payload".to_string(),
ty: TypeRef::String,
optional: false,
default: None,
sanitized: false,
typed_default: None,
is_ref: false,
is_mut: false,
newtype_wrapper: None,
original_type: None,
map_is_ahash: false,
map_key_is_cow: false,
vec_inner_is_ref: false,
map_is_btree: false,
core_wrapper: crate::core::ir::CoreWrapper::None,
}],
return_type: TypeRef::String,
is_async: false,
is_static: false,
error_type: None,
doc: String::new(),
receiver: Some(ReceiverKind::Ref),
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(),
};
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 fixture = Fixture {
id: "my_fixture".to_string(),
category: None,
description: "test".to_string(),
tags: vec![],
skip: None,
env: None,
setup: Vec::new(),
call: None,
input: serde_json::Value::Null,
mock_response: None,
source: String::new(),
http: None,
assertions: vec![],
visitor: None,
args: vec![],
assertion_recipes: vec![],
};
let methods = vec![&method];
let emission = super::emit_test_backend(&bridge, &methods, &fixture);
assert!(
emission.setup_block.contains("do_work"),
"setup_block should contain the method 'do_work', got:\n{}",
emission.setup_block
);
assert!(
emission.arg_expr.contains("r_backend_"),
"arg_expr should be the variable name (r_backend_*), got:\n{}",
emission.arg_expr
);
assert!(
emission.setup_block.contains("name = \"my_fixture\""),
"setup_block should contain fixture-derived name = \"my_fixture\" for super-trait, got:\n{}",
emission.setup_block
);
assert!(
emission.setup_block.contains("initialize = function()"),
"setup_block should contain initialize = function() for super-trait, got:\n{}",
emission.setup_block
);
assert!(
emission.setup_block.contains("shutdown = function()"),
"setup_block should contain shutdown = function() for super-trait, got:\n{}",
emission.setup_block
);
for name in &[
"ImageBackend",
"RecordProvider",
"process_image",
"extract_bytes",
"sample_lib",
] {
assert!(
!emission.setup_block.contains(name),
"setup_block must not contain domain name '{name}', got:\n{}",
emission.setup_block
);
}
}
}