use crate::core::backend::GeneratedFile;
use crate::core::config::ResolvedCrateConfig;
use crate::core::hash::{self, CommentStyle};
use crate::e2e::config::E2eConfig;
use crate::e2e::escape::{escape_csharp, sanitize_filename};
use crate::e2e::field_access::FieldResolver;
use crate::e2e::fixture::{Fixture, FixtureGroup};
use anyhow::Result;
use heck::ToUpperCamelCase;
use std::collections::HashMap;
use std::fmt::Write as FmtWrite;
use std::path::PathBuf;
use super::E2eCodegen;
pub(super) fn resolve_handle_config_type(
arg: &crate::e2e::config::ArgMapping,
options_type: Option<&str>,
type_defs: &[crate::core::ir::TypeDef],
) -> Option<String> {
if arg.arg_type != "handle" {
return None;
}
arg.element_type.as_deref().map(str::to_string).or_else(|| {
options_type.map(str::to_string).or_else(|| {
let candidate = format!("{}Config", arg.name.to_upper_camel_case());
type_defs.iter().any(|ty| ty.name == candidate).then_some(candidate)
})
})
}
pub struct CSharpCodegen;
impl E2eCodegen for CSharpCodegen {
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 function_name = overrides
.and_then(|o| o.function.as_ref())
.cloned()
.unwrap_or_else(|| call.function.to_upper_camel_case());
let class_name = overrides
.and_then(|o| o.class.as_ref())
.cloned()
.unwrap_or_else(|| format!("{}Lib", config.name.to_upper_camel_case()));
let exception_class = format!("{}Exception", config.name.to_upper_camel_case());
let namespace = overrides
.and_then(|o| o.module.as_ref())
.cloned()
.or_else(|| config.csharp.as_ref().and_then(|cs| cs.namespace.clone()))
.unwrap_or_else(|| {
if call.module.is_empty() {
config.name.to_upper_camel_case()
} else {
call.module.to_upper_camel_case()
}
});
let result_is_simple = call.result_is_simple || overrides.is_some_and(|o| o.result_is_simple);
let result_var = &call.result_var;
let is_async = call.r#async;
let cs_pkg = e2e_config.resolve_package("csharp");
let pkg_name = cs_pkg
.as_ref()
.and_then(|p| p.name.as_ref())
.cloned()
.unwrap_or_else(|| config.name.to_upper_camel_case());
let pkg_path = cs_pkg
.as_ref()
.and_then(|p| p.path.as_ref())
.cloned()
.unwrap_or_else(|| format!("../../packages/csharp/{pkg_name}/{pkg_name}.csproj"));
let pkg_version = cs_pkg
.as_ref()
.and_then(|p| p.version.as_ref())
.cloned()
.or_else(|| config.resolved_version())
.unwrap_or_else(|| "0.1.0".to_string());
let csproj_name = format!("{pkg_name}.E2eTests.csproj");
files.push(GeneratedFile {
path: output_base.join(&csproj_name),
content: render_csproj(&pkg_name, &pkg_path, &pkg_version, e2e_config.dep_mode),
generated_header: false,
});
let needs_mock_server = groups
.iter()
.flat_map(|g| g.fixtures.iter())
.any(|f| f.needs_mock_server());
files.push(GeneratedFile {
path: output_base.join("TestSetup.cs"),
content: render_test_setup(needs_mock_server, &e2e_config.test_documents_dir, &namespace),
generated_header: true,
});
let sealed_display_types: std::collections::BTreeSet<String> = std::iter::once(&e2e_config.call)
.chain(e2e_config.calls.values())
.filter_map(|c| c.overrides.get(lang))
.flat_map(|o| o.assert_enum_fields.values().cloned())
.collect();
let tests_base = output_base.join("tests");
for type_name in &sealed_display_types {
if let Some(enum_def) = enums.iter().find(|e| &e.name == type_name) {
files.push(GeneratedFile {
path: tests_base.join(format!("{type_name}Display.cs")),
content: render_sealed_display(type_name, enum_def, type_defs, &namespace),
generated_header: true,
});
}
}
let field_resolver = FieldResolver::new(
&e2e_config.fields,
&e2e_config.fields_optional,
&e2e_config.result_fields,
&e2e_config.fields_array,
&std::collections::HashSet::new(),
);
static EMPTY_ENUM_FIELDS: std::sync::LazyLock<HashMap<String, String>> = std::sync::LazyLock::new(HashMap::new);
static EMPTY_ASSERT_ENUM_FIELDS: std::sync::LazyLock<HashMap<String, String>> =
std::sync::LazyLock::new(HashMap::new);
let enum_fields = overrides.map(|o| &o.enum_fields).unwrap_or(&EMPTY_ENUM_FIELDS);
let assert_enum_fields = overrides
.and_then(|o| {
if o.assert_enum_fields.is_empty() {
None
} else {
Some(&o.assert_enum_fields)
}
})
.unwrap_or(&EMPTY_ASSERT_ENUM_FIELDS);
let mut effective_nested_types: HashMap<String, String> = HashMap::new();
if let Some(overrides_map) = overrides.map(|o| &o.nested_types) {
effective_nested_types.extend(overrides_map.clone());
}
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 test_class = format!("{}Tests", sanitize_filename(&group.category).to_upper_camel_case());
let filename = format!("{test_class}.cs");
let content = render_test_file(
&group.category,
&active,
&namespace,
&class_name,
&function_name,
&exception_class,
result_var,
&test_class,
&e2e_config.call.args,
&field_resolver,
result_is_simple,
is_async,
e2e_config,
enum_fields,
assert_enum_fields,
&effective_nested_types,
&config.adapters,
config,
type_defs,
enums,
);
files.push(GeneratedFile {
path: tests_base.join(filename),
content,
generated_header: true,
});
}
Ok(files)
}
fn language_name(&self) -> &'static str {
"csharp"
}
}
#[allow(clippy::too_many_arguments)]
fn render_test_file(
category: &str,
fixtures: &[&Fixture],
namespace: &str,
class_name: &str,
function_name: &str,
exception_class: &str,
result_var: &str,
test_class: &str,
args: &[crate::e2e::config::ArgMapping],
field_resolver: &FieldResolver,
result_is_simple: bool,
is_async: bool,
e2e_config: &E2eConfig,
enum_fields: &HashMap<String, String>,
assert_enum_fields: &HashMap<String, String>,
nested_types: &HashMap<String, String>,
adapters: &[crate::core::config::extras::AdapterConfig],
config: &ResolvedCrateConfig,
type_defs: &[crate::core::ir::TypeDef],
enums: &[crate::core::ir::EnumDef],
) -> String {
let mut using_imports = String::new();
using_imports.push_str("using System;\n");
using_imports.push_str("using System.Collections.Generic;\n");
using_imports.push_str("using System.Linq;\n");
using_imports.push_str("using System.Net.Http;\n");
using_imports.push_str("using System.Text;\n");
using_imports.push_str("using System.Text.Json;\n");
using_imports.push_str("using System.Text.Json.Serialization;\n");
using_imports.push_str("using System.Threading.Tasks;\n");
using_imports.push_str("using Xunit;\n");
using_imports.push_str(&format!("using {namespace};\n"));
using_imports.push_str(&format!("using static {namespace}.{class_name};\n"));
let config_options_field = " private static readonly JsonSerializerOptions ConfigOptions = new() { Converters = { new JsonStringEnumConverter(JsonNamingPolicy.SnakeCaseLower) }, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault };";
let mut visitor_class_decls: Vec<String> = Vec::new();
let mut fixtures_body = String::new();
for (i, fixture) in fixtures.iter().enumerate() {
render_test_method(
&mut fixtures_body,
&mut visitor_class_decls,
fixture,
class_name,
function_name,
exception_class,
result_var,
args,
field_resolver,
result_is_simple,
is_async,
e2e_config,
enum_fields,
assert_enum_fields,
nested_types,
adapters,
config,
type_defs,
enums,
);
if i + 1 < fixtures.len() {
fixtures_body.push('\n');
}
}
let mut visitor_classes_str = String::new();
for (i, decl) in visitor_class_decls.iter().enumerate() {
if i > 0 {
visitor_classes_str.push('\n');
}
visitor_classes_str.push('\n');
for line in decl.lines() {
visitor_classes_str.push_str(" ");
visitor_classes_str.push_str(line);
visitor_classes_str.push('\n');
}
}
let ctx = minijinja::context! {
header => hash::header(CommentStyle::DoubleSlash),
using_imports => using_imports,
category => category,
namespace => namespace,
test_class => test_class,
config_options_field => config_options_field,
fixtures_body => fixtures_body,
visitor_class_decls => visitor_classes_str,
};
crate::e2e::template_env::render("csharp/test_file.jinja", ctx)
}
#[allow(clippy::too_many_arguments)]
fn render_test_method(
out: &mut String,
visitor_class_decls: &mut Vec<String>,
fixture: &Fixture,
class_name: &str,
_function_name: &str,
exception_class: &str,
_result_var: &str,
_args: &[crate::e2e::config::ArgMapping],
_field_resolver: &FieldResolver,
result_is_simple: bool,
_is_async: bool,
e2e_config: &E2eConfig,
enum_fields: &HashMap<String, String>,
assert_enum_fields: &HashMap<String, String>,
nested_types: &HashMap<String, String>,
adapters: &[crate::core::config::extras::AdapterConfig],
config: &ResolvedCrateConfig,
type_defs: &[crate::core::ir::TypeDef],
enums: &[crate::core::ir::EnumDef],
) {
let method_name = fixture.id.to_upper_camel_case();
let description = &fixture.description;
if let Some(http) = &fixture.http {
render_http_test_method(out, fixture, http);
return;
}
if fixture.mock_response.is_none() && !fixture_has_csharp_callable(fixture, e2e_config) {
let skip_reason =
"non-HTTP fixture: C# binding does not expose a callable for the configured `[e2e.call]` function";
let ctx = minijinja::context! {
is_skipped => true,
skip_reason => skip_reason,
description => description,
method_name => method_name,
};
let rendered = crate::e2e::template_env::render("csharp/test_method.jinja", ctx);
out.push_str(&rendered);
return;
}
let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
let mut call_config = e2e_config.resolve_call_for_fixture(
fixture.call.as_deref(),
&fixture.id,
&fixture.resolved_category(),
&fixture.tags,
&fixture.input,
);
call_config = super::select_best_matching_call(call_config, e2e_config, fixture);
let call_field_resolver = FieldResolver::new(
e2e_config.effective_fields(call_config),
e2e_config.effective_fields_optional(call_config),
e2e_config.effective_result_fields(call_config),
e2e_config.effective_fields_array(call_config),
&std::collections::HashSet::new(),
);
let field_resolver = &call_field_resolver;
let lang = "csharp";
let cs_overrides = call_config.overrides.get(lang);
let raw_function_name = cs_overrides
.and_then(|o| o.function.as_ref())
.cloned()
.unwrap_or_else(|| call_config.function.clone());
if crate::e2e::codegen::streaming_assertions::resolve_is_streaming(fixture, call_config.streaming_enabled()) {
render_streaming_test_method(
out,
fixture,
class_name,
call_config,
cs_overrides,
e2e_config,
enum_fields,
assert_enum_fields,
nested_types,
exception_class,
adapters,
config,
type_defs,
enums,
resolve_csharp_streaming_item_type(call_config, adapters, &raw_function_name).as_deref(),
);
return;
}
let _is_streaming = call_config.streaming_enabled().unwrap_or(false);
let effective_function_name = {
let mut name = cs_overrides
.and_then(|o| o.function.as_ref())
.cloned()
.unwrap_or_else(|| call_config.function.to_upper_camel_case());
if call_config.r#async && !name.ends_with("Async") {
name.push_str("Async");
}
name
};
let effective_result_var = &call_config.result_var;
let effective_is_async = cs_overrides.and_then(|o| o.r#async).unwrap_or(call_config.r#async);
let function_name = effective_function_name.as_str();
let result_var = effective_result_var.as_str();
let is_async = effective_is_async;
let recipe = crate::e2e::codegen::recipe::ResolvedE2eCallRecipe::resolve("csharp", fixture, call_config, type_defs);
let args = recipe.args;
let per_call_result_is_simple = call_config.result_is_simple || cs_overrides.is_some_and(|o| o.result_is_simple);
let per_call_result_is_bytes = call_config.result_is_bytes || cs_overrides.is_some_and(|o| o.result_is_bytes);
let effective_result_is_simple = result_is_simple || per_call_result_is_simple || per_call_result_is_bytes;
let effective_result_is_bytes = per_call_result_is_bytes;
let returns_void = if call_config.returns_void {
true
} else {
let fn_name = call_config.function.as_str();
fn_name.starts_with("register_")
|| fn_name.starts_with("unregister_")
|| fn_name.starts_with("clear_")
|| fn_name == "initialize"
|| fn_name == "shutdown"
};
let extra_args_slice: &[String] = recipe.extra_args;
let top_level_options_type = e2e_config
.call
.overrides
.get("csharp")
.and_then(|o| o.options_type.as_deref());
let effective_options_type = recipe.options_type.or(top_level_options_type);
let top_level_options_via = e2e_config
.call
.overrides
.get("csharp")
.and_then(|o| o.options_via.as_deref());
let effective_options_via = cs_overrides
.and_then(|o| o.options_via.as_deref())
.or(top_level_options_via);
let adapter_request_type_owned: Option<String> = adapters
.iter()
.find(|a| a.name == call_config.function.as_str())
.and_then(|a| a.request_type.as_deref())
.map(|rt| rt.rsplit("::").next().unwrap_or(rt).to_string());
let mut effective_call_nested_types: std::collections::HashMap<String, String> = nested_types.clone();
if let Some(o) = cs_overrides {
for (k, v) in &o.nested_types {
effective_call_nested_types.insert(k.clone(), v.clone());
}
}
let mut teardown_lines: Vec<String> = Vec::new();
let (mut setup_lines, mut args_str) = build_args_and_setup(
&fixture.input,
args,
class_name,
effective_options_type,
effective_options_via,
enum_fields,
&effective_call_nested_types,
fixture,
adapter_request_type_owned.as_deref(),
config,
type_defs,
enums,
visitor_class_decls,
&mut teardown_lines,
);
if _is_streaming && adapter_request_type_owned.is_some() {
let has_mock_url_list = args.iter().any(|arg| arg.arg_type == "mock_url_list");
if has_mock_url_list {
if let Some(req_type) = &adapter_request_type_owned {
let parts: Vec<&str> = args_str.split(", ").collect();
if parts.len() >= 2 {
let urls_var = parts[parts.len() - 1]; let req_var = format!("{}Req", urls_var);
setup_lines.push(format!("var {req_var} = new {req_type} {{ Urls = {urls_var} }};"));
args_str = parts[..parts.len() - 1].join(", ");
if !args_str.is_empty() {
args_str.push_str(", ");
}
args_str.push_str(&req_var);
}
}
}
}
let mut visitor_arg = String::new();
let has_visitor = fixture.visitor.is_some();
if let Some(visitor_spec) = &fixture.visitor {
let visitor_config = resolve_csharp_visitor_config(config, cs_overrides, type_defs, visitor_spec);
visitor_arg = build_csharp_visitor(
&mut setup_lines,
visitor_class_decls,
&fixture.id,
visitor_spec,
&visitor_config,
);
}
let final_args = if has_visitor && !visitor_arg.is_empty() {
let Some(opts_type) =
effective_options_type.or_else(|| crate::e2e::codegen::recipe::trait_bridge_options_type(config))
else {
let return_type = if is_async { "async Task" } else { "void" };
let _ = writeln!(out, " [Fact]");
let _ = writeln!(out, " public {return_type} Test{method_name}()");
let _ = writeln!(out, " {{");
let _ = writeln!(out, " // {description}");
let _ = writeln!(out, " return;");
let _ = writeln!(out, " }}");
return;
};
if args_str.contains("JsonSerializer.Deserialize") {
setup_lines.push(format!("var options = {args_str};"));
setup_lines.push(format!("options.Visitor = {visitor_arg};"));
"options".to_string()
} else if args_str.ends_with(", null") {
setup_lines.push(format!("var options = new {opts_type} {{ Visitor = {visitor_arg} }};"));
let trimmed = args_str[..args_str.len() - 6].to_string(); format!("{trimmed}, options")
} else if args_str.contains(", null,") {
setup_lines.push(format!("var options = new {opts_type} {{ Visitor = {visitor_arg} }};"));
args_str.replace(", null,", ", options,")
} else if args_str.is_empty() {
setup_lines.push(format!("var options = new {opts_type} {{ Visitor = {visitor_arg} }};"));
"options".to_string()
} else {
setup_lines.push(format!("var options = new {opts_type} {{ Visitor = {visitor_arg} }};"));
format!("{args_str}, options")
}
} else if extra_args_slice.is_empty() {
args_str
} else if args_str.is_empty() {
extra_args_slice.join(", ")
} else {
format!("{args_str}, {}", extra_args_slice.join(", "))
};
let effective_function_name = function_name.to_string();
let return_type = if is_async { "async Task" } else { "void" };
let await_kw = if is_async { "await " } else { "" };
let client_factory = cs_overrides.and_then(|o| o.client_factory.as_deref()).or_else(|| {
e2e_config
.call
.overrides
.get("csharp")
.and_then(|o| o.client_factory.as_deref())
});
let call_target = if client_factory.is_some() {
"client".to_string()
} else if _is_streaming {
args.iter()
.find(|arg| arg.arg_type == "handle")
.map(|arg| arg.name.clone())
.unwrap_or_else(|| "engine".to_string())
} else {
class_name.to_string()
};
let mut client_factory_setup = String::new();
if let Some(factory) = client_factory {
let factory_name = factory.to_upper_camel_case();
let fixture_id = &fixture.id;
let has_mock = fixture.mock_response.is_some() || fixture.http.is_some();
let api_key_var_opt = fixture.env.as_ref().and_then(|e| e.api_key_var.as_deref());
let is_live_smoke = !has_mock && api_key_var_opt.is_some();
if let Some(api_key_var) = api_key_var_opt.filter(|_| has_mock) {
client_factory_setup.push_str(&format!(
" var apiKey = System.Environment.GetEnvironmentVariable(\"{api_key_var}\");\n"
));
client_factory_setup.push_str(&format!(
" var baseUrl = string.IsNullOrEmpty(apiKey)\n ? (System.Environment.GetEnvironmentVariable(\"MOCK_SERVER_URL\") ?? string.Empty) + \"/fixtures/{fixture_id}\"\n : null;\n"
));
client_factory_setup.push_str(&format!(
" Console.WriteLine($\"{fixture_id}: \" + (baseUrl == null ? \"using real API ({api_key_var} is set)\" : \"using mock server ({api_key_var} not set)\"));\n"
));
client_factory_setup.push_str(&format!(
" var client = {class_name}.{factory_name}(string.IsNullOrEmpty(apiKey) ? \"test-key\" : apiKey, baseUrl, null, null, null);\n"
));
} else if let Some(api_key_var) = api_key_var_opt.filter(|_| is_live_smoke) {
client_factory_setup.push_str(&format!(
" var apiKey = System.Environment.GetEnvironmentVariable(\"{api_key_var}\");\n"
));
client_factory_setup.push_str(" if (string.IsNullOrEmpty(apiKey)) { return; }\n");
client_factory_setup.push_str(&format!(
" var client = {class_name}.{factory_name}(apiKey, null, null, null, null);\n"
));
} else if fixture.has_host_root_route() {
let env_key = format!("MOCK_SERVER_{}", fixture_id.to_uppercase());
client_factory_setup.push_str(&format!(
" var _perFixtureUrl = System.Environment.GetEnvironmentVariable(\"{env_key}\");\n"
));
client_factory_setup.push_str(&format!(" var baseUrl = !string.IsNullOrEmpty(_perFixtureUrl) ? _perFixtureUrl : (System.Environment.GetEnvironmentVariable(\"MOCK_SERVER_URL\") ?? string.Empty) + \"/fixtures/{fixture_id}\";\n"));
client_factory_setup.push_str(&format!(
" var client = {class_name}.{factory_name}(\"test-key\", baseUrl, null, null, null);\n"
));
} else {
client_factory_setup.push_str(&format!(" var baseUrl = (System.Environment.GetEnvironmentVariable(\"MOCK_SERVER_URL\") ?? string.Empty) + \"/fixtures/{fixture_id}\";\n"));
client_factory_setup.push_str(&format!(
" var client = {class_name}.{factory_name}(\"test-key\", baseUrl, null, null, null);\n"
));
}
}
let is_trait_bridge_registration = args.iter().any(|arg| arg.arg_type == "test_backend");
let call_expr = if is_trait_bridge_registration {
final_args.clone()
} else if _is_streaming {
let args_parts: Vec<&str> = final_args.split(", ").collect();
let args_without_engine = if args_parts.len() > 1 {
args_parts[1..].join(", ")
} else {
String::new()
};
format!("{}({})", effective_function_name, args_without_engine)
} else {
format!("{}({})", effective_function_name, final_args)
};
let mut effective_enum_fields: std::collections::HashSet<String> = e2e_config.fields_enum.clone();
for k in enum_fields.keys() {
effective_enum_fields.insert(k.clone());
}
if let Some(o) = cs_overrides {
for k in o.enum_fields.keys() {
effective_enum_fields.insert(k.clone());
}
}
let mut effective_assert_enum_fields: std::collections::HashMap<String, String> = assert_enum_fields.clone();
if let Some(o) = cs_overrides {
for (k, v) in &o.assert_enum_fields {
effective_assert_enum_fields.insert(k.clone(), v.clone());
}
}
let mut assertions_body = String::new();
if !expects_error && !returns_void {
for assertion in &fixture.assertions {
render_assertion(
&mut assertions_body,
assertion,
result_var,
class_name,
exception_class,
field_resolver,
effective_result_is_simple,
call_config.result_is_vec || cs_overrides.is_some_and(|o| o.result_is_vec),
call_config.result_is_array,
effective_result_is_bytes,
&effective_enum_fields,
&effective_assert_enum_fields,
);
}
}
let ctx = minijinja::context! {
is_skipped => false,
expects_error => expects_error,
description => description,
return_type => return_type,
method_name => method_name,
async_kw => await_kw,
call_target => call_target,
setup_lines => setup_lines.clone(),
call_expr => call_expr,
exception_class => exception_class,
client_factory_setup => client_factory_setup,
has_usable_assertion => !expects_error && !returns_void,
result_var => result_var,
assertions_body => assertions_body,
is_streaming => _is_streaming,
is_trait_bridge => is_trait_bridge_registration,
teardown_lines => teardown_lines.clone(),
};
let rendered = crate::e2e::template_env::render("csharp/test_method.jinja", ctx);
for line in rendered.lines() {
let trimmed = line.trim_end();
if !trimmed.is_empty() {
out.push_str(" ");
out.push_str(trimmed);
}
out.push('\n');
}
}
mod assertions;
mod discriminated;
mod http;
mod project;
mod setup;
mod streaming;
mod values;
mod visitor;
use assertions::render_assertion;
use discriminated::{parse_discriminated_union_access, render_discriminated_union_assertion};
use http::render_http_test_method;
use project::{render_csproj, render_test_setup};
use setup::build_args_and_setup;
use streaming::{render_streaming_test_method, resolve_csharp_streaming_item_type};
use values::{json_to_csharp, render_sealed_display};
use visitor::{build_csharp_visitor, resolve_csharp_visitor_config};
pub(super) fn build_csharp_method_call(
result_var: &str,
method_name: &str,
args: Option<&serde_json::Value>,
class_name: &str,
) -> String {
match method_name {
"root_child_count" => format!("{result_var}.RootNode.ChildCount"),
"root_node_type" => format!("{result_var}.RootNode.Kind"),
"named_children_count" => format!("{result_var}.RootNode.NamedChildCount"),
"has_error_nodes" => format!("{class_name}.TreeHasErrorNodes({result_var})"),
"error_count" | "tree_error_count" => format!("{class_name}.TreeErrorCount({result_var})"),
"tree_to_sexp" => format!("{class_name}.TreeToSexp({result_var})"),
"contains_node_type" => {
let node_type = args
.and_then(|a| a.get("node_type"))
.and_then(|v| v.as_str())
.unwrap_or("");
format!("{class_name}.TreeContainsNodeType({result_var}, \"{node_type}\")")
}
"find_nodes_by_type" => {
let node_type = args
.and_then(|a| a.get("node_type"))
.and_then(|v| v.as_str())
.unwrap_or("");
format!("{class_name}.FindNodesByType({result_var}, \"{node_type}\")")
}
"run_query" => {
let query_source = args
.and_then(|a| a.get("query_source"))
.and_then(|v| v.as_str())
.unwrap_or("");
let language = args
.and_then(|a| a.get("language"))
.and_then(|v| v.as_str())
.unwrap_or("");
format!("{class_name}.RunQuery({result_var}, \"{language}\", \"{query_source}\", source)")
}
_ => {
use heck::ToUpperCamelCase;
let pascal = method_name.to_upper_camel_case();
format!("{result_var}.{pascal}()")
}
}
}
fn fixture_has_csharp_callable(fixture: &Fixture, e2e_config: &E2eConfig) -> bool {
if fixture.is_http_test() {
return false;
}
let call_config = e2e_config.resolve_call_for_fixture(
fixture.call.as_deref(),
&fixture.id,
&fixture.resolved_category(),
&fixture.tags,
&fixture.input,
);
let cs_override = call_config
.overrides
.get("csharp")
.or_else(|| e2e_config.call.overrides.get("csharp"));
if cs_override.and_then(|o| o.client_factory.as_deref()).is_some() {
return true;
}
cs_override.and_then(|o| o.function.as_deref()).is_some() || !call_config.function.is_empty()
}
pub(super) fn classify_bytes_value_csharp(s: &str) -> String {
if let Some(first) = s.chars().next() {
if first.is_ascii_alphanumeric() || first == '_' {
if let Some(slash_pos) = s.find('/') {
if slash_pos > 0 {
let after_slash = &s[slash_pos + 1..];
if after_slash.contains('.') && !after_slash.is_empty() {
return format!("System.IO.File.ReadAllBytes(\"{}\")", s);
}
}
}
}
}
if s.starts_with('<') || s.starts_with('{') || s.starts_with('[') || s.contains(' ') {
return format!("System.Text.Encoding.UTF8.GetBytes(\"{}\")", escape_csharp(s));
}
format!("System.Convert.FromBase64String(\"{}\")", s)
}
mod stubs;
pub use stubs::emit_test_backend;
#[cfg(test)]
mod tests;