#[path = "support/corpus.rs"]
mod support;
use std::borrow::Cow;
use std::panic::AssertUnwindSafe;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Mutex, MutexGuard, OnceLock, PoisonError, mpsc};
use mf2::native::{self, Catalogs, Error, LocaleSource};
use mf2::{BidiStrategy, Corpus, TimeZone};
use support::{TempDir, files, hello, welcome};
fn installed() -> &'static Corpus {
static CORPUS: OnceLock<&'static Corpus> = OnceLock::new();
let corpus = *CORPUS.get_or_init(|| support::corpus(true).0);
native::install(corpus);
corpus
}
fn app_wide() -> MutexGuard<'static, ()> {
static APP_WIDE: Mutex<()> = Mutex::new(());
APP_WIDE.lock().unwrap_or_else(PoisonError::into_inner)
}
fn said(payload: &(dyn std::any::Any + Send)) -> &str {
payload
.downcast_ref::<String>()
.map(String::as_str)
.or_else(|| payload.downcast_ref::<&str>().copied())
.unwrap_or_default()
}
#[test]
fn install_is_idempotent_and_refuses_another_corpus() {
let corpus = installed();
native::install(corpus);
let (other, _) = support::corpus(true);
let refused = std::panic::catch_unwind(AssertUnwindSafe(|| native::install(other)))
.expect_err("another corpus is refused");
assert!(
said(&*refused).starts_with("mf2: install(): another corpus is already installed"),
"{}",
said(&*refused)
);
assert!(matches!(
native::with_locale(other, "fr", || ()),
Err(Error::AnotherCorpus)
));
native::with_locale(corpus, "fr", || assert_eq!(welcome().to_cow(), "Bienvenue"))
.expect("fr is supported");
}
#[test]
fn a_simple_message_is_borrowed_from_the_executable() {
let corpus = installed();
native::with_locale(corpus, "fr", || {
let text = welcome().to_cow();
let Cow::Borrowed(borrowed) = text else {
panic!("a simple message is borrowed, not {text:?}");
};
assert_eq!(borrowed, "Bienvenue");
let bytes = corpus.catalogs()[1]
.bytes()
.expect("the catalog is embedded");
assert!(
bytes.as_ptr_range().contains(&borrowed.as_ptr()),
"the text is the embedded catalog's own bytes"
);
let formatted = files(3).to_cow();
assert!(matches!(formatted, Cow::Owned(_)), "{formatted:?}");
assert_eq!(formatted, "3 fichiers");
assert_eq!(format!("{}", welcome()), "Bienvenue");
assert_eq!(format!("[{:<12}]", welcome()), "[Bienvenue ]");
assert_eq!(format!("[{:>12}]", files(3)), "[ 3 fichiers]");
assert_eq!(welcome().to_string(), "Bienvenue");
assert_eq!(String::from(welcome()), "Bienvenue");
assert_eq!(files(2).to_plain_string(), "2 fichiers");
})
.expect("fr is supported");
}
#[test]
fn pinned_threads_format_in_their_own_language_in_parallel() {
let corpus = installed();
let _app_wide = app_wide();
let stop = AtomicBool::new(false);
std::thread::scope(|s| {
s.spawn(|| {
let zones = [
TimeZone::UTC,
TimeZone::named("Europe/Paris").expect("a zone"),
];
let mut i = 0_usize;
while !stop.load(Ordering::Relaxed) {
native::set_locale(["en", "fr"][i % 2]).expect("supported");
native::set_bidi([BidiStrategy::Default, BidiStrategy::None][i % 2]);
native::set_time_zone(zones[i % 2]);
i += 1;
std::thread::yield_now();
}
});
let pinned: Vec<_> = (0..16)
.map(|t| {
let (tag, simple, number, greeting) = if t % 2 == 0 {
("en", "Welcome", "2 files", "Hello, Ada!")
} else {
("fr", "Bienvenue", "2 fichiers", "Bonjour, Ada !")
};
s.spawn(move || {
native::with_locale(corpus, tag, || {
for _ in 0..2_000 {
assert_eq!(native::locale(), tag);
assert_eq!(welcome().to_cow(), simple);
assert_eq!(format!("{}", welcome()), simple);
assert_eq!(files(2).to_string(), number);
assert_eq!(hello("Ada").to_plain_string(), greeting);
}
})
.expect("supported");
})
})
.collect();
for thread in pinned {
thread
.join()
.expect("each pinned thread formats in its own language");
}
stop.store(true, Ordering::Relaxed);
});
}
#[test]
fn set_locale_is_seen_by_another_thread_on_its_next_format() {
installed();
let _app_wide = app_wide();
let (ask, asked) = mpsc::channel::<()>();
let (answer, answers) = mpsc::channel::<String>();
std::thread::scope(|s| {
s.spawn(move || {
for () in asked {
answer
.send(welcome().to_string())
.expect("the test listens");
}
});
let next = || {
ask.send(()).expect("the worker listens");
answers.recv().expect("the worker answers")
};
native::set_locale("fr").expect("fr is supported");
assert_eq!(next(), "Bienvenue");
assert_eq!(native::locale_source(), LocaleSource::Explicit);
native::set_locale("en_GB.UTF-8").expect("en-GB falls back to en");
assert_eq!(next(), "Welcome");
assert_eq!(native::locale(), "en");
assert!(matches!(
native::set_locale("de"),
Err(Error::UnknownLocale(tag)) if tag == "de"
));
assert_eq!(next(), "Welcome", "an unsupported locale changes nothing");
drop(ask);
});
}
#[test]
fn with_locale_nests_and_is_restored_when_its_body_unwinds() {
let corpus = installed();
native::with_locale(corpus, "fr", || {
assert_eq!(native::locale(), "fr");
native::with_locale(corpus, "EN", || {
assert_eq!(native::locale(), "en");
assert_eq!(welcome().to_cow(), "Welcome");
})
.expect("en is supported");
assert_eq!(native::locale(), "fr");
let unwound = std::panic::catch_unwind(AssertUnwindSafe(|| {
native::with_locale(corpus, "en", || panic!("inside with_locale"))
}));
assert_eq!(
said(&*unwound.expect_err("the body panicked")),
"inside with_locale"
);
assert_eq!(native::locale(), "fr", "restored as the body unwound");
assert_eq!(welcome().to_cow(), "Bienvenue");
})
.expect("fr is supported");
assert!(matches!(
native::with_locale(corpus, "de", || ()),
Err(Error::UnknownLocale(tag)) if tag == "de"
));
}
#[test]
fn the_settings_reach_every_thread_on_its_next_format() {
installed();
let _app_wide = app_wide();
native::set_locale("en").expect("en is supported");
let (ask, asked) = mpsc::channel::<()>();
let (answer, answers) = mpsc::channel();
std::thread::scope(|s| {
s.spawn(move || {
for () in asked {
let shown = (
hello("Ada").to_string(),
hello("Ada").to_plain_string(),
native::bidi(),
native::time_zone(),
);
answer.send(shown).expect("the test listens");
}
});
let next = || {
ask.send(()).expect("the worker listens");
answers.recv().expect("the worker answers")
};
native::set_bidi(BidiStrategy::None);
let (text, plain, bidi, _) = next();
assert_eq!(
(text.as_str(), plain.as_str()),
("Hello, Ada!", "Hello, Ada!")
);
assert_eq!(bidi, BidiStrategy::None);
native::set_bidi(BidiStrategy::Default);
let (text, plain, bidi, _) = next();
assert_eq!(
text, "Hello, \u{2068}Ada\u{2069}!",
"isolated as the setting says"
);
assert_eq!(plain, "Hello, Ada!", "never isolated");
assert_eq!(bidi, BidiStrategy::Default);
let tokyo = TimeZone::named("Asia/Tokyo").expect("a zone");
native::set_time_zone(tokyo);
assert_eq!(next().3, tokyo);
native::set_bidi(BidiStrategy::None);
drop(ask);
});
}
#[test]
fn catalogs_format_in_the_language_named_with_no_globals() {
let (corpus, raw) = support::corpus(true);
let mut catalogs = Catalogs::embedded(corpus).expect("the catalogs load");
assert_eq!(catalogs.locales().collect::<Vec<_>>(), ["en", "fr"]);
assert_eq!(catalogs.format("fr_CA.UTF-8", &welcome()), "Bienvenue");
assert_eq!(catalogs.format("en", &files(3)), "3 files");
assert_eq!(
catalogs.format("de", &welcome()),
"Welcome",
"an unsupported language is the source's"
);
assert_eq!(catalogs.bidi(), BidiStrategy::None);
assert_eq!(catalogs.format("en", &hello("Ada")), "Hello, Ada!");
catalogs.set_bidi(BidiStrategy::Default);
assert_eq!(
catalogs.format("en", &hello("Ada")),
"Hello, \u{2068}Ada\u{2069}!"
);
let (files_corpus, _) = support::corpus(false);
let (name, bytes) = &raw[0];
let partial = TempDir::with("ambient-partial", &[(name, bytes)]);
let loaded =
Catalogs::from_directory(files_corpus, &partial.0).expect("the source's is enough");
assert_eq!(loaded.locales().collect::<Vec<_>>(), ["en"]);
assert_eq!(loaded.format("fr", &welcome()), "Welcome");
let empty = TempDir::with("ambient-empty", &[]);
assert!(matches!(
Catalogs::from_directory(files_corpus, &empty.0),
Err(Error::Io { .. })
));
}