83412fde83
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
112 lines
4.2 KiB
Rust
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");
|
|
}
|