use std::fmt::Write as FmtWrite;
use crate::core::ir::ErrorDef;
use crate::e2e::fixture::Fixture;
fn is_valued_error_assertion(assertion: &crate::e2e::fixture::Assertion) -> bool {
assertion.assertion_type == "error" && assertion.value.as_ref().and_then(serde_json::Value::as_str).is_some()
}
pub(crate) fn render(fixture: &Fixture, line_prefix: &str, language: &str) -> String {
render_with_errors(fixture, line_prefix, language, &[])
}
pub(crate) fn render_with_errors(fixture: &Fixture, line_prefix: &str, language: &str, errors: &[ErrorDef]) -> String {
if !fixture.assertions.iter().any(|a| a.assertion_type == "error") {
return String::new();
}
let mut out = String::new();
let mut consumed_the_declared_value = false;
for assertion in &fixture.assertions {
if assertion.assertion_type == "error" {
if !is_valued_error_assertion(assertion) {
continue;
}
if !consumed_the_declared_value {
consumed_the_declared_value = true;
continue;
}
let _ = writeln!(
out,
"{line_prefix}skipped: {}",
super::assertion_type_skip::AssertionTypeSkip::AdditionalDeclaredErrorValueNotChecked.message("error")
);
continue;
}
if super::error_field_reachability::resolvable_equals_error_field(assertion, errors).is_some() {
continue;
}
let field = assertion.field.as_deref().unwrap_or("<none>");
let _ = writeln!(
out,
"{line_prefix}skipped: assertion type '{}' has no accessor for error field {field} in this backend",
assertion.assertion_type
);
}
super::fail_on_unsupported_assertion_type_markers(&out, language, &fixture.id);
out
}
pub(crate) fn emit(out: &mut String, fixture: &Fixture, line_prefix: &str, language: &str) {
out.push_str(&render(fixture, line_prefix, language));
}
pub(crate) fn emit_with_errors(
out: &mut String,
fixture: &Fixture,
line_prefix: &str,
language: &str,
errors: &[ErrorDef],
) {
out.push_str(&render_with_errors(fixture, line_prefix, language, errors));
}
#[cfg(test)]
mod tests {
use super::*;
use crate::e2e::fixture::Assertion;
fn assertion(assertion_type: &str, field: Option<&str>) -> Assertion {
Assertion {
assertion_type: assertion_type.to_string(),
field: field.map(str::to_string),
..Assertion::default()
}
}
fn error_assertion_with_value(value: &str) -> Assertion {
Assertion {
assertion_type: "error".to_string(),
value: Some(serde_json::Value::String(value.to_string())),
..Assertion::default()
}
}
fn fixture_with(assertions: Vec<Assertion>) -> Fixture {
Fixture {
id: "rate_limited".to_string(),
assertions,
..Fixture::default()
}
}
#[test]
fn a_lone_error_assertion_renders_no_marker() {
let fixture = fixture_with(vec![assertion("error", None)]);
let _ = crate::e2e::codegen::take_skip_records();
assert_eq!(render(&fixture, " // ", "go"), "");
assert!(crate::e2e::codegen::take_skip_records().is_empty());
}
#[test]
fn an_error_field_equals_assertion_is_named_and_counted() {
let fixture = fixture_with(vec![
assertion("error", None),
assertion("equals", Some("error.status_code")),
]);
let _ = crate::e2e::codegen::take_skip_records();
let rendered = render(&fixture, "\t// ", "go");
assert_eq!(
rendered,
"\t// skipped: assertion type 'equals' has no accessor for error field error.status_code in this \
backend\n"
);
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].language, "go");
assert_eq!(records[0].fixture_id, "rate_limited");
assert_eq!(records[0].origin, crate::e2e::codegen::SkipOrigin::AssertionType);
assert_eq!(
records[0].verdict,
crate::e2e::codegen::SkipVerdict::AwaitingGeneratorSupport
);
}
fn error_def_with_methods(name: &str, method_names: &[&str]) -> crate::core::ir::ErrorDef {
crate::core::ir::ErrorDef {
name: name.to_string(),
rust_path: format!("demo::{name}"),
original_rust_path: String::new(),
variants: vec![crate::core::ir::ErrorVariant::default()],
doc: String::new(),
methods: method_names
.iter()
.map(|method_name| crate::core::ir::MethodDef {
name: (*method_name).to_string(),
params: Vec::new(),
return_type: crate::core::ir::TypeRef::Primitive(crate::core::ir::PrimitiveType::U16),
is_async: false,
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(),
})
.collect(),
binding_excluded: false,
binding_exclusion_reason: None,
version: Default::default(),
}
}
#[test]
fn render_with_errors_suppresses_the_marker_only_when_the_field_resolves() {
let fixture = fixture_with(vec![
assertion("error", None),
assertion("equals", Some("error.status_code")),
]);
let errors = vec![error_def_with_methods("SampleError", &["status_code"])];
let _ = crate::e2e::codegen::take_skip_records();
let resolved = render_with_errors(&fixture, " // ", "python", &errors);
assert_eq!(resolved, "", "a resolvable field must render no skip marker at all");
assert!(crate::e2e::codegen::take_skip_records().is_empty());
let _ = crate::e2e::codegen::take_skip_records();
let unresolved = render(&fixture, " // ", "go");
assert_eq!(
unresolved,
" // skipped: assertion type 'equals' has no accessor for error field error.status_code in this \
backend\n"
);
assert_eq!(crate::e2e::codegen::take_skip_records().len(), 1);
}
#[test]
fn render_with_errors_still_names_an_unresolvable_field() {
let fixture = fixture_with(vec![
assertion("error", None),
assertion("equals", Some("error.retry_after")),
]);
let errors = vec![error_def_with_methods("SampleError", &["status_code"])];
let _ = crate::e2e::codegen::take_skip_records();
let rendered = render_with_errors(&fixture, " // ", "python", &errors);
assert_eq!(
rendered,
" // skipped: assertion type 'equals' has no accessor for error field error.retry_after in this \
backend\n"
);
assert_eq!(crate::e2e::codegen::take_skip_records().len(), 1);
}
#[test]
fn render_with_errors_still_names_a_resolvable_field_with_an_unrendered_assertion_type() {
let fixture = fixture_with(vec![
assertion("error", None),
assertion("greater_than", Some("error.status_code")),
]);
let errors = vec![error_def_with_methods("SampleError", &["status_code"])];
let _ = crate::e2e::codegen::take_skip_records();
let rendered = render_with_errors(&fixture, " // ", "python", &errors);
assert_eq!(
rendered,
" // skipped: assertion type 'greater_than' has no accessor for error field error.status_code in \
this backend\n"
);
assert_eq!(crate::e2e::codegen::take_skip_records().len(), 1);
}
#[test]
fn two_bare_error_assertions_render_no_marker() {
let fixture = fixture_with(vec![assertion("error", None), assertion("error", None)]);
let _ = crate::e2e::codegen::take_skip_records();
assert_eq!(render(&fixture, " # ", "ruby"), "");
assert!(crate::e2e::codegen::take_skip_records().is_empty());
}
#[test]
fn a_bare_check_followed_by_a_valued_one_renders_no_marker() {
let fixture = fixture_with(vec![
assertion("error", None),
error_assertion_with_value("ssrf_policy_violation"),
]);
assert_eq!(
crate::e2e::codegen::declared_error_value(&fixture),
Some("ssrf_policy_violation"),
"the shared lookup must find the value on the second assertion, not just the first"
);
let _ = crate::e2e::codegen::take_skip_records();
assert_eq!(render(&fixture, " # ", "python"), "");
assert!(crate::e2e::codegen::take_skip_records().is_empty());
}
#[test]
fn a_valued_check_followed_by_a_bare_one_renders_no_marker() {
let fixture = fixture_with(vec![
error_assertion_with_value("Authentication"),
assertion("error", None),
]);
assert_eq!(
crate::e2e::codegen::declared_error_value(&fixture),
Some("Authentication")
);
let _ = crate::e2e::codegen::take_skip_records();
assert_eq!(render(&fixture, " # ", "go"), "");
assert!(crate::e2e::codegen::take_skip_records().is_empty());
}
#[test]
fn a_second_declared_value_is_named_and_counted() {
let fixture = fixture_with(vec![
error_assertion_with_value("rate_limited"),
error_assertion_with_value("429"),
]);
let _ = crate::e2e::codegen::take_skip_records();
let rendered = render(&fixture, " # ", "ruby");
assert_eq!(
rendered,
" # skipped: assertion type 'error' has an additional declared value not checked by this \
backend\n"
);
let records = crate::e2e::codegen::take_skip_records();
assert_eq!(records.len(), 1, "got: {records:?}");
assert_eq!(records[0].field, "error");
assert_eq!(
records[0].verdict,
crate::e2e::codegen::SkipVerdict::AwaitingGeneratorSupport
);
}
#[test]
fn a_fixture_with_no_error_assertion_renders_nothing() {
let fixture = fixture_with(vec![
assertion("equals", Some("status_code")),
assertion("not_empty", Some("content")),
]);
let _ = crate::e2e::codegen::take_skip_records();
assert_eq!(render(&fixture, " // ", "dart"), "");
assert!(crate::e2e::codegen::take_skip_records().is_empty());
}
#[test]
fn every_trailing_assertion_gets_its_own_line() {
let fixture = fixture_with(vec![
assertion("error", None),
assertion("equals", Some("error.status_code")),
assertion("contains", Some("error.message")),
]);
let _ = crate::e2e::codegen::take_skip_records();
let rendered = render(&fixture, " // ", "swift");
assert_eq!(rendered.lines().count(), 2, "got: {rendered}");
assert_eq!(crate::e2e::codegen::take_skip_records().len(), 2);
}
}