use std::collections::{HashMap, HashSet};
use crate::core::config::ResolvedCrateConfig;
use crate::e2e::config::E2eConfig;
use crate::e2e::escape::{ruby_string_literal, sanitize_ident};
use crate::e2e::field_access::FieldResolver;
use crate::e2e::fixture::{Assertion, Fixture};
use super::args::build_args_and_setup;
use super::assertions::render_assertion;
use super::spec_file::has_usable_assertion;
use super::values::json_to_ruby;
use super::visitor::build_ruby_visitor;
#[allow(clippy::too_many_arguments)]
pub(super) fn render_chat_stream_example(
fixture: &Fixture,
function_name: &str,
call_receiver: &str,
module_name: &str,
args: &[crate::e2e::config::ArgMapping],
options_type: Option<&str>,
enum_fields: &HashMap<String, String>,
e2e_config: &E2eConfig,
client_factory: Option<&str>,
extra_args: &[String],
adapter_request_type: Option<&str>,
streaming_item_type: Option<&str>,
config: &ResolvedCrateConfig,
type_defs: &[crate::core::ir::TypeDef],
) -> String {
let test_name = sanitize_ident(&fixture.id);
let description = fixture.description.clone();
let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
let fixture_id = fixture.id.clone();
let (mut setup_lines, args_str, mut teardown_lines) = build_args_and_setup(
&fixture.input,
args,
call_receiver,
module_name,
options_type,
enum_fields,
false,
fixture,
adapter_request_type,
config,
type_defs,
);
for setup_call in &fixture.setup {
let setup_call_config = e2e_config.resolve_call_for_fixture(
Some(&setup_call.call),
&fixture.id,
&fixture.resolved_category(),
&fixture.tags,
&setup_call.input,
);
let setup_args = &setup_call_config.args;
let (setup_setup_lines, setup_args_str, setup_teardown_lines) = build_args_and_setup(
&setup_call.input,
setup_args,
call_receiver,
module_name,
None,
enum_fields,
false,
fixture,
None,
config,
type_defs,
);
for line in setup_setup_lines {
setup_lines.push(line);
}
let setup_fn = &setup_call_config.function;
let setup_call_expr = if setup_args_str.is_empty() {
format!("{}.{}()", call_receiver, setup_fn)
} else {
format!("{}.{}({})", call_receiver, setup_fn, setup_args_str)
};
setup_lines.push(setup_call_expr);
for line in setup_teardown_lines {
teardown_lines.push(line);
}
}
let mut final_args = args_str;
if !extra_args.is_empty() {
let extra_str = extra_args.join(", ");
if final_args.is_empty() {
final_args = extra_str;
} else {
final_args = format!("{final_args}, {extra_str}");
}
}
let mut out = String::new();
let description_literal = ruby_string_literal(&format!("{test_name}: {description}"));
out.push_str(&format!(" it {description_literal} do\n"));
let has_mock = fixture.mock_response.is_some() || fixture.http.is_some();
let api_key_var = fixture.env.as_ref().and_then(|e| e.api_key_var.as_deref());
if let Some(cf) = client_factory {
if has_mock && let Some(key_var) = api_key_var {
let mock_url_expr = format!("\"#{{ENV['MOCK_SERVER_URL']}}/fixtures/{fixture_id}\"");
out.push_str(&format!(" api_key = ENV['{key_var}']\n"));
out.push_str(" if api_key && !api_key.empty?\n");
out.push_str(&format!(
" warn \"{test_name}: using real API ({key_var} is set)\"\n"
));
out.push_str(&format!(" client = {call_receiver}.{cf}(api_key)\n"));
out.push_str(" else\n");
out.push_str(&format!(
" warn \"{test_name}: using mock server ({key_var} not set)\"\n"
));
out.push_str(&format!(" mock_url = {mock_url_expr}\n"));
out.push_str(&format!(" client = {call_receiver}.{cf}('test-key', mock_url)\n"));
out.push_str(" end\n");
} else if has_mock {
let base_url_expr = if fixture.has_host_root_route() {
let env_key = format!("MOCK_SERVER_{}", fixture_id.to_uppercase());
format!("(ENV.fetch('{env_key}', nil) || ENV.fetch('MOCK_SERVER_URL') + '/fixtures/{fixture_id}')")
} else {
format!("ENV.fetch('MOCK_SERVER_URL') + '/fixtures/{fixture_id}'")
};
out.push_str(&format!(
" client = {call_receiver}.{cf}('test-key', {base_url_expr})\n"
));
} else if let Some(key_var) = api_key_var {
out.push_str(&format!(" api_key = ENV['{key_var}']\n"));
out.push_str(&format!(" skip '{key_var} not set' unless api_key\n"));
out.push_str(&format!(" client = {call_receiver}.{cf}(api_key)\n"));
} else {
out.push_str(&format!(" client = {call_receiver}.{cf}('test-key')\n"));
}
}
if let Some(visitor_spec) = &fixture.visitor {
let _ = build_ruby_visitor(&mut setup_lines, visitor_spec);
}
for line in &setup_lines {
out.push_str(&format!(" {line}\n"));
}
let call_expr = if client_factory.is_some() {
format!("client.{function_name}({final_args})")
} else {
format!("{call_receiver}.{function_name}({final_args})")
};
if expects_error {
out.push_str(&format!(" expect {{ {call_expr} {{ |_chunk| }} }}.to raise_error\n"));
out.push_str(" end\n");
return out;
}
out.push_str(" chunks = []\n");
out.push_str(" stream_complete = false\n");
out.push_str(&format!(" {call_expr} do |chunk|\n"));
out.push_str(" chunks << chunk\n");
out.push_str(" end\n");
out.push_str(" stream_complete = true\n");
for assertion in &fixture.assertions {
emit_chat_stream_assertion(&mut out, assertion, e2e_config, streaming_item_type);
}
if !fixture
.assertions
.iter()
.any(|a| a.field.as_deref() == Some("stream_complete"))
{
out.push_str(" expect(stream_complete).to be(true)\n");
}
for line in &teardown_lines {
out.push_str(&format!(" {line}\n"));
}
out.push_str(" end\n");
out
}
pub(super) fn emit_chat_stream_assertion(
out: &mut String,
assertion: &Assertion,
_e2e_config: &E2eConfig,
streaming_item_type: Option<&str>,
) {
let atype = assertion.assertion_type.as_str();
if atype == "not_error" || atype == "error" {
return;
}
let field = assertion.field.as_deref().unwrap_or("");
enum Kind {
Chunks,
Bool,
Str,
IntTokens,
Json,
Unsupported,
}
let expr_opt = crate::e2e::codegen::streaming_assertions::StreamingFieldResolver::accessor_with_streaming_context(
field,
"ruby",
"chunks",
None,
streaming_item_type,
);
let (expr, kind) = match (field, expr_opt) {
("chunks", Some(expr)) => (expr, Kind::Chunks),
("chunks.length", Some(expr)) => (expr, Kind::Chunks),
("stream_content", Some(expr)) => (expr, Kind::Str),
("finish_reason", Some(expr)) => (expr, Kind::Str),
("tool_calls", Some(expr)) => (expr, Kind::Json),
("tool_calls[0].function.name", Some(expr)) => (expr, Kind::Str),
("usage.total_tokens", Some(expr)) => (expr, Kind::IntTokens),
("stream_complete", None) => ("stream_complete".to_string(), Kind::Bool),
("no_chunks_after_done", None) => ("stream_complete".to_string(), Kind::Bool),
_ => ("".to_string(), Kind::Unsupported),
};
if matches!(kind, Kind::Unsupported) {
out.push_str(&format!(
" # skipped: streaming assertion on unsupported field '{field}'\n"
));
return;
}
match (atype, &kind) {
("count_min", Kind::Chunks) => {
if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
out.push_str(&format!(" expect({expr}.length).to be >= {n}\n"));
}
}
("count_equals", Kind::Chunks) => {
if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
out.push_str(&format!(" expect({expr}.length).to eq({n})\n"));
}
}
("equals", Kind::Str) => {
if let Some(val) = &assertion.value {
let rb_val = json_to_ruby(val);
out.push_str(&format!(" expect({expr}.to_s.strip).to eq({rb_val}.strip)\n"));
}
}
("contains", Kind::Str) => {
if let Some(val) = &assertion.value {
let rb_val = json_to_ruby(val);
out.push_str(&format!(" expect({expr}.to_s).to include({rb_val})\n"));
}
}
("not_empty", Kind::Str) => {
out.push_str(&format!(" expect({expr}.to_s).not_to be_empty\n"));
}
("not_empty", Kind::Json) => {
out.push_str(&format!(" expect({expr}).not_to be_nil\n"));
}
("is_empty", Kind::Str) => {
out.push_str(&format!(" expect({expr}.to_s).to be_empty\n"));
}
("is_true", Kind::Bool) => {
out.push_str(&format!(" expect({expr}).to be(true)\n"));
}
("is_false", Kind::Bool) => {
out.push_str(&format!(" expect({expr}).to be(false)\n"));
}
("greater_than_or_equal", Kind::IntTokens) => {
if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
out.push_str(&format!(" expect({expr}).to be >= {n}\n"));
}
}
("equals", Kind::IntTokens) => {
if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
out.push_str(&format!(" expect({expr}).to eq({n})\n"));
}
}
_ => {
out.push_str(&format!(
" # skipped: streaming assertion '{atype}' on field '{field}' not supported\n"
));
}
}
}
#[allow(clippy::too_many_arguments)]
pub(super) fn render_example(
fixture: &Fixture,
function_name: &str,
call_receiver: &str,
module_name: &str,
result_var: &str,
args: &[crate::e2e::config::ArgMapping],
field_resolver: &FieldResolver,
options_type: Option<&str>,
enum_fields: &HashMap<String, String>,
fields_enum: &HashSet<String>,
result_is_simple: bool,
returns_void: bool,
e2e_config: &E2eConfig,
client_factory: Option<&str>,
extra_args: &[String],
adapter_request_type: Option<&str>,
config: &ResolvedCrateConfig,
type_defs: &[crate::core::ir::TypeDef],
) -> String {
let test_name = sanitize_ident(&fixture.id);
let description_literal = ruby_string_literal(&format!("{test_name}: {}", fixture.description));
let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
let fixture_id = fixture.id.clone();
let (mut setup_lines, args_str, teardown_lines) = build_args_and_setup(
&fixture.input,
args,
call_receiver,
module_name,
options_type,
enum_fields,
result_is_simple,
fixture,
adapter_request_type,
config,
type_defs,
);
let mut visitor_arg = String::new();
if let Some(visitor_spec) = &fixture.visitor {
visitor_arg = build_ruby_visitor(&mut setup_lines, visitor_spec);
}
let mut final_args = if visitor_arg.is_empty() {
args_str
} else if args_str.is_empty() {
visitor_arg
} else {
format!("{args_str}, {visitor_arg}")
};
if !extra_args.is_empty() {
let extra_str = extra_args.join(", ");
if final_args.is_empty() {
final_args = extra_str;
} else {
final_args = format!("{final_args}, {extra_str}");
}
}
let call_expr = if client_factory.is_some() {
format!("client.{function_name}({final_args})")
} else {
format!("{call_receiver}.{function_name}({final_args})")
};
let has_usable = has_usable_assertion(fixture, field_resolver, result_is_simple);
let mut assertions_rendered = String::new();
for assertion in &fixture.assertions {
render_assertion(
&mut assertions_rendered,
assertion,
result_var,
field_resolver,
result_is_simple,
e2e_config,
fields_enum,
enum_fields,
);
}
let has_mock = fixture.mock_response.is_some() || fixture.http.is_some();
let api_key_var = fixture.env.as_ref().and_then(|e| e.api_key_var.as_deref());
let has_mock_and_key = has_mock && api_key_var.is_some();
let has_not_error = fixture.assertions.iter().any(|a| a.assertion_type == "not_error");
let is_only_not_error = has_not_error && !has_usable && !expects_error;
let is_clear_op = function_name.ends_with("_clear");
let post_clear_list_call = if is_clear_op {
let list_fn = function_name.replace("_clear", "_list");
format!("{}.{}()", call_receiver, list_fn)
} else {
String::new()
};
crate::e2e::template_env::render(
"ruby/test_function.jinja",
minijinja::context! {
test_name => test_name,
description => description_literal,
expects_error => expects_error,
setup_lines => setup_lines,
call_expr => call_expr,
result_var => result_var,
assertions_rendered => assertions_rendered,
has_usable => has_usable,
returns_void => returns_void,
client_factory => client_factory,
fixture_id => fixture_id,
call_receiver => call_receiver,
has_mock => has_mock,
api_key_var => api_key_var,
has_mock_and_key => has_mock_and_key,
is_only_not_error => is_only_not_error,
is_clear_op => is_clear_op,
post_clear_list_call => post_clear_list_call,
teardown_lines => teardown_lines,
},
)
}