use std::fmt::Write as FmtWrite;
use crate::e2e::codegen::field_skip::FieldSkip;
use crate::e2e::config::E2eConfig;
use crate::e2e::field_access::FieldResolver;
use crate::e2e::fixture::{Assertion, Fixture};
use super::super::assertions::render_assertion;
use super::super::helpers::{resolve_assert_enum_fields, strip_redundant_call_arg_parens};
use super::super::json::value_to_python_string;
fn has_real_assertion(body: &str) -> bool {
body.lines().any(|line| {
let trimmed = line.trim();
!trimmed.is_empty() && !trimmed.starts_with('#')
})
}
fn apply_vacuous_assertion_fallback(
temp_assertions: &mut String,
has_declared_assertions: bool,
result_var: &str,
returns_void: bool,
) {
if !has_declared_assertions || has_real_assertion(temp_assertions) {
return;
}
if returns_void {
return;
}
let _ = writeln!(temp_assertions, " assert {result_var} is not None");
}
#[allow(clippy::too_many_arguments)]
pub(super) fn emit_result_and_assertions(
out: &mut String,
fixture: &Fixture,
e2e_config: &E2eConfig,
call_config: &crate::e2e::config::CallConfig,
call_expr: &str,
result_var: &str,
field_resolver: &FieldResolver,
result_is_simple: bool,
is_streaming: bool,
force_bind_result: bool,
) {
let chunks_var = "chunks";
let fields_enum = e2e_config.effective_fields_enum(call_config);
let assert_enum_fields = resolve_assert_enum_fields(call_config);
if is_streaming {
let _ = writeln!(out, " {result_var} = {call_expr}");
if let Some(collect) = crate::e2e::codegen::streaming_assertions::StreamingFieldResolver::collect_snippet(
"python", result_var, chunks_var,
) {
let _ = writeln!(out, " {collect}");
}
let mut streaming_assertions = String::new();
for assertion in &fixture.assertions {
if assertion.assertion_type == "not_error" || assertion.assertion_type == "error" {
continue;
}
if let Some(f) = &assertion.field
&& crate::e2e::codegen::streaming_assertions::is_streaming_virtual_field(f)
{
emit_streaming_virtual_assertion(&mut streaming_assertions, assertion, f, chunks_var);
continue;
}
if let Some(f) = assertion.field.as_deref().filter(|f| !f.is_empty()) {
let _ = writeln!(
streaming_assertions,
" # skipped: {}",
FieldSkip::NotAvailableOnStreamingResultType.message(f)
);
}
}
apply_vacuous_assertion_fallback(
&mut streaming_assertions,
!fixture.assertions.is_empty(),
chunks_var,
call_config.returns_void,
);
crate::e2e::codegen::fail_on_unavailable_field_markers(
&streaming_assertions,
"python",
&fixture.id,
&fixture.assertions,
);
crate::e2e::codegen::fail_on_unsupported_assertion_type_markers(&streaming_assertions, "python", &fixture.id);
out.push_str(&streaming_assertions);
} else {
let mut temp_assertions = String::new();
for assertion in &fixture.assertions {
if assertion.assertion_type == "not_error" {
continue;
}
render_assertion(
&mut temp_assertions,
assertion,
result_var,
field_resolver,
fields_enum,
assert_enum_fields,
result_is_simple,
);
}
apply_vacuous_assertion_fallback(
&mut temp_assertions,
!fixture.assertions.is_empty(),
result_var,
call_config.returns_void,
);
crate::e2e::codegen::fail_on_unavailable_field_markers(
&temp_assertions,
"python",
&fixture.id,
&fixture.assertions,
);
crate::e2e::codegen::fail_on_unsupported_assertion_type_markers(&temp_assertions, "python", &fixture.id);
let result_var_used = temp_assertions.lines().any(|line| {
let trimmed = line.trim();
!trimmed.starts_with('#') && trimmed.contains(result_var)
});
let result_binding =
(result_var_used || fixture.has_docs_presentation() || force_bind_result).then_some(result_var);
out.push_str(&crate::e2e::template_env::render(
"python/call_statement.py.jinja",
minijinja::context! { result_binding => result_binding, call_expr => call_expr },
));
out.push_str(&temp_assertions);
}
}
fn emit_streaming_virtual_assertion(out: &mut String, assertion: &Assertion, field: &str, chunks_var: &str) {
use crate::e2e::codegen::streaming_assertions::StreamingFieldResolver;
let Some(expr) = StreamingFieldResolver::accessor(field, "python", chunks_var) else {
let _ = writeln!(
out,
" # skipped: {}",
FieldSkip::NoPythonStreamingAccessor.message(field)
);
return;
};
let expr_arg = strip_redundant_call_arg_parens(&expr);
match assertion.assertion_type.as_str() {
"count_min" => {
if let Some(val) = &assertion.value
&& let Some(n) = val.as_u64()
{
let _ = writeln!(out, " assert len({expr_arg}) >= {n}");
}
}
"count_equals" => {
if let Some(val) = &assertion.value
&& let Some(n) = val.as_u64()
{
let _ = writeln!(out, " assert len({expr_arg}) == {n}");
}
}
"equals" => {
if let Some(val) = &assertion.value {
let expected = value_to_python_string(val);
let op = if val.is_boolean() || val.is_null() { "is" } else { "==" };
if val.is_string() {
let _ = writeln!(out, " assert {expr}.strip() {op} {expected}.strip()");
} else {
let _ = writeln!(out, " assert {expr} {op} {expected}");
}
}
}
"not_empty" => {
let _ = writeln!(
out,
" assert {expr} is not None and (not hasattr({expr_arg}, \"__len__\") or len({expr_arg}) > 0)"
);
}
"is_empty" => {
let _ = writeln!(out, " assert not {expr}");
}
"is_true" => {
let py_expr = if expr == "true" {
"True".to_string()
} else if expr == "false" {
"False".to_string()
} else {
expr.clone()
};
let _ = writeln!(out, " assert {py_expr}");
}
"is_false" => {
let py_expr = if expr == "true" {
"True".to_string()
} else if expr == "false" {
"False".to_string()
} else {
expr.clone()
};
let _ = writeln!(out, " assert not {py_expr}");
}
"greater_than" => {
if let Some(val) = &assertion.value {
let expected = value_to_python_string(val);
let _ = writeln!(out, " assert {expr} > {expected}");
}
}
"greater_than_or_equal" => {
if let Some(val) = &assertion.value {
let expected = value_to_python_string(val);
let _ = writeln!(out, " assert {expr} >= {expected}");
}
}
"contains" => {
if let Some(val) = &assertion.value {
let expected = value_to_python_string(val);
let _ = writeln!(out, " assert {expected} in {expr}");
}
}
other => {
panic!("Python e2e generator: unsupported assertion type '{other}' on synthetic field '{field}'");
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn assertion(assertion_type: &str, field: Option<&str>, value: Option<serde_json::Value>) -> Assertion {
Assertion {
skip: None,
assertion_type: assertion_type.to_string(),
field: field.map(str::to_string),
value,
values: None,
method: None,
check: None,
args: None,
return_type: None,
}
}
fn minimal_fixture() -> Fixture {
Fixture {
docs: None,
requirements: Vec::new(),
id: "widget_smoke".to_string(),
description: "Create a widget".to_string(),
input: serde_json::Value::Null,
http: None,
asyncapi: None,
websocket: None,
preserve_input_urls: false,
assertions: Vec::new(),
call: None,
skip: None,
env: None,
setup: Vec::new(),
visitor: None,
args: vec![],
assertion_recipes: vec![],
mock_response: None,
source: String::new(),
category: None,
tags: Vec::new(),
}
}
#[test]
fn streaming_virtual_assertion_renders_collected_chunks_access() {
let mut out = String::new();
let assertion = assertion("count_min", Some("chunks"), Some(serde_json::Value::from(1)));
emit_streaming_virtual_assertion(&mut out, &assertion, "chunks", "chunks");
assert!(out.contains("assert len(chunks) >= 1"), "got: {out}");
}
#[test]
fn not_empty_for_python_streaming_rejects_empty_chunks_but_accepts_zero() {
let mut out = String::new();
let assertion = assertion("not_empty", Some("chunks"), None);
emit_streaming_virtual_assertion(&mut out, &assertion, "chunks", "chunks");
assert_eq!(
out.trim(),
"assert chunks is not None and (not hasattr(chunks, \"__len__\") or len(chunks) > 0)"
);
}
#[test]
fn call_statement_omits_binding_when_the_result_is_unused() {
let rendered = crate::e2e::template_env::render(
"python/call_statement.py.jinja",
minijinja::context! { result_binding => Option::<&str>::None, call_expr => "await process(value)" },
);
assert_eq!(rendered, " await process(value)\n");
assert!(!rendered.contains("result ="));
assert!(!rendered.contains("_ ="));
}
#[test]
fn should_bind_result_when_force_bind_result_is_set_with_no_assertions() {
let fixture = minimal_fixture();
let e2e_config = E2eConfig::default();
let call_config = crate::e2e::config::CallConfig::default();
let field_resolver = FieldResolver::new(
&std::collections::HashMap::new(),
&std::collections::HashSet::new(),
&std::collections::HashSet::new(),
&std::collections::HashSet::new(),
&std::collections::HashSet::new(),
);
let mut out = String::new();
emit_result_and_assertions(
&mut out,
&fixture,
&e2e_config,
&call_config,
"await widget_client.create()",
"result",
&field_resolver,
false,
false,
true,
);
assert!(
out.contains("result = await widget_client.create()"),
"expected the call result to be bound so a caller can print it, got: {out}"
);
}
#[test]
fn should_discard_result_when_force_bind_result_is_unset_and_unused() {
let fixture = minimal_fixture();
let e2e_config = E2eConfig::default();
let call_config = crate::e2e::config::CallConfig::default();
let field_resolver = FieldResolver::new(
&std::collections::HashMap::new(),
&std::collections::HashSet::new(),
&std::collections::HashSet::new(),
&std::collections::HashSet::new(),
&std::collections::HashSet::new(),
);
let mut out = String::new();
emit_result_and_assertions(
&mut out,
&fixture,
&e2e_config,
&call_config,
"await widget_client.create()",
"result",
&field_resolver,
false,
false,
false,
);
assert!(!out.contains("result ="), "unused result must not be bound, got: {out}");
}
#[test]
fn has_real_assertion_is_false_for_comment_only_body() {
let body = " # skipped: field 'foo' not available on result type\n";
assert!(
!has_real_assertion(body),
"comment-only body must not count as asserting"
);
}
#[test]
fn has_real_assertion_is_true_when_a_real_statement_is_present() {
let body = " # skipped: field 'foo' not available on result type\n assert result.ok\n";
assert!(has_real_assertion(body), "a real assert line must count as asserting");
}
#[test]
fn vacuous_fallback_is_a_noop_without_declared_assertions() {
let mut body = String::new();
apply_vacuous_assertion_fallback(&mut body, false, "result", false);
assert!(
body.is_empty(),
"a fixture with no declared assertions is an intentional smoke test and must stay untouched"
);
}
#[test]
fn vacuous_fallback_emits_a_real_assertion_when_body_is_empty() {
let mut body = String::new();
apply_vacuous_assertion_fallback(&mut body, true, "result", false);
assert_eq!(body, " assert result is not None\n");
}
#[test]
fn vacuous_fallback_emits_a_real_assertion_over_comment_only_body() {
let mut body = " # skipped: field 'chunks' not available on result type\n".to_string();
apply_vacuous_assertion_fallback(&mut body, true, "result", false);
assert!(
body.contains("assert result is not None"),
"a comment-only body must still get a real fallback assertion, got: {body}"
);
}
#[test]
fn vacuous_fallback_leaves_a_real_assertion_untouched() {
let mut body = " assert result.count == 1\n".to_string();
let original = body.clone();
apply_vacuous_assertion_fallback(&mut body, true, "result", false);
assert_eq!(
body, original,
"a fixture with a real assertion must not get an extra fallback line"
);
}
#[test]
fn vacuous_fallback_emits_nothing_for_a_void_call() {
let mut body = String::new();
apply_vacuous_assertion_fallback(&mut body, true, "result", true);
assert!(
body.is_empty(),
"a void call's result is always None; asserting not-None would fail every successful \
call, got: {body}"
);
}
#[test]
fn not_error_only_fixture_binds_result_and_emits_real_assertion() {
let mut fixture = minimal_fixture();
fixture.assertions = vec![assertion("not_error", None, None)];
let e2e_config = E2eConfig::default();
let call_config = crate::e2e::config::CallConfig::default();
let field_resolver = FieldResolver::new(
&std::collections::HashMap::new(),
&std::collections::HashSet::new(),
&std::collections::HashSet::new(),
&std::collections::HashSet::new(),
&std::collections::HashSet::new(),
);
let mut out = String::new();
emit_result_and_assertions(
&mut out,
&fixture,
&e2e_config,
&call_config,
"await widget_client.create()",
"result",
&field_resolver,
false,
false,
false,
);
assert!(
out.contains("result = await widget_client.create()"),
"a not_error-only fixture must bind the result, got: {out}"
);
assert!(
out.contains("assert result is not None"),
"a not_error-only fixture must emit a real assertion instead of a vacuous body, got: {out}"
);
}
#[test]
fn void_not_error_fixture_emits_a_bare_unbound_call_not_a_guaranteed_failure() {
let mut fixture = minimal_fixture();
fixture.assertions = vec![assertion("not_error", None, None)];
let e2e_config = E2eConfig::default();
let call_config = crate::e2e::config::CallConfig {
returns_void: true,
..Default::default()
};
let field_resolver = FieldResolver::new(
&std::collections::HashMap::new(),
&std::collections::HashSet::new(),
&std::collections::HashSet::new(),
&std::collections::HashSet::new(),
&std::collections::HashSet::new(),
);
let mut out = String::new();
emit_result_and_assertions(
&mut out,
&fixture,
&e2e_config,
&call_config,
"await widget_client.prefetch()",
"result",
&field_resolver,
false,
false,
false,
);
assert!(
!out.contains("assert result is not None"),
"a void call's result is always None; asserting not-None would fail every successful \
call, got: {out}"
);
assert!(
out.contains("await widget_client.prefetch()") && !out.contains("result ="),
"a void not_error-only fixture must emit a bare, unbound call statement, got: {out}"
);
}
#[test]
#[should_panic(expected = "unsupported assertion type 'bogus_type' on synthetic field 'chunks'")]
fn python_streaming_virtual_unsupported_type_fails_loudly() {
let mut out = String::new();
let assertion = assertion("bogus_type", Some("chunks"), None);
emit_streaming_virtual_assertion(&mut out, &assertion, "chunks", "chunks");
}
#[test]
fn python_streaming_virtual_supported_type_renders_assertion() {
let mut out = String::new();
let assertion = assertion("greater_than", Some("chunks"), Some(serde_json::Value::from(2)));
emit_streaming_virtual_assertion(&mut out, &assertion, "chunks", "chunks");
assert_eq!(out.trim(), "assert chunks > 2");
}
#[test]
fn streaming_fixture_whose_only_assertion_is_non_virtual_gets_a_vacuous_fallback() {
let mut fixture = minimal_fixture();
fixture.assertions = vec![assertion(
"equals",
Some("not_a_streaming_field"),
Some(serde_json::json!("x")),
)];
let e2e_config = E2eConfig::default();
let call_config = crate::e2e::config::CallConfig::default();
let field_resolver = FieldResolver::new(
&std::collections::HashMap::new(),
&std::collections::HashSet::new(),
&std::collections::HashSet::new(),
&std::collections::HashSet::new(),
&std::collections::HashSet::new(),
);
let mut out = String::new();
emit_result_and_assertions(
&mut out,
&fixture,
&e2e_config,
&call_config,
"chat_stream(request)",
"result",
&field_resolver,
false,
true,
false,
);
assert!(
out.contains("not_a_streaming_field' not available on streaming result type"),
"the dropped field must still be named in a skip comment, got: {out}"
);
assert!(
out.contains("assert chunks is not None"),
"a streaming fixture with a declared but unusable assertion must still get a real \
fallback assertion instead of an entirely empty body, got: {out}"
);
}
#[test]
fn streaming_fixture_with_a_real_streaming_assertion_is_not_touched_by_the_fallback() {
let mut fixture = minimal_fixture();
fixture.assertions = vec![assertion("count_min", Some("chunks"), Some(serde_json::json!(1)))];
let e2e_config = E2eConfig::default();
let call_config = crate::e2e::config::CallConfig::default();
let field_resolver = FieldResolver::new(
&std::collections::HashMap::new(),
&std::collections::HashSet::new(),
&std::collections::HashSet::new(),
&std::collections::HashSet::new(),
&std::collections::HashSet::new(),
);
let mut out = String::new();
emit_result_and_assertions(
&mut out,
&fixture,
&e2e_config,
&call_config,
"chat_stream(request)",
"result",
&field_resolver,
false,
true,
false,
);
assert!(out.contains("assert len(chunks) >= 1"), "got: {out}");
assert!(
!out.contains("not available"),
"a real assertion must not trigger the fallback, got: {out}"
);
}
#[test]
fn non_streaming_skip_comment_carries_the_marker_the_strict_mode_matches_on() {
let mut fixture = minimal_fixture();
fixture.assertions = vec![assertion(
"equals",
Some("nonexistent_field"),
Some(serde_json::json!("x")),
)];
let e2e_config = E2eConfig::default();
let call_config = crate::e2e::config::CallConfig::default();
let result_fields: std::collections::HashSet<String> = ["content".to_string()].into_iter().collect();
let field_resolver = FieldResolver::new(
&std::collections::HashMap::new(),
&std::collections::HashSet::new(),
&result_fields,
&std::collections::HashSet::new(),
&std::collections::HashSet::new(),
);
let mut out = String::new();
emit_result_and_assertions(
&mut out,
&fixture,
&e2e_config,
&call_config,
"widget_client.create()",
"result",
&field_resolver,
false,
false,
false,
);
assert!(
out.contains("field 'nonexistent_field' not available on result type"),
"got: {out}"
);
}
}