use std::fmt::Write as FmtWrite;
use crate::codegen::naming::go_error_type_name;
use crate::core::ir::{ErrorDef, TypeRef};
use crate::e2e::codegen::declared_error_variant::{DeclaredErrorAssertion, classify, declared_variant, skip_line};
use crate::e2e::escape::go_string_literal;
use crate::e2e::fixture::Fixture;
fn carries_error_type_field(error: &ErrorDef) -> bool {
!error.binding_excluded
&& error
.methods
.iter()
.any(|method| method.name == "error_type" && matches!(method.return_type, TypeRef::String))
}
fn emit_typed(out: &mut String, fixture: &Fixture, errors: &[ErrorDef], import_alias: &str, go_package: &str) -> bool {
let DeclaredErrorAssertion::Assert(_) = classify("go", fixture, errors) else {
return false;
};
let Some((error, variant)) = declared_variant(fixture, errors) else {
return false;
};
if !carries_error_type_field(error) {
return false;
}
let error_type = go_error_type_name(&error.name, go_package);
let expected = go_string_literal(&variant.name);
let _ = writeln!(out, "\tif err != nil {{");
let _ = writeln!(out, "\t\tvar typedError *{import_alias}.{error_type}");
let _ = writeln!(out, "\t\tif !errors.As(err, &typedError) {{");
let _ = writeln!(
out,
"\t\t\tt.Errorf(\"expected a *{import_alias}.{error_type} for {}, got %T: %v\", err, err)",
variant.name
);
let _ = writeln!(out, "\t\t}} else if typedError.ErrorType != {expected} {{");
let _ = writeln!(
out,
"\t\t\tt.Errorf(\"expected ErrorType %s, got %q\", {expected}, typedError.ErrorType)"
);
let _ = writeln!(out, "\t\t}}");
let _ = writeln!(out, "\t}}");
true
}
pub(super) fn emit_declared_error_value_assertion(
out: &mut String,
fixture: &Fixture,
errors: &[crate::core::ir::ErrorDef],
) {
match classify("go", fixture, errors) {
DeclaredErrorAssertion::Undeclared => {}
DeclaredErrorAssertion::Assert(declared) => {
let expected = go_string_literal(declared);
let _ = writeln!(out, "\tif err != nil {{");
let _ = writeln!(
out,
"\t\tif !strings.Contains(err.Error(), {expected}) && !strings.Contains(fmt.Sprintf(\"%T\", err), \
{expected}) {{"
);
let _ = writeln!(
out,
"\t\t\tt.Errorf(\"expected error to match %s, got message=%q type=%T\", {expected}, err.Error(), \
err)"
);
let _ = writeln!(out, "\t\t}}");
let _ = writeln!(out, "\t}}");
}
DeclaredErrorAssertion::Unsubstantiable(variant) => {
let _ = writeln!(out, "{}", skip_line("\t", "//", variant, &fixture.id, "go"));
}
}
}
pub(super) fn emit(out: &mut String, fixture: &Fixture, errors: &[ErrorDef], import_alias: &str, go_package: &str) {
if !emit_typed(out, fixture, errors, import_alias, go_package) {
emit_declared_error_value_assertion(out, fixture, errors);
}
}
#[cfg(test)]
mod tests {
use super::emit_typed as emit;
use crate::core::ir::{ErrorDef, ErrorVariant, MethodDef, TypeRef};
use crate::e2e::fixture::{Assertion, Fixture};
fn fixture_declaring(value: &str) -> Fixture {
Fixture {
id: "budget_enforced".to_string(),
assertions: vec![Assertion {
assertion_type: "error".to_string(),
value: Some(serde_json::Value::String(value.to_string())),
..Assertion::default()
}],
..Fixture::default()
}
}
fn method(name: &str, return_type: TypeRef) -> MethodDef {
MethodDef {
name: name.to_string(),
return_type,
..MethodDef::default()
}
}
fn error_def(name: &str, error_code: Option<u32>, methods: Vec<MethodDef>) -> Vec<ErrorDef> {
vec![ErrorDef {
name: name.to_string(),
rust_path: format!("samplelib::{name}"),
original_rust_path: String::new(),
variants: vec![ErrorVariant {
name: "BudgetExceeded".to_string(),
error_code,
is_unit: true,
..ErrorVariant::default()
}],
doc: String::new(),
methods,
binding_excluded: false,
binding_exclusion_reason: None,
version: Default::default(),
}]
}
fn render(fixture: &Fixture, errors: &[ErrorDef]) -> Option<String> {
let mut out = String::new();
emit(&mut out, fixture, errors, "pkg", "samplelib").then_some(out)
}
#[test]
fn a_named_variant_is_compared_through_the_pointer_typed_error_field() {
let errors = error_def("SamplelibError", Some(100), vec![method("error_type", TypeRef::String)]);
let out = render(&fixture_declaring("BudgetExceeded"), &errors).expect("typed assertion");
assert!(out.contains("var typedError *pkg.Error\n"), "{out}");
assert!(out.contains("errors.As(err, &typedError)"), "{out}");
assert!(out.contains("typedError.ErrorType != `BudgetExceeded`"), "{out}");
assert!(
!out.contains("strings."),
"identity must not fall back to a message search: {out}"
);
}
#[test]
fn an_error_type_without_the_field_keeps_the_message_check() {
let errors = error_def(
"SamplelibError",
Some(100),
vec![method("status_code", TypeRef::String)],
);
assert_eq!(render(&fixture_declaring("BudgetExceeded"), &errors), None);
}
#[test]
fn a_non_string_error_type_method_is_not_a_variant_name() {
let errors = error_def(
"SamplelibError",
Some(100),
vec![method(
"error_type",
TypeRef::Primitive(crate::core::ir::PrimitiveType::U16),
)],
);
assert_eq!(render(&fixture_declaring("BudgetExceeded"), &errors), None);
}
#[test]
fn a_message_style_value_is_left_to_the_message_check() {
let errors = error_def("SamplelibError", Some(100), vec![method("error_type", TypeRef::String)]);
assert_eq!(render(&fixture_declaring("budget was exceeded"), &errors), None);
}
#[test]
fn an_uncoded_variant_is_not_asserted() {
let errors = error_def("SamplelibError", None, vec![method("error_type", TypeRef::String)]);
assert_eq!(render(&fixture_declaring("BudgetExceeded"), &errors), None);
}
#[test]
fn the_error_type_name_follows_the_generators_prefix_stripping() {
let methods = || vec![method("error_type", TypeRef::String)];
let stripped = error_def("SamplelibError", Some(100), methods());
let verbatim = error_def("ConversionError", Some(100), methods());
let stripped_out = render(&fixture_declaring("BudgetExceeded"), &stripped).expect("typed assertion");
let verbatim_out = render(&fixture_declaring("BudgetExceeded"), &verbatim).expect("typed assertion");
assert!(stripped_out.contains("var typedError *pkg.Error\n"), "{stripped_out}");
assert!(
verbatim_out.contains("var typedError *pkg.ConversionError\n"),
"{verbatim_out}"
);
}
}