#[cfg(test)]
mod not_empty_tests {
use super::super::assertions::render_assertion;
use crate::e2e::config::E2eConfig;
use crate::e2e::field_access::FieldResolver;
use crate::e2e::fixture::Assertion;
use std::collections::{HashMap, HashSet};
fn render_not_empty(field: Option<&str>, result_is_simple: bool) -> String {
let resolver = FieldResolver::new(
&HashMap::new(),
&HashSet::new(),
&HashSet::new(),
&HashSet::new(),
&HashSet::new(),
);
let assertion = Assertion {
assertion_type: "not_empty".to_string(),
field: field.map(str::to_string),
..Default::default()
};
let mut out = String::new();
render_assertion(
&mut out,
&assertion,
"result",
&resolver,
result_is_simple,
&E2eConfig::default(),
&HashSet::new(),
&HashMap::new(),
);
out
}
#[test]
fn not_empty_for_ruby_asks_the_value_not_its_string_form() {
let out = render_not_empty(None, false);
assert!(!out.contains(".to_s"), "got: {out}");
assert_eq!(
out.trim(),
"expect(result.respond_to?(:empty?) ? !result.empty? : !result.nil?).to be(true)"
);
}
#[test]
fn not_empty_for_ruby_simple_results_asks_the_value_not_its_string_form() {
let out = render_not_empty(Some("audio"), true);
assert!(!out.contains(".to_s"), "got: {out}");
assert_eq!(
out.trim(),
"expect(result.respond_to?(:empty?) ? !result.empty? : !result.nil?).to be(true)"
);
}
}
#[cfg(test)]
mod chunk_heading_context_tests {
use super::super::assertions::render_assertion;
use crate::e2e::config::E2eConfig;
use crate::e2e::field_access::FieldResolver;
use crate::e2e::fixture::Assertion;
use std::collections::{HashMap, HashSet};
fn render(field: &str, assertion_type: &str, value: Option<serde_json::Value>) -> String {
let resolver = FieldResolver::new(
&HashMap::new(),
&HashSet::new(),
&HashSet::new(),
&HashSet::new(),
&HashSet::new(),
);
let assertion = Assertion {
assertion_type: assertion_type.to_string(),
field: Some(field.to_string()),
value,
..Default::default()
};
let mut out = String::new();
render_assertion(
&mut out,
&assertion,
"result",
&resolver,
false,
&E2eConfig::default(),
&HashSet::new(),
&HashMap::new(),
);
out
}
#[test]
fn chunks_have_heading_context_is_asserted_not_skipped() {
let out = render("chunks_have_heading_context", "is_true", None);
assert!(!out.contains("skipped"), "got: {out}");
assert_eq!(
out.trim(),
"expect((result.chunks || []).all? { |c| c.metadata && !c.metadata.heading_context.nil? }).to be(true)"
);
}
#[test]
fn chunks_have_heading_context_is_false_is_asserted_not_skipped() {
let out = render("chunks_have_heading_context", "is_false", None);
assert!(!out.contains("skipped"), "got: {out}");
assert_eq!(
out.trim(),
"expect((result.chunks || []).all? { |c| c.metadata && !c.metadata.heading_context.nil? }).to be(false)"
);
}
#[test]
fn first_chunk_starts_with_heading_is_asserted_not_skipped() {
let out = render("first_chunk_starts_with_heading", "is_true", None);
assert!(!out.contains("skipped"), "got: {out}");
assert_eq!(
out.trim(),
"expect(!(result.chunks || []).first&.metadata&.heading_context.nil?).to be(true)"
);
}
#[test]
fn enum_variant_accessor_still_skips_because_ruby_serializes_it_to_a_hash() {
let out = render("metadata.format.excel", "equals", Some(serde_json::json!("Excel")));
assert!(out.contains("# skipped:"), "got: {out}");
assert!(
out.contains("enum variant accessor 'metadata.format.excel' not available on Ruby (serialized to Hash)"),
"got: {out}"
);
}
}
#[cfg(test)]
mod trait_bridge_tests {
use super::super::project::render_spec_helper;
use super::super::stubs::emit_test_backend;
use crate::core::config::TraitBridgeConfig;
use crate::core::ir::{MethodDef, ParamDef, TypeRef};
use crate::e2e::fixture::Fixture;
use std::collections::HashMap;
fn make_fixture(id: &str) -> Fixture {
Fixture {
docs: None,
requirements: Vec::new(),
id: id.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,
asyncapi: None,
websocket: None,
preserve_input_urls: false,
assertions: vec![],
visitor: None,
args: vec![],
assertion_recipes: vec![],
}
}
fn make_param(name: &str, ty: TypeRef) -> ParamDef {
ParamDef {
name: name.to_string(),
ty,
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,
}
}
fn make_method(name: &str, params: Vec<(&str, TypeRef)>, ret: TypeRef, is_async: bool) -> MethodDef {
MethodDef {
name: name.to_string(),
params: params.into_iter().map(|(n, ty)| make_param(n, ty)).collect(),
return_type: ret,
is_async,
is_static: false,
error_type: None,
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(),
}
}
#[test]
fn spec_helper_stays_generic_for_library_specific_setup() {
let content = render_spec_helper(
true,
false,
false,
"../../fixtures",
"custom_gem",
"custom_module",
"127.0.0.1",
8000,
&HashMap::new(),
);
assert!(
!content.contains("require 'custom_gem'"),
"spec helper must not require the generated gem directly:\n{content}"
);
assert!(
!content.contains("CustomModule") && !content.contains("SampleCrate") && !content.contains("sample_crate"),
"spec helper must avoid library-specific module cleanup:\n{content}"
);
}
#[test]
fn test_backend_emission_is_generic() {
let trait_bridge = TraitBridgeConfig {
trait_name: "TestTrait".to_string(),
super_trait: Some("SomeSuperTrait".to_string()),
register_fn: Some("register_test_trait".to_string()),
..TraitBridgeConfig::default()
};
let do_thing = make_method(
"do_thing",
vec![("x", TypeRef::Primitive(crate::core::ir::PrimitiveType::I32))],
TypeRef::String,
false,
);
let fixture = make_fixture("my_test_fixture");
let methods = vec![&do_thing];
let emission = emit_test_backend(&trait_bridge, &methods, &fixture);
assert!(
!emission.setup_block.contains("OcrBackend"),
"setup_block must not hardcode domain trait names, got:\n{}",
emission.setup_block
);
assert!(
!emission.setup_block.contains("process_image"),
"setup_block must not hardcode domain method names, got:\n{}",
emission.setup_block
);
assert!(
emission.setup_block.contains("do_thing"),
"setup_block must contain the method name 'do_thing', got:\n{}",
emission.setup_block
);
assert!(
emission.setup_block.contains("name"),
"setup_block must emit 'name' for super_trait, got:\n{}",
emission.setup_block
);
assert!(
emission.arg_expr.contains("my_test_fixture"),
"arg_expr must reference fixture id, got: {}",
emission.arg_expr
);
}
#[test]
fn test_backend_named_return_emits_json_string() {
let trait_bridge = TraitBridgeConfig {
trait_name: "DocumentExtractor".to_string(),
super_trait: Some("Plugin".to_string()),
register_fn: Some("register_document_extractor".to_string()),
..TraitBridgeConfig::default()
};
let extract_bytes = make_method(
"extract_bytes",
vec![("content", TypeRef::Bytes), ("mime_type", TypeRef::String)],
TypeRef::Named("HiddenRecord".to_string()),
false,
);
let fixture = make_fixture("register_document_extractor_trait_bridge");
let methods = vec![&extract_bytes];
let emission = emit_test_backend(&trait_bridge, &methods, &fixture);
assert!(
emission.setup_block.contains("'{}'"),
"Named return type must emit '{{}}' not a constructor call, got:\n{}",
emission.setup_block
);
assert!(
!emission.setup_block.contains("HiddenRecord.new"),
"setup_block must not reference undefined constant HiddenRecord, got:\n{}",
emission.setup_block
);
}
#[test]
fn test_backend_name_from_input() {
let trait_bridge = TraitBridgeConfig {
trait_name: "DocumentExtractor".to_string(),
super_trait: Some("Plugin".to_string()),
register_fn: Some("register_document_extractor".to_string()),
..TraitBridgeConfig::default()
};
let extract_bytes = make_method(
"extract_bytes",
vec![("content", TypeRef::Bytes)],
TypeRef::Named("HiddenRecord".to_string()),
false,
);
let mut fixture = make_fixture("register_document_extractor_trait_bridge");
fixture.input = serde_json::json!({
"extractor": { "type": "test", "name": "test-extractor" }
});
let methods = vec![&extract_bytes];
let emission = emit_test_backend(&trait_bridge, &methods, &fixture);
assert!(
emission.setup_block.contains("test-extractor"),
"setup_block must use input-derived name 'test-extractor', got:\n{}",
emission.setup_block
);
assert!(
!emission
.setup_block
.contains("= 'register_document_extractor_trait_bridge'"),
"name() method must not return fixture id, got:\n{}",
emission.setup_block
);
}
#[test]
fn test_backend_snapshot() {
let trait_bridge = TraitBridgeConfig {
trait_name: "DocumentExtractor".to_string(),
super_trait: Some("Plugin".to_string()),
register_fn: Some("register_document_extractor".to_string()),
..TraitBridgeConfig::default()
};
let extract_bytes = make_method(
"extract_bytes",
vec![
("content", TypeRef::Bytes),
("mime_type", TypeRef::String),
("config", TypeRef::Named("ExtractionConfig".to_string())),
],
TypeRef::Named("HiddenRecord".to_string()),
false,
);
let mut fixture = make_fixture("register_document_extractor_trait_bridge");
fixture.input = serde_json::json!({
"extractor": { "type": "test", "name": "test-extractor" }
});
let methods = vec![&extract_bytes];
let emission = emit_test_backend(&trait_bridge, &methods, &fixture);
let expected_setup = concat!(
"stub_register_document_extractor_trait_bridge = Class.new do\n",
" def name = 'test-extractor'\n",
" def initialize\n",
" nil\n",
" end\n",
" def shutdown\n",
" nil\n",
" end\n",
" def version = '1.0.0'\n",
" def extract_bytes(content, mime_type, config) = '{}'\n",
"end.new\n",
);
assert_eq!(emission.setup_block, expected_setup, "setup_block snapshot mismatch");
assert_eq!(emission.arg_expr, "stub_register_document_extractor_trait_bridge");
}
}
#[cfg(test)]
mod app_harness_tests {
use super::super::project::render_app_harness;
#[test]
fn app_harness_rb_contains_eaddrinuse_retry_block() {
use crate::core::config::e2e::{E2eConfig, HarnessConfig};
use crate::e2e::fixture::{Fixture, FixtureGroup, HttpExpectedResponse, HttpFixture, HttpHandler, HttpRequest};
use std::collections::BTreeMap;
let fixture = Fixture {
docs: None,
requirements: Vec::new(),
id: "test_get".to_owned(),
description: "test fixture".to_owned(),
category: Some("smoke".to_owned()),
tags: vec![],
skip: None,
env: None,
setup: Vec::new(),
call: None,
input: serde_json::Value::Null,
mock_response: None,
visitor: None,
args: vec![],
assertion_recipes: vec![],
assertions: vec![],
source: "test".to_owned(),
http: Some(HttpFixture {
handler: HttpHandler {
route: "/test".to_owned(),
method: "GET".to_owned(),
body_schema: None,
parameters: BTreeMap::new(),
middleware: None,
},
request: HttpRequest {
method: "GET".to_owned(),
path: "/test".to_owned(),
headers: BTreeMap::new(),
query_params: BTreeMap::new(),
cookies: BTreeMap::new(),
body: None,
form_data: None,
content_type: None,
},
expected_response: HttpExpectedResponse {
status_code: 200,
body: Some(serde_json::json!({"ok": true})),
body_partial: None,
headers: BTreeMap::new(),
validation_errors: None,
},
}),
asyncapi: None,
websocket: None,
preserve_input_urls: false,
};
let groups = vec![FixtureGroup {
category: "smoke".to_owned(),
fixtures: vec![fixture],
}];
let e2e_config = E2eConfig {
harness: HarnessConfig {
imports: vec!["my_gem".to_owned()],
..HarnessConfig::default()
},
..E2eConfig::default()
};
let out = render_app_harness(&e2e_config, &groups);
assert!(
out.contains("Errno::EADDRINUSE"),
"expected `Errno::EADDRINUSE` retry block in generated app_harness.rb:\n{out}"
);
assert!(
out.contains("rand(40000..60000)") || out.contains("rand("),
"expected random port selection in generated app_harness.rb:\n{out}"
);
assert!(
out.contains("HARNESS_PORT="),
"expected `HARNESS_PORT=` output in generated app_harness.rb:\n{out}"
);
}
}
#[cfg(test)]
mod env_setup_tests {
use super::super::project::render_env_setup;
use std::collections::HashMap;
#[test]
fn empty_env_produces_no_setup_block() {
let env = HashMap::new();
let output = render_env_setup(&env);
assert_eq!(output, "", "empty env must produce empty string");
}
#[test]
fn non_empty_env_produces_sorted_lines() {
let mut env = HashMap::new();
env.insert("E2E_ALLOW_PRIVATE_NETWORK".to_string(), "true".to_string());
env.insert("FOO".to_string(), "bar".to_string());
env.insert("BAZ".to_string(), "qux".to_string());
let output = render_env_setup(&env);
let lines: Vec<&str> = output.lines().collect();
assert_eq!(lines.len(), 3, "expected 3 lines, got: {output}");
assert!(
lines[0].contains("BAZ"),
"first line should be BAZ (alphabetically first), got: {}",
lines[0]
);
assert!(
lines[1].contains("E2E_ALLOW_PRIVATE_NETWORK"),
"second line should be E2E_ALLOW_PRIVATE_NETWORK, got: {}",
lines[1]
);
assert!(lines[2].contains("FOO"), "third line should be FOO, got: {}", lines[2]);
for line in lines {
assert!(line.contains("||="), "line must use ||= operator: {line}");
}
}
}
#[cfg(test)]
mod error_path_marker_tests {
use crate::core::config::ResolvedCrateConfig;
use crate::e2e::config::E2eConfig;
use crate::e2e::fixture::{Assertion, Fixture};
use std::collections::HashMap;
fn render(extra: Vec<Assertion>) -> String {
let mut assertions = vec![Assertion {
assertion_type: "error".to_string(),
..Default::default()
}];
assertions.extend(extra);
let fixture = Fixture {
id: "rate_limited".to_string(),
description: "Rejects the request".to_string(),
assertions,
..Fixture::default()
};
let mut e2e_config = E2eConfig::default();
e2e_config.call.function = "parse".to_string();
e2e_config.call.result_var = "result".to_string();
let enum_fields: HashMap<String, String> = HashMap::new();
let _ = crate::e2e::codegen::take_skip_records();
super::super::spec_file::render_spec_file(
"error",
&[&fixture],
"Sample",
None,
"sample",
None,
&enum_fields,
false,
&e2e_config,
false,
false,
&[],
&ResolvedCrateConfig::default(),
&[],
&[],
)
}
#[test]
fn ruby_equals_on_an_error_field_is_named_instead_of_dropped() {
let out = render(vec![Assertion {
assertion_type: "equals".to_string(),
field: Some("error.status_code".to_string()),
..Default::default()
}]);
assert!(
out.contains("raise_error(RuntimeError)"),
"the error block must render:\n{out}"
);
assert!(
out.contains(
"# skipped: assertion type 'equals' has no accessor for error field error.status_code in this backend"
),
"got:\n{out}"
);
let records = crate::e2e::codegen::take_skip_records();
assert_eq!(records.len(), 1, "got: {records:?}");
assert_eq!(records[0].language, "ruby");
assert_eq!(records[0].field, "equals");
}
#[test]
fn ruby_a_lone_error_assertion_renders_no_marker() {
let out = render(Vec::new());
assert!(
out.contains("raise_error(RuntimeError)"),
"the error block must render:\n{out}"
);
assert!(!out.contains("has no accessor for error field"), "got:\n{out}");
}
}