use crate::core::ir::{ErrorDef, ErrorVariant};
use crate::e2e::codegen::assertion_type_skip::AssertionTypeSkip;
use crate::e2e::fixture::Fixture;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum DeclaredErrorAssertion<'a> {
Undeclared,
Assert(&'a str),
Unsubstantiable(&'a str),
}
pub(crate) fn classify<'a>(lang: &str, fixture: &'a Fixture, errors: &[ErrorDef]) -> DeclaredErrorAssertion<'a> {
let Some(declared) = super::declared_error_value(fixture) else {
return DeclaredErrorAssertion::Undeclared;
};
let named_variant = errors
.iter()
.flat_map(|error| &error.variants)
.find(|variant| variant.name == declared);
match named_variant {
None => DeclaredErrorAssertion::Assert(declared),
Some(variant) if substantiates_variant_identity(lang, variant) => DeclaredErrorAssertion::Assert(declared),
Some(_) => DeclaredErrorAssertion::Unsubstantiable(declared),
}
}
fn substantiates_variant_identity(lang: &str, variant: &ErrorVariant) -> bool {
match lang {
"python" => true,
"go" | "java" | "zig" => variant.error_code.is_some(),
"c" | "dart" | "csharp" | "php" | "swift" | "ruby" | "elixir" | "gleam" | "r" | "node" => false,
_ => true,
}
}
fn skip_variant_for(lang: &str) -> AssertionTypeSkip {
match lang {
"c" | "dart" | "go" | "java" | "zig" => AssertionTypeSkip::DeclaredErrorVariantNotSubstantiated,
_ => AssertionTypeSkip::DeclaredErrorVariantNotYetPreservedByGenerator,
}
}
pub(crate) fn skip_line(indent: &str, comment_open: &str, variant: &str, fixture_id: &str, language: &str) -> String {
let line = format!(
"{indent}{comment_open} skipped: {}",
skip_variant_for(language).message(variant)
);
super::fail_on_unsupported_assertion_type_markers(&line, language, fixture_id);
line
}
#[cfg(test)]
mod tests {
use super::{DeclaredErrorAssertion, classify, skip_line};
use crate::core::ir::{ErrorDef, ErrorVariant};
use crate::e2e::fixture::{Assertion, Fixture};
fn error_assertion(value: &str) -> Assertion {
Assertion {
assertion_type: "error".to_string(),
value: Some(serde_json::Value::String(value.to_string())),
..Assertion::default()
}
}
fn fixture_with(value: &str) -> Fixture {
Fixture {
id: "declares_error".to_string(),
assertions: vec![error_assertion(value)],
..Fixture::default()
}
}
fn coded_variant(name: &str, code: Option<u32>) -> ErrorVariant {
ErrorVariant {
name: name.to_string(),
error_code: code,
is_unit: true,
..ErrorVariant::default()
}
}
fn error_def(variants: Vec<ErrorVariant>) -> ErrorDef {
ErrorDef {
name: "ApiError".to_string(),
rust_path: "lib::ApiError".to_string(),
original_rust_path: String::new(),
variants,
doc: String::new(),
methods: vec![],
binding_excluded: false,
binding_exclusion_reason: None,
version: Default::default(),
}
}
#[test]
fn no_declared_value_is_undeclared() {
let fixture = Fixture {
id: "no_error".to_string(),
..Fixture::default()
};
assert_eq!(classify("php", &fixture, &[]), DeclaredErrorAssertion::Undeclared);
}
#[test]
fn message_style_value_is_always_assertable() {
let fixture = fixture_with("size");
let errors = vec![error_def(vec![coded_variant("Authentication", None)])];
assert_eq!(
classify("php", &fixture, &errors),
DeclaredErrorAssertion::Assert("size")
);
assert_eq!(classify("c", &fixture, &errors), DeclaredErrorAssertion::Assert("size"));
}
#[test]
fn variant_shaped_value_with_no_ir_data_still_asserts() {
let fixture = fixture_with("Authentication");
assert_eq!(
classify("php", &fixture, &[]),
DeclaredErrorAssertion::Assert("Authentication")
);
}
#[test]
fn python_always_substantiates_a_known_variant() {
let fixture = fixture_with("Authentication");
let errors = vec![error_def(vec![coded_variant("Authentication", None)])];
assert_eq!(
classify("python", &fixture, &errors),
DeclaredErrorAssertion::Assert("Authentication")
);
}
#[test]
fn php_never_substantiates_a_known_variant() {
let fixture = fixture_with("Authentication");
let errors = vec![error_def(vec![coded_variant("Authentication", Some(100))])];
assert_eq!(
classify("php", &fixture, &errors),
DeclaredErrorAssertion::Unsubstantiable("Authentication")
);
}
#[test]
fn c_never_substantiates_a_known_variant_even_when_coded() {
let fixture = fixture_with("Authentication");
let errors = vec![error_def(vec![coded_variant("Authentication", Some(100))])];
assert_eq!(
classify("c", &fixture, &errors),
DeclaredErrorAssertion::Unsubstantiable("Authentication")
);
}
#[test]
fn go_java_zig_are_conditional_on_error_code() {
let fixture = fixture_with("Authentication");
let coded = vec![error_def(vec![coded_variant("Authentication", Some(100))])];
let uncoded = vec![error_def(vec![coded_variant("Authentication", None)])];
for lang in ["go", "java", "zig"] {
assert_eq!(
classify(lang, &fixture, &coded),
DeclaredErrorAssertion::Assert("Authentication"),
"{lang} must assert a coded variant"
);
assert_eq!(
classify(lang, &fixture, &uncoded),
DeclaredErrorAssertion::Unsubstantiable("Authentication"),
"{lang} must skip an uncoded variant"
);
}
}
#[test]
fn dart_swift_ruby_csharp_elixir_gleam_r_node_never_substantiate_a_known_variant() {
let fixture = fixture_with("BadRequest");
let errors = vec![error_def(vec![coded_variant("BadRequest", Some(200))])];
for lang in ["dart", "swift", "ruby", "csharp", "elixir", "gleam", "r", "node"] {
assert_eq!(
classify(lang, &fixture, &errors),
DeclaredErrorAssertion::Unsubstantiable("BadRequest"),
"{lang} must skip a known variant it cannot substantiate"
);
}
}
#[test]
fn an_unaudited_language_keeps_asserting() {
let fixture = fixture_with("Authentication");
let errors = vec![error_def(vec![coded_variant("Authentication", None)])];
assert_eq!(
classify("kotlin", &fixture, &errors),
DeclaredErrorAssertion::Assert("Authentication")
);
}
#[test]
fn skip_line_renders_the_language_limitation_wording_and_records_it() {
let _ = crate::e2e::codegen::take_skip_records();
let line = skip_line(" ", "//", "Authentication", "auth_fails", "c");
assert_eq!(
line,
" // skipped: declared error variant 'Authentication' not substantiated by this backend's generated \
error type"
);
let records = crate::e2e::codegen::take_skip_records();
assert_eq!(records.len(), 1, "got: {records:?}");
assert_eq!(records[0].language, "c");
assert_eq!(records[0].fixture_id, "auth_fails");
assert_eq!(records[0].field, "Authentication");
}
#[test]
fn skip_line_renders_the_generator_gap_wording_for_a_fixable_backend() {
let _ = crate::e2e::codegen::take_skip_records();
let line = skip_line(" ", "#", "BadRequest", "bad_request_fails", "ruby");
assert_eq!(
line,
" # skipped: declared error variant 'BadRequest' not yet preserved as a distinct identity by \
this backend's generator"
);
let records = crate::e2e::codegen::take_skip_records();
assert_eq!(records.len(), 1, "got: {records:?}");
assert_eq!(records[0].language, "ruby");
}
}