Files
Marcos Paulo 83412fde83 feat(i18n): wave 5 i18n
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-13 20:01:23 -03:00

112 lines
4.2 KiB
Rust

//! Integration tests: coverage reports, the embedded base locale and
//! hostile-input robustness.
use notescraft_i18n::{Catalog, Error, coverage, placeholders, placeholders_match};
/// Parses or fails the test with the error text.
fn parse(locale: &str, source: &str) -> Catalog {
match Catalog::from_toml(locale, source) {
Ok(cat) => cat,
Err(e) => {
assert_eq!(e.to_string(), format!("expected a catalog, got: {e:?}"));
Catalog::default()
}
}
}
#[test]
fn coverage_reports_missing_extra_and_placeholder_mismatches() {
let en = parse("en", "greet = \"Hello {name}\"\nbye = \"Bye\"\nnote = \"plain\"\n");
let fr = parse("fr", "greet = \"Bonjour {nom}\"\nnote = \"simple\"\nextra = \"de trop\"\n");
let report = coverage(&en, &fr);
assert_eq!(report.missing, ["bye".to_owned()]);
assert_eq!(report.extra, ["extra".to_owned()]);
assert_eq!(report.mismatched_placeholders, ["greet".to_owned()], "{{name}} vs {{nom}} is a mismatch");
assert!((report.percent - 200.0 / 3.0).abs() < 1e-9, "2 of 3 base keys covered: {}", report.percent);
}
#[test]
fn coverage_report_is_cloneable_and_comparable() {
let en = parse("en", "a = \"x\"\n");
let fr = parse("fr", "a = \"y\"\n");
let report = coverage(&en, &fr);
assert_eq!(report, report.clone(), "Clone + PartialEq for callers");
assert_ne!(format!("{report:?}"), "");
}
#[test]
fn placeholders_listing_and_matching() {
assert_eq!(placeholders("{a} {b} {a}"), ["a".to_owned(), "b".to_owned()]);
assert!(placeholders_match("{a} {b} twice {a}", "{b} {a}"));
assert!(!placeholders_match("{a}", "{a} {b}"));
assert!(placeholders_match("", "{}"));
}
#[test]
fn embedded_base_locale_parses_cleanly() {
let src = include_str!("../locales/en.toml");
let cat = parse("en", src);
assert!(cat.len() >= 60, "embedded base has {} keys, want >= 60", cat.len());
assert_eq!(cat.locale(), "en");
for key in [
"menus.file",
"menus.edit",
"menus.view",
"menus.help",
"buttons.save",
"palette.open",
"palette.placeholder",
"settings.general",
"assistant.title",
"assistant.send",
"errors.validation.required",
"errors.io",
"callouts.note",
"callouts.tip",
"empty.search",
"notes.count.other",
"files.selected.one",
] {
assert!(cat.contains(key), "missing base key {key}");
let value = cat.get(key).unwrap_or_default();
assert!(!value.is_empty(), "key {key} has an empty value");
}
// from_toml already rejects duplicates and invalid characters, so a
// clean load proves the embedded file has none.
let keys: Vec<String> = cat.keys().map(ToOwned::to_owned).collect();
let unique: usize = keys.iter().collect::<std::collections::BTreeSet<_>>().len();
assert_eq!(unique, keys.len(), "no duplicate keys in en.toml");
}
#[test]
fn hostile_deep_tables_error_without_panicking() {
let path = (0..400).map(|i| format!("a{i:03}")).collect::<Vec<_>>().join(".");
let src = format!("[{path}]\nleaf = \"x\"\n");
let outcome = std::panic::catch_unwind(|| Catalog::from_toml("en", &src));
match outcome {
Ok(Err(Error::TooDeep(_) | Error::Parse(_))) => {}
Ok(other) => assert!(other.is_err(), "deep tables must be Err, got {other:?}"),
Err(_) => assert_eq!("caught a panic", "deep tables must not panic"),
}
}
#[test]
fn hostile_oversize_input_errors_without_panicking() {
let one_mb = "x".repeat(1_000_000);
let src = format!("big = \"{one_mb}{one_mb}{one_mb}\""); // 3 MB > 2 MiB cap
let outcome = std::panic::catch_unwind(|| Catalog::from_toml("en", &src));
match outcome {
Ok(Err(Error::TooLarge { .. })) => {}
Ok(other) => assert!(other.is_err(), "oversize input must be Err, got {other:?}"),
Err(_) => assert_eq!("caught a panic", "oversize input must not panic"),
}
}
#[test]
fn one_mb_string_value_loads_without_panic() {
let one_mb = "x".repeat(1_000_000);
let src = format!("big = \"{one_mb}\""); // inside the 2 MiB cap
let parsed = parse("en", &src);
assert_eq!(parsed.get("big"), Some(one_mb.as_str()), "a legal 1 MB value loads verbatim");
}