#![cfg(feature = "hot-reload")]
use std::io::Write;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex, MutexGuard, OnceLock};
use std::thread::sleep;
use std::time::Duration;
use lang_lib::{ChangeKind, Lang, LangChangeEvent};
use tempfile::TempDir;
fn test_guard() -> MutexGuard<'static, ()> {
static TEST_MUTEX: OnceLock<Mutex<()>> = OnceLock::new();
TEST_MUTEX
.get_or_init(|| Mutex::new(()))
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
fn reset_lang() {
Lang::unwatch();
for locale in Lang::loaded() {
Lang::unload(locale);
}
Lang::set_path("locales");
Lang::set_locale("en");
Lang::set_fallbacks(vec!["en".to_string()]);
}
fn write_locale(dir: &TempDir, locale: &str, content: &str) {
let mut f = std::fs::File::create(dir.path().join(format!("{locale}.toml")))
.expect("create locale file");
write!(f, "{content}").expect("write locale content");
f.sync_all().expect("sync locale file");
}
#[test]
fn watcher_picks_up_file_changes_and_fires_reloaded() {
let _guard = test_guard();
reset_lang();
let dir = tempfile::tempdir().expect("tempdir");
write_locale(&dir, "watch_a", "greeting = \"v1\"");
let path_str = dir.path().to_str().expect("utf8 path").to_owned();
Lang::set_path(path_str.clone());
Lang::load("watch_a").expect("initial load");
assert_eq!(Lang::translate("greeting", Some("watch_a"), None), "v1");
let events: Arc<Mutex<Vec<LangChangeEvent>>> = Arc::new(Mutex::new(Vec::new()));
let sink = Arc::clone(&events);
let id = Lang::on_change(move |event| {
sink.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.push(*event);
});
Lang::watch(&path_str).expect("start watcher");
sleep(Duration::from_millis(200));
write_locale(&dir, "watch_a", "greeting = \"v2\"");
let deadline = std::time::Instant::now() + Duration::from_secs(2);
loop {
if Lang::translate("greeting", Some("watch_a"), None) == "v2" {
break;
}
if std::time::Instant::now() >= deadline {
panic!(
"watcher did not pick up change within deadline; current value is {}",
Lang::translate("greeting", Some("watch_a"), None)
);
}
sleep(Duration::from_millis(50));
}
let captured = events
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone();
assert!(
captured
.iter()
.any(|e| e.locale == "watch_a" && e.kind == ChangeKind::Reloaded),
"expected a Reloaded event for watch_a, got {captured:?}"
);
Lang::unwatch();
assert!(Lang::off_change(id));
}
#[test]
fn hot_reload_storage_drops_arc_str_on_reload() {
let _guard = test_guard();
reset_lang();
let dir = tempfile::tempdir().expect("tempdir");
write_locale(&dir, "reclaim_a", "greeting = \"v1\"");
let path_str = dir.path().to_str().expect("utf8 path").to_owned();
Lang::set_path(path_str.clone());
Lang::load("reclaim_a").expect("load");
let held: std::sync::Arc<str> = Lang::translate_arc("greeting", Some("reclaim_a"), None);
assert_eq!(held.as_ref(), "v1");
assert!(
std::sync::Arc::strong_count(&held) >= 2,
"expected the store and the local handle to both reference the Arc"
);
write_locale(&dir, "reclaim_a", "greeting = \"v2\"");
Lang::load_from("reclaim_a", &path_str).expect("reload");
let next: std::sync::Arc<str> = Lang::translate_arc("greeting", Some("reclaim_a"), None);
assert_eq!(next.as_ref(), "v2");
assert!(
!std::sync::Arc::ptr_eq(&held, &next),
"reload should produce a distinct Arc<str> backing the value"
);
assert_eq!(
std::sync::Arc::strong_count(&held),
1,
"after reload, the only remaining reference to the old Arc<str> \
should be the local `held` handle; the store reclaimed its copy"
);
}
#[test]
fn unwatch_stops_event_delivery() {
let _guard = test_guard();
reset_lang();
let dir = tempfile::tempdir().expect("tempdir");
write_locale(&dir, "watch_b", "k = \"a\"");
let path_str = dir.path().to_str().expect("utf8 path").to_owned();
Lang::set_path(path_str.clone());
Lang::load("watch_b").expect("load");
let count = Arc::new(AtomicUsize::new(0));
let sink = Arc::clone(&count);
let id = Lang::on_change(move |_| {
let _ = sink.fetch_add(1, Ordering::Relaxed);
});
Lang::watch(&path_str).expect("start watcher");
sleep(Duration::from_millis(200));
Lang::unwatch();
let before = count.load(Ordering::Relaxed);
write_locale(&dir, "watch_b", "k = \"b\"");
sleep(Duration::from_millis(500));
let after = count.load(Ordering::Relaxed);
assert_eq!(
before, after,
"no events should fire after Lang::unwatch (before={before}, after={after})"
);
assert!(Lang::off_change(id));
}