use std::fmt::Write as FmtWrite;
use crate::e2e::escape::escape_python;
use crate::e2e::fixture::Fixture;
pub(super) fn emit_error_assertion(
out: &mut String,
fixture: &Fixture,
arg_bindings_str: &str,
call_expr: &str,
is_streaming_error_call: bool,
) {
let declared_value = crate::e2e::codegen::declared_error_value(fixture);
let has_message = declared_value.is_some();
render_unrenderable_error_path_assertions(out, fixture);
let indented_bindings: String = arg_bindings_str
.lines()
.filter(|l| !l.trim().is_empty())
.map(|l| format!(" {l}\n"))
.collect();
if has_message {
let _ = writeln!(out, " with pytest.raises(Exception) as exc_info: # noqa: B017");
out.push_str(&indented_bindings);
if is_streaming_error_call {
let sync_call_expr = call_expr.strip_prefix("await ").unwrap_or(call_expr);
let _ = writeln!(out, " _iterator = {sync_call_expr}");
let _ = writeln!(out, " async for _ in _iterator:");
let _ = writeln!(out, " pass");
} else {
let _ = writeln!(out, " {call_expr}");
}
if let Some(msg) = declared_value {
let escaped = escape_python(msg);
let _ = writeln!(
out,
" assert \"{escaped}\" in str(exc_info.value) or \"{escaped}\" in type(exc_info.value).__name__"
);
}
} else {
let _ = writeln!(out, " with pytest.raises(Exception): # noqa: B017");
out.push_str(&indented_bindings);
if is_streaming_error_call {
let _ = writeln!(out, " _iterator = {call_expr}");
let _ = writeln!(out, " async for _ in _iterator:");
let _ = writeln!(out, " pass");
} else {
let _ = writeln!(out, " {call_expr}");
}
}
}
fn render_unrenderable_error_path_assertions(out: &mut String, fixture: &Fixture) {
crate::e2e::codegen::error_path_assertions::emit(out, fixture, " # ", "python");
}
#[cfg(test)]
mod tests {
use super::*;
fn fixture_with_error(value: Option<serde_json::Value>) -> Fixture {
Fixture {
docs: None,
requirements: Vec::new(),
id: "streaming_error".to_string(),
description: "streaming error".to_string(),
input: serde_json::Value::Null,
http: None,
asyncapi: None,
websocket: None,
preserve_input_urls: false,
assertions: vec![crate::e2e::fixture::Assertion {
skip: None,
assertion_type: "error".to_string(),
field: None,
value,
values: None,
method: None,
check: None,
args: None,
return_type: None,
}],
call: None,
skip: None,
env: None,
setup: Vec::new(),
visitor: None,
args: Vec::new(),
assertion_recipes: Vec::new(),
mock_response: None,
source: String::new(),
category: None,
tags: Vec::new(),
}
}
#[test]
fn streaming_error_assertion_drains_iterator_inside_raises() {
let fixture = fixture_with_error(Some(serde_json::Value::String("BadRequest".to_string())));
let mut out = String::new();
emit_error_assertion(
&mut out,
&fixture,
" payload = {}\n",
"await client.chat_stream(payload)",
true,
);
assert!(out.contains("with pytest.raises(Exception) as exc_info"), "got: {out}");
assert!(out.contains(" payload = {}"), "got: {out}");
assert!(
out.contains(" _iterator = client.chat_stream(payload)"),
"got: {out}"
);
assert!(out.contains(" async for _ in _iterator:"), "got: {out}");
assert!(out.contains("BadRequest"), "got: {out}");
}
#[test]
fn plain_error_assertion_emits_call_inside_raises() {
let fixture = fixture_with_error(None);
let mut out = String::new();
emit_error_assertion(
&mut out,
&fixture,
" payload = {}\n",
"client.create(payload)",
false,
);
assert!(out.contains("with pytest.raises(Exception):"), "got: {out}");
assert!(out.contains(" payload = {}"), "got: {out}");
assert!(out.contains(" client.create(payload)"), "got: {out}");
assert!(!out.contains("async for _ in _iterator"), "got: {out}");
}
fn assertion(
assertion_type: &str,
field: Option<&str>,
value: Option<serde_json::Value>,
) -> crate::e2e::fixture::Assertion {
crate::e2e::fixture::Assertion {
assertion_type: assertion_type.to_string(),
field: field.map(|f| f.to_string()),
value,
..crate::e2e::fixture::Assertion::default()
}
}
fn fixture_with_assertions(assertions: Vec<crate::e2e::fixture::Assertion>) -> Fixture {
Fixture {
assertions,
..fixture_with_error(None)
}
}
#[test]
fn equals_on_error_field_is_now_visible_and_counted_by_the_gate() {
let fixture = fixture_with_assertions(vec![
assertion("error", None, Some(serde_json::Value::String("BadRequest".to_string()))),
assertion("equals", Some("error.status_code"), Some(serde_json::Value::from(429))),
]);
let mut out = String::new();
emit_error_assertion(
&mut out,
&fixture,
" payload = {}\n",
"client.create(payload)",
false,
);
assert!(out.contains("with pytest.raises(Exception) as exc_info"), "got: {out}");
assert!(
out.contains(
"assert \"BadRequest\" in str(exc_info.value) or \"BadRequest\" in type(exc_info.value).__name__"
),
"the primary error assertion must still render: got: {out}"
);
assert!(
out.contains(
"# skipped: assertion type 'equals' has no accessor for error field error.status_code in this backend"
),
"got: {out}"
);
let _ = crate::e2e::codegen::take_skip_records();
crate::e2e::codegen::fail_on_unsupported_assertion_type_markers(&out, "python", &fixture.id);
let records = crate::e2e::codegen::take_skip_records();
assert_eq!(records.len(), 1, "got: {records:?}");
assert_eq!(records[0].field, "equals");
assert_eq!(
records[0].verdict,
crate::e2e::codegen::SkipVerdict::AwaitingGeneratorSupport
);
assert_eq!(records[0].origin, crate::e2e::codegen::SkipOrigin::AssertionType);
}
#[test]
fn a_rendered_error_assertion_does_not_trip_the_assertion_type_gate() {
let fixture = fixture_with_error(Some(serde_json::Value::String("BadRequest".to_string())));
let mut out = String::new();
emit_error_assertion(
&mut out,
&fixture,
" payload = {}\n",
"client.create(payload)",
false,
);
assert!(
out.contains("assert \"BadRequest\" in str(exc_info.value)"),
"the fixture's only assertion must actually render before we assert nothing was \
flagged: got: {out}"
);
let _ = crate::e2e::codegen::take_skip_records();
crate::e2e::codegen::fail_on_unsupported_assertion_type_markers(&out, "python", &fixture.id);
assert!(
crate::e2e::codegen::take_skip_records().is_empty(),
"a rendered assertion must not be recognised as an assertion-type skip"
);
}
#[test]
fn a_bare_check_followed_by_a_valued_one_still_renders_the_message_check() {
let fixture = fixture_with_assertions(vec![
assertion("error", None, None),
assertion(
"error",
None,
Some(serde_json::Value::String("ssrf_policy_violation".to_string())),
),
]);
let mut out = String::new();
emit_error_assertion(
&mut out,
&fixture,
" url = \"http://127.0.0.1:9/\"\n",
"scrape(engine, url)",
false,
);
assert!(out.contains("with pytest.raises(Exception) as exc_info"), "got: {out}");
assert!(
out.contains(
"assert \"ssrf_policy_violation\" in str(exc_info.value) or \"ssrf_policy_violation\" in \
type(exc_info.value).__name__"
),
"the declared value on the second `error` assertion must still render a message \
check: got: {out}"
);
}
}