use std::ops::Range;
mod catalog;
mod render;
mod suggest;
pub use catalog::{error_catalog, explain};
pub use render::{render_diagnostic, report_diagnostics_human};
pub use suggest::{did_you_mean, edit_distance, suggest_error_code, unknown_error_code_message};
pub type Span = Range<usize>;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize)]
#[serde(transparent)]
pub struct ErrorCode(String);
impl ErrorCode {
pub fn as_str(&self) -> &str {
&self.0
}
}
impl std::fmt::Display for ErrorCode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl AsRef<str> for ErrorCode {
fn as_ref(&self) -> &str {
&self.0
}
}
impl From<&str> for ErrorCode {
fn from(s: &str) -> Self {
Self(s.to_owned())
}
}
impl From<String> for ErrorCode {
fn from(s: String) -> Self {
Self(s)
}
}
impl PartialEq<str> for ErrorCode {
fn eq(&self, other: &str) -> bool {
self.0 == other
}
}
impl PartialEq<&str> for ErrorCode {
fn eq(&self, other: &&str) -> bool {
self.0 == *other
}
}
impl PartialEq<String> for ErrorCode {
fn eq(&self, other: &String) -> bool {
self.0 == *other
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize)]
#[serde(rename_all = "lowercase")]
pub enum Severity {
Info,
Warning,
Error,
}
#[derive(Debug, Clone, PartialEq, serde::Serialize)]
pub struct SecondaryLabel {
pub span: Span,
pub message: String,
}
#[derive(Debug, Clone, PartialEq, serde::Serialize)]
pub struct Suggestion {
pub message: String,
pub span: Span,
pub replacement: String,
}
#[derive(Debug, Clone, PartialEq, serde::Serialize)]
pub struct Diagnostic {
pub code: ErrorCode,
pub severity: Severity,
pub message: String,
pub file: String,
pub primary: Span,
pub secondary: Vec<SecondaryLabel>,
pub suggestion: Option<Suggestion>,
}
impl Diagnostic {
pub fn error(code: impl Into<ErrorCode>, message: impl Into<String>, span: Span) -> Self {
Self {
code: code.into(),
severity: Severity::Error,
message: message.into(),
file: String::new(),
primary: span,
secondary: Vec::new(),
suggestion: None,
}
}
pub fn warning(code: impl Into<ErrorCode>, message: impl Into<String>, span: Span) -> Self {
Self {
code: code.into(),
severity: Severity::Warning,
message: message.into(),
file: String::new(),
primary: span,
secondary: Vec::new(),
suggestion: None,
}
}
pub fn with_file(mut self, file: impl Into<String>) -> Self {
self.file = file.into();
self
}
pub fn with_secondary(mut self, span: Span, label: impl Into<String>) -> Self {
self.secondary.push(SecondaryLabel {
span,
message: label.into(),
});
self
}
pub fn with_suggestion(
mut self,
message: impl Into<String>,
span: Span,
replacement: impl Into<String>,
) -> Self {
self.suggestion = Some(Suggestion {
message: message.into(),
span,
replacement: replacement.into(),
});
self
}
pub fn is_error(&self) -> bool {
self.severity == Severity::Error
}
}
impl std::fmt::Display for Diagnostic {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "[{}] {}", self.code, self.message)
}
}
impl std::fmt::Display for Severity {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Severity::Info => write!(f, "info"),
Severity::Warning => write!(f, "warning"),
Severity::Error => write!(f, "error"),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct ErrorInfo {
pub code: &'static str,
pub name: &'static str,
pub description: &'static str,
pub example: &'static str,
pub fix: &'static str,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn error_diagnostic_creation() {
let d = Diagnostic::error("A03001", "type mismatch", 10..20);
assert_eq!(d.code, "A03001");
assert_eq!(d.severity, Severity::Error);
assert_eq!(d.primary, 10..20);
assert!(d.is_error());
}
#[test]
fn warning_diagnostic_creation() {
let d = Diagnostic::warning("A05001", "unused variable", 5..10);
assert_eq!(d.severity, Severity::Warning);
assert!(!d.is_error());
}
#[test]
fn diagnostic_with_secondary() {
let d = Diagnostic::error("A03002", "expected Int", 10..20)
.with_secondary(30..40, "declared here");
assert_eq!(d.secondary.len(), 1);
assert_eq!(d.secondary[0].message, "declared here");
}
#[test]
fn diagnostic_with_suggestion() {
let d = Diagnostic::error("A01001", "unexpected token", 5..8).with_suggestion(
"try adding a semicolon",
7..8,
";",
);
let s = d.suggestion.unwrap();
assert_eq!(s.replacement, ";");
}
#[test]
fn diagnostic_display() {
let d = Diagnostic::error("A03001", "type mismatch", 0..1);
assert_eq!(format!("{d}"), "[A03001] type mismatch");
}
#[test]
fn severity_ordering() {
assert!(Severity::Info < Severity::Warning);
assert!(Severity::Warning < Severity::Error);
}
#[test]
fn test_error_diagnostic_is_error() {
let d = Diagnostic::error("A01001", "syntax error", 0..5);
assert!(d.is_error());
assert_eq!(d.severity, Severity::Error);
}
#[test]
fn test_warning_diagnostic_is_not_error() {
let d = Diagnostic::warning("A02007", "unused import", 10..20);
assert!(!d.is_error());
assert_eq!(d.severity, Severity::Warning);
}
#[test]
fn test_severity_display() {
assert_eq!(format!("{}", Severity::Info), "info");
assert_eq!(format!("{}", Severity::Warning), "warning");
assert_eq!(format!("{}", Severity::Error), "error");
}
#[test]
fn test_diagnostic_with_file() {
let d = Diagnostic::error("A03001", "type mismatch", 0..10).with_file("test.assura");
assert_eq!(d.file, "test.assura");
}
#[test]
fn test_diagnostic_multiple_secondary_spans() {
let d = Diagnostic::error("A03001", "type mismatch", 10..20)
.with_secondary(30..40, "expected type here")
.with_secondary(50..60, "found type here");
assert_eq!(d.secondary.len(), 2);
assert_eq!(d.secondary[0].message, "expected type here");
assert_eq!(d.secondary[0].span, 30..40);
assert_eq!(d.secondary[1].message, "found type here");
assert_eq!(d.secondary[1].span, 50..60);
}
#[test]
fn test_diagnostic_suggestion_fields() {
let d = Diagnostic::error("A01002", "unexpected token", 5..8).with_suggestion(
"add a colon",
7..8,
":",
);
let s = d.suggestion.as_ref().unwrap();
assert_eq!(s.message, "add a colon");
assert_eq!(s.span, 7..8);
assert_eq!(s.replacement, ":");
}
#[test]
fn test_diagnostic_json_serialization() {
let d = Diagnostic::error("A03001", "type mismatch", 10..20)
.with_file("main.assura")
.with_secondary(30..40, "declared here");
let json = serde_json::to_string(&d).unwrap();
assert!(json.contains("A03001"));
assert!(json.contains("type mismatch"));
assert!(json.contains("main.assura"));
assert!(json.contains("declared here"));
let val: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(val["code"], "A03001");
assert_eq!(val["severity"], "error");
assert_eq!(val["message"], "type mismatch");
}
#[test]
fn test_diagnostic_collection() {
let diags = vec![
Diagnostic::error("A01001", "unexpected char", 0..1),
Diagnostic::warning("A02007", "unused import", 10..20),
Diagnostic::error("A03001", "type mismatch", 30..40),
];
assert_eq!(diags.len(), 3);
let errors: Vec<_> = diags.iter().filter(|d| d.is_error()).collect();
assert_eq!(errors.len(), 2);
let warnings: Vec<_> = diags
.iter()
.filter(|d| d.severity == Severity::Warning)
.collect();
assert_eq!(warnings.len(), 1);
}
#[test]
fn test_diagnostic_empty_secondary_spans() {
let d = Diagnostic::error("A03001", "error", 0..5);
assert!(d.secondary.is_empty());
assert!(d.suggestion.is_none());
}
#[test]
fn test_error_code_formatting_display() {
let d = Diagnostic::error("A05001", "linear variable used twice", 0..10);
let display = format!("{d}");
assert_eq!(display, "[A05001] linear variable used twice");
}
#[test]
fn test_error_catalog_not_empty() {
let catalog = error_catalog();
assert!(!catalog.is_empty());
for entry in &catalog {
assert!(!entry.code.is_empty());
assert!(!entry.name.is_empty());
assert!(!entry.description.is_empty());
assert!(!entry.example.is_empty());
assert!(!entry.fix.is_empty());
}
}
#[test]
fn test_explain_known_code() {
let info = explain("A01001");
let info = info.unwrap();
assert_eq!(info.code, "A01001");
assert_eq!(info.name, "Unexpected character");
}
#[test]
fn test_explain_unknown_code() {
let info = explain("A00000");
assert!(info.is_none());
}
#[test]
fn high_traffic_index_codes_are_in_catalog() {
const CODES: &[&str] = &[
"A01001", "A01002", "A02001", "A02003", "A02005", "A03001", "A03002", "A03005",
"A03006", "A05001", "A05002", "A05003", "A05004", "A06001", "A06002", "A06003",
"A06004", "A07001", "A07002", "A07003", "A08001", "A08002", "A08003", "A08004",
"A08005", "A09001", "A09002", "A09003", "A09004", "A11001", "A11002", "A11003",
"A11004", "A12001", "A12002", "A12003", "A13001", "A13002", "A13003", "A16001",
"A16002", "A16003", "A17001", "A17002", "A17003", "A21001", "A21002", "A21003",
"A22001", "A22002", "A22003", "A05100", "A05101", "A05102", "A05103", "A10002",
"A01000", "A02006", "A02007", "A02008", "A02010", "A03007", "A03010", "A08102",
"A10001", "A10101", "A11005", "A14001", "A14002", "A04008", "A05025", "A05026",
"A08101", "A09101", "A23003", "A26001", "A43005", "A17004", "A23016", "A24001",
"A27003", "A28001", "A33001", "A37003", "A38001", "A42003", "A43001", "A43002",
"A44001", "A45001", "A47001", "A48002", "A49001", "A49002", "A50001", "A52001",
"A54001", "A55001", "A64001", "A31006", "A31007", "A32002", "A36003", "A52002",
"A46002", "A29001", "A25003", "A09103", "A53006", "A49003", "A35003", "A34003",
"A30002", "A23001", "A10104", "A09102", "A08103", "A51003", "A46003", "A36001",
"A35001", "A10102", "A10103", "A42001", "A20001", "A20002", "A18001", "A18003",
"A24003", "A25001", "A22004", "A44003", "A46001", "A55003", "A32001", "A48001",
"A34001", "A37001", "A30003", "A15004", "A15001", "A18002", "A33003", "A03012",
"A23002", "A45003", "A42002", "A31001", "A31003", "A32003", "A51001", "A48003",
"A54003", "A30001", "A29003", "A28003", "A27001", "A26004", "A26003", "A15002",
"A15003", "A33002", "A03011", "A03008", "A25002", "A24002", "A23019", "A47002",
"A47003", "A45002", "A38002", "A44002", "A55002", "A54002", "A43003", "A43004",
"A31002", "A53003", "A53001", "A53002", "A52003", "A50002", "A50003", "A36002",
"A38003", "A35002", "A34002", "A29002", "A28002", "A27002", "A05200", "A51002",
"A37002", "A03009",
];
for code in CODES {
let info = explain(code).unwrap_or_else(|| {
panic!(
"{code}: listed in docs/error-codes.md high-traffic table but missing from catalog"
)
});
assert_eq!(info.code, *code);
assert!(
!info.name.is_empty(),
"{code}: catalog entry must have a non-empty name"
);
}
}
#[test]
fn test_explain_all_catalog_codes() {
let catalog = error_catalog();
for entry in &catalog {
let found = explain(entry.code).unwrap_or_else(|| {
panic!("should find {}", entry.code);
});
assert_eq!(found.code, entry.code);
}
}
#[test]
fn test_warning_serialization() {
let d = Diagnostic::warning("A02007", "unused import", 5..15);
let json = serde_json::to_string(&d).unwrap();
let val: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(val["severity"], "warning");
}
#[test]
fn test_suggestion_serialization() {
let s = Suggestion {
message: "add semicolon".to_string(),
span: 10..11,
replacement: ";".to_string(),
};
let json = serde_json::to_string(&s).unwrap();
assert!(json.contains("add semicolon"));
}
#[test]
fn test_secondary_label_equality() {
let a = SecondaryLabel {
span: 0..5,
message: "here".to_string(),
};
let b = SecondaryLabel {
span: 0..5,
message: "here".to_string(),
};
assert_eq!(a, b);
}
#[test]
fn test_no_duplicate_error_codes() {
let catalog = error_catalog();
let mut seen = std::collections::HashSet::new();
for entry in &catalog {
assert!(
seen.insert(entry.code),
"duplicate error code in catalog: {}",
entry.code
);
}
}
#[test]
fn test_a03005_catalog_is_unknown_field() {
let info = explain("A03005").expect("A03005 should exist");
assert_eq!(info.name, "Unknown field");
assert!(
info.description.to_lowercase().contains("field"),
"A03005 description should mention fields, got: {}",
info.description
);
let fix_lower = info.fix.to_lowercase();
assert!(
!fix_lower.contains("calling a function"),
"A03005 fix/Help must not mention calling a function (that was the bug): {}",
info.fix
);
assert!(
fix_lower.contains("field") || fix_lower.contains("tuple"),
"A03005 fix should be field-oriented, got: {}",
info.fix
);
assert!(
info.example.contains(".z") || info.example.contains("t.2"),
"A03005 example should show unknown field or OOB tuple index"
);
assert!(
!info.example.contains("Foo(42)"),
"A03005 example must not be the old type-as-call snippet"
);
}
#[test]
fn test_render_diagnostic_does_not_panic() {
let d = Diagnostic::error("A01001", "unexpected char", 0..1);
render_diagnostic(&d, "test.assura", "x");
let d = Diagnostic::warning("A02007", "unused import", 0..5)
.with_secondary(6..10, "imported here");
render_diagnostic(&d, "test.assura", "import std.math;");
}
#[test]
fn test_report_diagnostics_human_multiple() {
let diags = vec![
Diagnostic::error("A01001", "bad char", 0..1),
Diagnostic::warning("A02007", "unused", 2..5),
];
report_diagnostics_human(&diags, "multi.assura", "x = 42;");
}
#[test]
fn test_error_code_as_str() {
let code = ErrorCode::from("A03001");
assert_eq!(code.as_str(), "A03001");
}
#[test]
fn test_error_code_from_string() {
let code = ErrorCode::from(String::from("A05001"));
assert_eq!(code, "A05001");
}
#[test]
fn test_error_code_partial_eq_str() {
let code = ErrorCode::from("A07003");
assert!(code == "A07003");
assert!(code == *"A07003");
}
#[test]
fn test_error_code_as_ref() {
let code = ErrorCode::from("A01002");
let s: &str = code.as_ref();
assert_eq!(s, "A01002");
}
#[test]
fn test_error_code_display() {
let code = ErrorCode::from("A03005");
assert_eq!(format!("{code}"), "A03005");
}
#[test]
fn test_error_code_ordering() {
let a = ErrorCode::from("A01001");
let b = ErrorCode::from("A03001");
assert!(a < b);
}
#[test]
fn test_error_catalog_entries_have_fields() {
let catalog = error_catalog();
for entry in &catalog {
assert!(!entry.code.is_empty(), "code must not be empty");
assert!(
!entry.name.is_empty(),
"name must not be empty for {}",
entry.code
);
assert!(
!entry.description.is_empty(),
"description must not be empty for {}",
entry.code
);
assert!(
!entry.fix.is_empty(),
"fix must not be empty for {}",
entry.code
);
}
}
#[test]
fn test_diagnostic_chaining() {
let d = Diagnostic::error("A03001", "mismatch", 10..20)
.with_file("test.assura")
.with_secondary(30..40, "defined here")
.with_suggestion("use Int", 10..20, "Int");
assert_eq!(d.file, "test.assura");
assert_eq!(d.secondary.len(), 1);
d.suggestion.unwrap();
}
#[test]
fn test_severity_serde() {
let json = serde_json::to_string(&Severity::Error).unwrap();
assert_eq!(json, "\"error\"");
let json = serde_json::to_string(&Severity::Warning).unwrap();
assert_eq!(json, "\"warning\"");
let json = serde_json::to_string(&Severity::Info).unwrap();
assert_eq!(json, "\"info\"");
}
#[test]
fn test_error_code_eq_string_owned() {
let code = ErrorCode::from("A03001");
assert!(code == String::from("A03001"));
}
#[test]
fn test_error_code_ne() {
let a = ErrorCode::from("A01001");
let b = ErrorCode::from("A03001");
assert_ne!(a, b);
}
#[test]
fn test_error_code_clone_eq() {
let code = ErrorCode::from("A05001");
let cloned = code.clone();
assert_eq!(code, cloned);
}
#[test]
fn test_error_code_hash_consistent() {
use std::collections::HashSet;
let mut set = HashSet::new();
set.insert(ErrorCode::from("A01001"));
set.insert(ErrorCode::from("A01001")); set.insert(ErrorCode::from("A03001"));
assert_eq!(set.len(), 2);
}
#[test]
fn test_error_code_empty() {
let code = ErrorCode::from("");
assert_eq!(code.as_str(), "");
assert_eq!(format!("{code}"), "");
}
#[test]
fn test_error_catalog_all_codes_valid_format() {
let catalog = error_catalog();
for entry in &catalog {
assert_eq!(
entry.code.len(),
6,
"error code '{}' should be 6 chars (Axxxxx)",
entry.code
);
assert!(
entry.code.starts_with('A'),
"error code '{}' should start with 'A'",
entry.code
);
assert!(
entry.code[1..].chars().all(|c| c.is_ascii_digit()),
"error code '{}' should have 5 digits after 'A'",
entry.code
);
}
}
#[test]
fn test_error_catalog_has_major_categories() {
let catalog = error_catalog();
let codes: Vec<&str> = catalog.iter().map(|e| e.code).collect();
assert!(
codes.iter().any(|c| c.starts_with("A01")),
"missing A01xxx (syntax)"
);
assert!(
codes.iter().any(|c| c.starts_with("A02")),
"missing A02xxx (resolve)"
);
assert!(
codes.iter().any(|c| c.starts_with("A03")),
"missing A03xxx (type)"
);
assert!(
codes.iter().any(|c| c.starts_with("A05")),
"missing A05xxx (linear)"
);
assert!(
codes.iter().any(|c| c.starts_with("A07")),
"missing A07xxx (effect)"
);
}
#[test]
fn test_error_catalog_size_reasonable() {
let catalog = error_catalog();
assert!(
catalog.len() >= 150,
"catalog should have 150+ entries (emitted + wired codes), got {}",
catalog.len()
);
}
#[test]
fn test_explain_empty_string() {
assert!(explain("").is_none());
}
#[test]
fn test_explain_partial_code() {
assert!(explain("A01").is_none());
assert!(explain("A").is_none());
}
#[test]
fn test_explain_nonexistent_category() {
assert!(explain("A88888").is_none());
}
#[test]
fn test_diagnostic_zero_length_span() {
let d = Diagnostic::error("A01001", "at position", 5..5);
assert_eq!(d.primary, 5..5);
assert!(d.primary.is_empty());
}
#[test]
fn test_diagnostic_large_span() {
let d = Diagnostic::error("A01001", "whole file", 0..100_000);
assert_eq!(d.primary, 0..100_000);
}
#[test]
fn test_diagnostic_empty_message() {
let d = Diagnostic::error("A01001", "", 0..1);
assert_eq!(d.message, "");
assert_eq!(format!("{d}"), "[A01001] ");
}
#[test]
fn test_diagnostic_default_file_empty() {
let d = Diagnostic::error("A01001", "err", 0..1);
assert!(d.file.is_empty());
}
#[test]
fn test_diagnostic_with_file_overwrites() {
let d = Diagnostic::error("A01001", "err", 0..1)
.with_file("first.assura")
.with_file("second.assura");
assert_eq!(d.file, "second.assura");
}
#[test]
fn test_render_diagnostic_with_suggestion() {
let d = Diagnostic::error("A01002", "missing colon", 8..9).with_suggestion(
"add colon",
8..9,
":",
);
render_diagnostic(&d, "test.assura", "requires x > 0");
}
#[test]
fn test_render_advice_only_suggestion_no_empty_backticks() {
let d = Diagnostic::error("A03006", "requires clause must be Bool", 0..1).with_suggestion(
"Ensure clauses are boolean expressions",
0..1,
"",
);
render_diagnostic(&d, "test.assura", "x");
assert_eq!(d.suggestion.as_ref().unwrap().replacement, "");
}
#[test]
fn test_report_diagnostics_human_empty() {
report_diagnostics_human(&[], "empty.assura", "");
}
#[test]
fn test_render_diagnostic_info_severity() {
let d = Diagnostic {
code: ErrorCode::from("A99999"),
severity: Severity::Info,
message: "informational".into(),
file: String::new(),
primary: 0..1,
secondary: Vec::new(),
suggestion: None,
};
render_diagnostic(&d, "test.assura", "x");
}
#[test]
fn test_severity_equality() {
assert_eq!(Severity::Error, Severity::Error);
assert_ne!(Severity::Error, Severity::Warning);
assert_ne!(Severity::Warning, Severity::Info);
}
#[test]
fn test_severity_copy() {
let s = Severity::Error;
let s2 = s; assert_eq!(s, s2);
}
#[test]
fn test_secondary_label_inequality() {
let a = SecondaryLabel {
span: 0..5,
message: "here".to_string(),
};
let b = SecondaryLabel {
span: 0..5,
message: "there".to_string(),
};
assert_ne!(a, b);
}
#[test]
fn test_secondary_label_serialization() {
let label = SecondaryLabel {
span: 10..20,
message: "declared here".to_string(),
};
let json = serde_json::to_string(&label).unwrap();
assert!(json.contains("declared here"));
assert!(json.contains("\"start\":10"), "span start: {json}");
assert!(json.contains("\"end\":20"), "span end: {json}");
}
#[test]
fn test_suggestion_equality() {
let a = Suggestion {
message: "fix".into(),
span: 0..1,
replacement: ";".into(),
};
let b = Suggestion {
message: "fix".into(),
span: 0..1,
replacement: ";".into(),
};
assert_eq!(a, b);
}
#[test]
fn test_suggestion_inequality() {
let a = Suggestion {
message: "fix".into(),
span: 0..1,
replacement: ";".into(),
};
let b = Suggestion {
message: "fix".into(),
span: 0..1,
replacement: ":".into(),
};
assert_ne!(a, b);
}
#[test]
fn test_diagnostic_full_json_structure() {
let d = Diagnostic::error("A03001", "type mismatch", 10..20)
.with_file("test.assura")
.with_secondary(30..40, "expected here")
.with_secondary(50..60, "found here")
.with_suggestion("change type", 10..20, "Int");
let json = serde_json::to_string_pretty(&d).unwrap();
let val: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(val["code"], "A03001");
assert_eq!(val["severity"], "error");
assert_eq!(val["file"], "test.assura");
assert!(val["secondary"].is_array());
assert_eq!(val["secondary"].as_array().unwrap().len(), 2);
assert!(val["suggestion"].is_object());
assert_eq!(val["suggestion"]["replacement"], "Int");
}
#[test]
fn test_diagnostic_json_no_suggestion() {
let d = Diagnostic::warning("A02007", "unused", 0..5);
let json = serde_json::to_string(&d).unwrap();
let val: serde_json::Value = serde_json::from_str(&json).unwrap();
assert!(val["suggestion"].is_null());
}
#[test]
fn test_error_info_equality() {
let a = ErrorInfo {
code: "A01001",
name: "Unexpected character",
description: "desc",
example: "ex",
fix: "fix",
};
let b = ErrorInfo {
code: "A01001",
name: "Unexpected character",
description: "desc",
example: "ex",
fix: "fix",
};
assert_eq!(a, b);
}
#[test]
fn test_error_info_clone() {
let a = ErrorInfo {
code: "A01001",
name: "test",
description: "desc",
example: "ex",
fix: "fix",
};
let b = a.clone();
assert_eq!(a, b);
}
#[test]
fn test_explain_returns_same_as_catalog_entry() {
let catalog = error_catalog();
for code in &["A01001", "A02001", "A03001", "A05001", "A07003", "A10001"] {
let from_explain = explain(code).expect(&format!("{code} should exist"));
let from_catalog = catalog
.iter()
.find(|e| e.code == *code)
.expect("in catalog");
assert_eq!(from_explain.name, from_catalog.name);
assert_eq!(from_explain.description, from_catalog.description);
}
}
#[test]
fn explain_a07003_covers_must_not() {
let info = explain("A07003").expect("A07003 should exist");
let blob = format!("{} {} {}", info.name, info.description, info.fix);
assert!(
blob.contains("must-not"),
"explain A07003 must mention must-not, got: {blob}"
);
}
#[test]
fn explain_a05102_covers_unconstrained_result() {
let info = explain("A05102").expect("A05102 should exist");
let blob = format!("{} {} {}", info.name, info.description, info.fix);
let blob_lc = blob.to_lowercase();
assert!(
blob_lc.contains("unconstrained") && blob_lc.contains("result"),
"explain A05102 must mention unconstrained `result`, got: {blob}"
);
assert!(
blob.contains("--write-ir") || blob.contains("write-ir") || blob.contains("IR"),
"explain A05102 must mention IR or --write-ir, got: {blob}"
);
assert!(
!info.fix.trim_start().starts_with("No action needed"),
"explain A05102 must not say only No action needed, got: {}",
info.fix
);
}
}