summaryrefslogtreecommitdiffstats
path: root/intl/l10n/rust/l10nregistry-tests
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /intl/l10n/rust/l10nregistry-tests
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'intl/l10n/rust/l10nregistry-tests')
-rw-r--r--intl/l10n/rust/l10nregistry-tests/Cargo.toml43
-rw-r--r--intl/l10n/rust/l10nregistry-tests/benches/localization.rs70
-rw-r--r--intl/l10n/rust/l10nregistry-tests/benches/preferences.rs65
-rw-r--r--intl/l10n/rust/l10nregistry-tests/benches/registry.rs133
-rw-r--r--intl/l10n/rust/l10nregistry-tests/benches/solver.rs120
-rw-r--r--intl/l10n/rust/l10nregistry-tests/benches/source.rs60
-rw-r--r--intl/l10n/rust/l10nregistry-tests/src/lib.rs324
-rw-r--r--intl/l10n/rust/l10nregistry-tests/src/solver/mod.rs38
-rw-r--r--intl/l10n/rust/l10nregistry-tests/src/solver/scenarios.rs151
-rw-r--r--intl/l10n/rust/l10nregistry-tests/tests/localization.rs201
-rw-r--r--intl/l10n/rust/l10nregistry-tests/tests/registry.rs304
-rw-r--r--intl/l10n/rust/l10nregistry-tests/tests/scenarios_async.rs109
-rw-r--r--intl/l10n/rust/l10nregistry-tests/tests/scenarios_sync.rs107
-rw-r--r--intl/l10n/rust/l10nregistry-tests/tests/source.rs305
-rw-r--r--intl/l10n/rust/l10nregistry-tests/tests/tokio.rs65
15 files changed, 2095 insertions, 0 deletions
diff --git a/intl/l10n/rust/l10nregistry-tests/Cargo.toml b/intl/l10n/rust/l10nregistry-tests/Cargo.toml
new file mode 100644
index 0000000000..12ffbfdc66
--- /dev/null
+++ b/intl/l10n/rust/l10nregistry-tests/Cargo.toml
@@ -0,0 +1,43 @@
+[package]
+name = "l10nregistry-tests"
+version = "0.3.0"
+authors = ["Zibi Braniecki <gandalf@mozilla.com>"]
+license = "Apache-2.0/MIT"
+edition = "2018"
+
+[dependencies]
+l10nregistry = { path = "../l10nregistry-rs", features = ["test-fluent"] }
+async-trait = "0.1"
+fluent-fallback = "0.7.0"
+fluent-testing = { version = "0.0.3", features = ["sync", "async"] }
+tokio = { version = "1.0", features = ["rt-multi-thread", "macros"] }
+unic-langid = { version = "0.9", features = ["macros"] }
+
+[dev-dependencies]
+criterion = "0.3"
+fluent-bundle = "0.15.2"
+futures = "0.3"
+serial_test = "0.6"
+
+[features]
+default = []
+
+[[bench]]
+name = "preferences"
+harness = false
+
+[[bench]]
+name = "localization"
+harness = false
+
+[[bench]]
+name = "source"
+harness = false
+
+[[bench]]
+name = "solver"
+harness = false
+
+[[bench]]
+name = "registry"
+harness = false
diff --git a/intl/l10n/rust/l10nregistry-tests/benches/localization.rs b/intl/l10n/rust/l10nregistry-tests/benches/localization.rs
new file mode 100644
index 0000000000..b946d2cf6c
--- /dev/null
+++ b/intl/l10n/rust/l10nregistry-tests/benches/localization.rs
@@ -0,0 +1,70 @@
+use criterion::criterion_group;
+use criterion::criterion_main;
+use criterion::Criterion;
+
+use fluent_bundle::FluentArgs;
+use fluent_fallback::{types::L10nKey, Localization};
+use fluent_testing::get_scenarios;
+use l10nregistry_tests::TestFileFetcher;
+
+fn preferences_bench(c: &mut Criterion) {
+ let fetcher = TestFileFetcher::new();
+
+ let mut group = c.benchmark_group("localization/scenarios");
+
+ for scenario in get_scenarios() {
+ let res_ids = scenario.res_ids.clone();
+ let l10n_keys: Vec<(String, Option<FluentArgs>)> = scenario
+ .queries
+ .iter()
+ .map(|q| {
+ (
+ q.input.id.clone(),
+ q.input.args.as_ref().map(|args| {
+ let mut result = FluentArgs::new();
+ for arg in args.as_slice() {
+ result.set(arg.id.clone(), arg.value.clone());
+ }
+ result
+ }),
+ )
+ })
+ .collect();
+
+ group.bench_function(format!("{}/format_value_sync", scenario.name), |b| {
+ b.iter(|| {
+ let (env, reg) = fetcher.get_registry_and_environment(&scenario);
+ let mut errors = vec![];
+
+ let loc = Localization::with_env(res_ids.clone(), true, env.clone(), reg.clone());
+ let bundles = loc.bundles();
+
+ for key in l10n_keys.iter() {
+ bundles.format_value_sync(&key.0, key.1.as_ref(), &mut errors);
+ }
+ })
+ });
+
+ let keys: Vec<L10nKey> = l10n_keys
+ .into_iter()
+ .map(|key| L10nKey {
+ id: key.0.into(),
+ args: key.1,
+ })
+ .collect();
+ group.bench_function(format!("{}/format_messages_sync", scenario.name), |b| {
+ b.iter(|| {
+ let (env, reg) = fetcher.get_registry_and_environment(&scenario);
+ let mut errors = vec![];
+ let loc = Localization::with_env(res_ids.clone(), true, env.clone(), reg.clone());
+ let bundles = loc.bundles();
+ bundles.format_messages_sync(&keys, &mut errors);
+ })
+ });
+ }
+
+ group.finish();
+}
+
+criterion_group!(benches, preferences_bench);
+criterion_main!(benches);
diff --git a/intl/l10n/rust/l10nregistry-tests/benches/preferences.rs b/intl/l10n/rust/l10nregistry-tests/benches/preferences.rs
new file mode 100644
index 0000000000..ea657ad474
--- /dev/null
+++ b/intl/l10n/rust/l10nregistry-tests/benches/preferences.rs
@@ -0,0 +1,65 @@
+use criterion::criterion_group;
+use criterion::criterion_main;
+use criterion::Criterion;
+
+use fluent_testing::get_scenarios;
+use l10nregistry_tests::TestFileFetcher;
+
+use unic_langid::LanguageIdentifier;
+
+fn preferences_bench(c: &mut Criterion) {
+ let fetcher = TestFileFetcher::new();
+
+ let mut group = c.benchmark_group("registry/scenarios");
+
+ for scenario in get_scenarios() {
+ let res_ids = scenario.res_ids.clone();
+
+ let locales: Vec<LanguageIdentifier> = scenario
+ .locales
+ .iter()
+ .map(|l| l.parse().unwrap())
+ .collect();
+
+ group.bench_function(format!("{}/sync/first_bundle", scenario.name), |b| {
+ b.iter(|| {
+ let reg = fetcher.get_registry(&scenario);
+ let mut bundles =
+ reg.generate_bundles_sync(locales.clone().into_iter(), res_ids.clone());
+ for _ in 0..locales.len() {
+ if bundles.next().is_some() {
+ break;
+ }
+ }
+ })
+ });
+
+ #[cfg(feature = "tokio")]
+ {
+ use futures::stream::StreamExt;
+
+ let rt = tokio::runtime::Runtime::new().unwrap();
+
+ group.bench_function(&format!("{}/async/first_bundle", scenario.name), |b| {
+ b.iter(|| {
+ rt.block_on(async {
+ let reg = fetcher.get_registry(&scenario);
+
+ let mut bundles =
+ reg.generate_bundles(locales.clone().into_iter(), res_ids.clone());
+ for _ in 0..locales.len() {
+ if bundles.next().await.is_some() {
+ break;
+ }
+ }
+ });
+ })
+ });
+ }
+ }
+
+ group.finish();
+}
+
+criterion_group!(benches, preferences_bench);
+criterion_main!(benches);
diff --git a/intl/l10n/rust/l10nregistry-tests/benches/registry.rs b/intl/l10n/rust/l10nregistry-tests/benches/registry.rs
new file mode 100644
index 0000000000..3d456092f1
--- /dev/null
+++ b/intl/l10n/rust/l10nregistry-tests/benches/registry.rs
@@ -0,0 +1,133 @@
+use criterion::criterion_group;
+use criterion::criterion_main;
+use criterion::Criterion;
+
+use futures::stream::StreamExt;
+use l10nregistry::source::ResourceId;
+use l10nregistry_tests::{FileSource, RegistrySetup, TestFileFetcher};
+use unic_langid::LanguageIdentifier;
+
+fn get_paths() -> Vec<ResourceId> {
+ let paths: Vec<&'static str> = vec![
+ "branding/brand.ftl",
+ "browser/sanitize.ftl",
+ "browser/preferences/blocklists.ftl",
+ "browser/preferences/colors.ftl",
+ "browser/preferences/selectBookmark.ftl",
+ "browser/preferences/connection.ftl",
+ "browser/preferences/addEngine.ftl",
+ "browser/preferences/siteDataSettings.ftl",
+ "browser/preferences/fonts.ftl",
+ "browser/preferences/languages.ftl",
+ "browser/preferences/preferences.ftl",
+ "security/certificates/certManager.ftl",
+ "security/certificates/deviceManager.ftl",
+ "toolkit/global/textActions.ftl",
+ "toolkit/printing/printUI.ftl",
+ "toolkit/updates/history.ftl",
+ "toolkit/featuregates/features.ftl",
+ ];
+
+ paths.into_iter().map(ResourceId::from).collect()
+}
+
+fn registry_bench(c: &mut Criterion) {
+ let en_us: LanguageIdentifier = "en-US".parse().unwrap();
+ let mut group = c.benchmark_group("non-metasource");
+
+ let setup = RegistrySetup::new(
+ "test",
+ vec![
+ FileSource::new("toolkit", None, vec![en_us.clone()], "toolkit/{locale}/"),
+ FileSource::new("browser", None, vec![en_us.clone()], "browser/{locale}/"),
+ FileSource::new("toolkit", None, vec![en_us.clone()], "toolkit/{locale}/"),
+ FileSource::new("browser", None, vec![en_us.clone()], "browser/{locale}/"),
+ ],
+ vec![en_us.clone()],
+ );
+ let fetcher = TestFileFetcher::new();
+ let (_, reg) = fetcher.get_registry_and_environment(setup);
+
+ group.bench_function(&format!("serial",), |b| {
+ b.iter(|| {
+ let lang_ids = vec![en_us.clone()];
+ let mut i = reg.generate_bundles_sync(lang_ids.into_iter(), get_paths());
+ while let Some(_) = i.next() {}
+ })
+ });
+
+ let rt = tokio::runtime::Runtime::new().unwrap();
+ group.bench_function(&format!("parallel",), |b| {
+ b.iter(|| {
+ let lang_ids = vec![en_us.clone()];
+ let mut i = reg.generate_bundles(lang_ids.into_iter(), get_paths());
+ rt.block_on(async { while let Some(_) = i.as_mut().unwrap().next().await {} });
+ })
+ });
+
+ group.finish();
+}
+
+fn registry_metasource_bench(c: &mut Criterion) {
+ let en_us: LanguageIdentifier = "en-US".parse().unwrap();
+ let mut group = c.benchmark_group("metasource");
+
+ let setup = RegistrySetup::new(
+ "test",
+ vec![
+ FileSource::new(
+ "toolkit",
+ Some("app"),
+ vec![en_us.clone()],
+ "toolkit/{locale}/",
+ ),
+ FileSource::new(
+ "browser",
+ Some("app"),
+ vec![en_us.clone()],
+ "browser/{locale}/",
+ ),
+ FileSource::new(
+ "toolkit",
+ Some("langpack"),
+ vec![en_us.clone()],
+ "toolkit/{locale}/",
+ ),
+ FileSource::new(
+ "browser",
+ Some("langpack"),
+ vec![en_us.clone()],
+ "browser/{locale}/",
+ ),
+ ],
+ vec![en_us.clone()],
+ );
+ let fetcher = TestFileFetcher::new();
+ let (_, reg) = fetcher.get_registry_and_environment(setup);
+
+ group.bench_function(&format!("serial",), |b| {
+ b.iter(|| {
+ let lang_ids = vec![en_us.clone()];
+ let mut i = reg.generate_bundles_sync(lang_ids.into_iter(), get_paths());
+ while let Some(_) = i.next() {}
+ })
+ });
+
+ let rt = tokio::runtime::Runtime::new().unwrap();
+ group.bench_function(&format!("parallel",), |b| {
+ b.iter(|| {
+ let lang_ids = vec![en_us.clone()];
+ let mut i = reg.generate_bundles(lang_ids.into_iter(), get_paths());
+ rt.block_on(async { while let Some(_) = i.as_mut().unwrap().next().await {} });
+ })
+ });
+
+ group.finish();
+}
+
+criterion_group!(
+ name = benches;
+ config = Criterion::default().sample_size(10);
+ targets = registry_bench, registry_metasource_bench
+);
+criterion_main!(benches);
diff --git a/intl/l10n/rust/l10nregistry-tests/benches/solver.rs b/intl/l10n/rust/l10nregistry-tests/benches/solver.rs
new file mode 100644
index 0000000000..515b5ef1f9
--- /dev/null
+++ b/intl/l10n/rust/l10nregistry-tests/benches/solver.rs
@@ -0,0 +1,120 @@
+use criterion::criterion_group;
+use criterion::criterion_main;
+use criterion::Criterion;
+
+use futures::stream::Collect;
+use futures::stream::FuturesOrdered;
+use futures::StreamExt;
+use l10nregistry_tests::solver::get_scenarios;
+use l10nregistry::solver::{AsyncTester, ParallelProblemSolver, SerialProblemSolver, SyncTester};
+use std::future::Future;
+use std::pin::Pin;
+use std::task::{Context, Poll};
+
+pub struct MockTester {
+ values: Vec<Vec<bool>>,
+}
+
+impl SyncTester for MockTester {
+ fn test_sync(&self, res_idx: usize, source_idx: usize) -> bool {
+ self.values[res_idx][source_idx]
+ }
+}
+
+pub struct SingleTestResult(bool);
+
+impl Future for SingleTestResult {
+ type Output = bool;
+
+ fn poll(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Self::Output> {
+ self.0.into()
+ }
+}
+
+pub type ResourceSetStream = Collect<FuturesOrdered<SingleTestResult>, Vec<bool>>;
+pub struct TestResult(ResourceSetStream);
+
+impl std::marker::Unpin for TestResult {}
+
+impl Future for TestResult {
+ type Output = Vec<bool>;
+
+ fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
+ let pinned = Pin::new(&mut self.0);
+ pinned.poll(cx)
+ }
+}
+
+impl AsyncTester for MockTester {
+ type Result = TestResult;
+
+ fn test_async(&self, query: Vec<(usize, usize)>) -> Self::Result {
+ let futures = query
+ .into_iter()
+ .map(|(res_idx, source_idx)| SingleTestResult(self.test_sync(res_idx, source_idx)))
+ .collect::<Vec<_>>();
+ TestResult(futures.into_iter().collect::<FuturesOrdered<_>>().collect())
+ }
+}
+
+struct TestStream<'t> {
+ solver: ParallelProblemSolver<MockTester>,
+ tester: &'t MockTester,
+}
+
+impl<'t> TestStream<'t> {
+ pub fn new(solver: ParallelProblemSolver<MockTester>, tester: &'t MockTester) -> Self {
+ Self { solver, tester }
+ }
+}
+
+impl<'t> futures::stream::Stream for TestStream<'t> {
+ type Item = Vec<usize>;
+
+ fn poll_next(
+ mut self: std::pin::Pin<&mut Self>,
+ cx: &mut std::task::Context<'_>,
+ ) -> std::task::Poll<Option<Self::Item>> {
+ let tester = self.tester;
+ let solver = &mut self.solver;
+ let pinned = std::pin::Pin::new(solver);
+ pinned
+ .try_poll_next(cx, tester, false)
+ .map(|v| v.ok().flatten())
+ }
+}
+
+fn solver_bench(c: &mut Criterion) {
+ let scenarios = get_scenarios();
+
+ let mut group = c.benchmark_group("solver");
+
+ for scenario in scenarios {
+ let tester = MockTester {
+ values: scenario.values.clone(),
+ };
+
+ group.bench_function(&format!("serial/{}", &scenario.name), |b| {
+ b.iter(|| {
+ let mut gen = SerialProblemSolver::new(scenario.width, scenario.depth);
+ while let Ok(Some(_)) = gen.try_next(&tester, false) {}
+ })
+ });
+
+ {
+ let rt = tokio::runtime::Runtime::new().unwrap();
+
+ group.bench_function(&format!("parallel/{}", &scenario.name), |b| {
+ b.iter(|| {
+ let gen = ParallelProblemSolver::new(scenario.width, scenario.depth);
+ let mut t = TestStream::new(gen, &tester);
+ rt.block_on(async { while let Some(_) = t.next().await {} });
+ })
+ });
+ }
+ }
+ group.finish();
+}
+
+criterion_group!(benches, solver_bench);
+criterion_main!(benches);
diff --git a/intl/l10n/rust/l10nregistry-tests/benches/source.rs b/intl/l10n/rust/l10nregistry-tests/benches/source.rs
new file mode 100644
index 0000000000..040bcc8d28
--- /dev/null
+++ b/intl/l10n/rust/l10nregistry-tests/benches/source.rs
@@ -0,0 +1,60 @@
+use criterion::criterion_group;
+use criterion::criterion_main;
+use criterion::Criterion;
+
+use fluent_testing::get_scenarios;
+use l10nregistry_tests::TestFileFetcher;
+
+use unic_langid::LanguageIdentifier;
+
+fn get_locales<S>(input: &[S]) -> Vec<LanguageIdentifier>
+where
+ S: AsRef<str>,
+{
+ input.iter().map(|s| s.as_ref().parse().unwrap()).collect()
+}
+
+fn source_bench(c: &mut Criterion) {
+ let fetcher = TestFileFetcher::new();
+
+ let mut group = c.benchmark_group("source/scenarios");
+
+ for scenario in get_scenarios() {
+ let res_ids = scenario.res_ids.clone();
+
+ let locales: Vec<LanguageIdentifier> = get_locales(&scenario.locales);
+
+ let sources: Vec<_> = scenario
+ .file_sources
+ .iter()
+ .map(|s| {
+ fetcher.get_test_file_source(&s.name, None, get_locales(&s.locales), &s.path_scheme)
+ })
+ .collect();
+
+ group.bench_function(format!("{}/has_file", scenario.name), |b| {
+ b.iter(|| {
+ for source in &sources {
+ for res_id in &res_ids {
+ source.has_file(&locales[0], &res_id);
+ }
+ }
+ })
+ });
+
+ group.bench_function(format!("{}/sync/fetch_file_sync", scenario.name), |b| {
+ b.iter(|| {
+ for source in &sources {
+ for res_id in &res_ids {
+ source.fetch_file_sync(&locales[0], &res_id, false);
+ }
+ }
+ })
+ });
+ }
+
+ group.finish();
+}
+
+criterion_group!(benches, source_bench);
+criterion_main!(benches);
diff --git a/intl/l10n/rust/l10nregistry-tests/src/lib.rs b/intl/l10n/rust/l10nregistry-tests/src/lib.rs
new file mode 100644
index 0000000000..0194775b22
--- /dev/null
+++ b/intl/l10n/rust/l10nregistry-tests/src/lib.rs
@@ -0,0 +1,324 @@
+use l10nregistry::env::ErrorReporter;
+use l10nregistry::errors::L10nRegistryError;
+use l10nregistry::fluent::FluentBundle;
+use l10nregistry::registry::BundleAdapter;
+use l10nregistry::registry::L10nRegistry;
+use l10nregistry::source::FileFetcher;
+use async_trait::async_trait;
+use fluent_fallback::{env::LocalesProvider, types::ResourceId};
+use fluent_testing::MockFileSystem;
+use std::cell::RefCell;
+use std::rc::Rc;
+use unic_langid::LanguageIdentifier;
+
+pub mod solver;
+
+pub struct RegistrySetup {
+ pub name: String,
+ pub file_sources: Vec<FileSource>,
+ pub locales: Vec<LanguageIdentifier>,
+}
+
+pub struct FileSource {
+ pub name: String,
+ pub metasource: String,
+ pub locales: Vec<LanguageIdentifier>,
+ pub path_scheme: String,
+}
+
+#[derive(Clone)]
+pub struct MockBundleAdapter;
+
+impl BundleAdapter for MockBundleAdapter {
+ fn adapt_bundle(&self, _bundle: &mut FluentBundle) {}
+}
+
+impl FileSource {
+ pub fn new<S>(
+ name: S,
+ metasource: Option<S>,
+ locales: Vec<LanguageIdentifier>,
+ path_scheme: S,
+ ) -> Self
+ where
+ S: ToString,
+ {
+ let metasource = match metasource {
+ Some(s) => s.to_string(),
+ None => String::default(),
+ };
+
+ Self {
+ name: name.to_string(),
+ metasource,
+ locales,
+ path_scheme: path_scheme.to_string(),
+ }
+ }
+}
+
+impl RegistrySetup {
+ pub fn new(
+ name: &str,
+ file_sources: Vec<FileSource>,
+ locales: Vec<LanguageIdentifier>,
+ ) -> Self {
+ Self {
+ name: name.to_string(),
+ file_sources,
+ locales,
+ }
+ }
+}
+
+impl From<fluent_testing::scenarios::structs::Scenario> for RegistrySetup {
+ fn from(s: fluent_testing::scenarios::structs::Scenario) -> Self {
+ Self {
+ name: s.name,
+ file_sources: s
+ .file_sources
+ .into_iter()
+ .map(|source| {
+ FileSource::new(
+ source.name,
+ None,
+ source
+ .locales
+ .into_iter()
+ .map(|l| l.parse().unwrap())
+ .collect(),
+ source.path_scheme,
+ )
+ })
+ .collect(),
+ locales: s
+ .locales
+ .into_iter()
+ .map(|loc| loc.parse().unwrap())
+ .collect(),
+ }
+ }
+}
+
+impl From<&fluent_testing::scenarios::structs::Scenario> for RegistrySetup {
+ fn from(s: &fluent_testing::scenarios::structs::Scenario) -> Self {
+ Self {
+ name: s.name.clone(),
+ file_sources: s
+ .file_sources
+ .iter()
+ .map(|source| {
+ FileSource::new(
+ source.name.clone(),
+ None,
+ source.locales.iter().map(|l| l.parse().unwrap()).collect(),
+ source.path_scheme.clone(),
+ )
+ })
+ .collect(),
+ locales: s.locales.iter().map(|loc| loc.parse().unwrap()).collect(),
+ }
+ }
+}
+
+#[derive(Default)]
+struct InnerFileFetcher {
+ fs: MockFileSystem,
+}
+
+#[derive(Clone)]
+pub struct TestFileFetcher {
+ inner: Rc<InnerFileFetcher>,
+}
+
+impl TestFileFetcher {
+ pub fn new() -> Self {
+ Self {
+ inner: Rc::new(InnerFileFetcher::default()),
+ }
+ }
+
+ pub fn get_test_file_source(
+ &self,
+ name: &str,
+ metasource: Option<String>,
+ locales: Vec<LanguageIdentifier>,
+ path: &str,
+ ) -> l10nregistry::source::FileSource {
+ l10nregistry::source::FileSource::new(
+ name.to_string(),
+ metasource,
+ locales,
+ path.to_string(),
+ Default::default(),
+ self.clone(),
+ )
+ }
+
+ pub fn get_test_file_source_with_index(
+ &self,
+ name: &str,
+ metasource: Option<String>,
+ locales: Vec<LanguageIdentifier>,
+ path: &str,
+ index: Vec<&str>,
+ ) -> l10nregistry::source::FileSource {
+ l10nregistry::source::FileSource::new_with_index(
+ name.to_string(),
+ metasource,
+ locales,
+ path.to_string(),
+ Default::default(),
+ self.clone(),
+ index.into_iter().map(|s| s.to_string()).collect(),
+ )
+ }
+
+ pub fn get_registry<S>(&self, setup: S) -> L10nRegistry<TestEnvironment, MockBundleAdapter>
+ where
+ S: Into<RegistrySetup>,
+ {
+ self.get_registry_and_environment(setup).1
+ }
+
+ pub fn get_registry_and_environment<S>(
+ &self,
+ setup: S,
+ ) -> (
+ TestEnvironment,
+ L10nRegistry<TestEnvironment, MockBundleAdapter>,
+ )
+ where
+ S: Into<RegistrySetup>,
+ {
+ let setup: RegistrySetup = setup.into();
+ let provider = TestEnvironment::new(setup.locales);
+
+ let reg = L10nRegistry::with_provider(provider.clone());
+ let sources = setup
+ .file_sources
+ .into_iter()
+ .map(|source| {
+ let mut s = self.get_test_file_source(
+ &source.name,
+ Some(source.metasource),
+ source.locales,
+ &source.path_scheme,
+ );
+ s.set_reporter(provider.clone());
+ s
+ })
+ .collect();
+ reg.register_sources(sources).unwrap();
+ (provider, reg)
+ }
+
+ pub fn get_registry_and_environment_with_adapter<S, B>(
+ &self,
+ setup: S,
+ bundle_adapter: B,
+ ) -> (TestEnvironment, L10nRegistry<TestEnvironment, B>)
+ where
+ S: Into<RegistrySetup>,
+ B: BundleAdapter,
+ {
+ let setup: RegistrySetup = setup.into();
+ let provider = TestEnvironment::new(setup.locales);
+
+ let mut reg = L10nRegistry::with_provider(provider.clone());
+ let sources = setup
+ .file_sources
+ .into_iter()
+ .map(|source| {
+ let mut s = self.get_test_file_source(
+ &source.name,
+ None,
+ source.locales,
+ &source.path_scheme,
+ );
+ s.set_reporter(provider.clone());
+ s
+ })
+ .collect();
+ reg.register_sources(sources).unwrap();
+ reg.set_bundle_adapter(bundle_adapter)
+ .expect("Failed to set bundle adapter.");
+ (provider, reg)
+ }
+}
+
+#[async_trait(?Send)]
+impl FileFetcher for TestFileFetcher {
+ fn fetch_sync(&self, resource_id: &ResourceId) -> std::io::Result<String> {
+ self.inner.fs.get_test_file_sync(&resource_id.value)
+ }
+
+ async fn fetch(&self, resource_id: &ResourceId) -> std::io::Result<String> {
+ self.inner.fs.get_test_file_async(&resource_id.value).await
+ }
+}
+
+pub enum ErrorStrategy {
+ Panic,
+ Report,
+ Nothing,
+}
+
+pub struct InnerTestEnvironment {
+ locales: Vec<LanguageIdentifier>,
+ errors: Vec<L10nRegistryError>,
+ error_strategy: ErrorStrategy,
+}
+
+#[derive(Clone)]
+pub struct TestEnvironment {
+ inner: Rc<RefCell<InnerTestEnvironment>>,
+}
+
+impl TestEnvironment {
+ pub fn new(locales: Vec<LanguageIdentifier>) -> Self {
+ Self {
+ inner: Rc::new(RefCell::new(InnerTestEnvironment {
+ locales,
+ errors: vec![],
+ error_strategy: ErrorStrategy::Report,
+ })),
+ }
+ }
+
+ pub fn set_locales(&self, locales: Vec<LanguageIdentifier>) {
+ self.inner.borrow_mut().locales = locales;
+ }
+
+ pub fn errors(&self) -> Vec<L10nRegistryError> {
+ self.inner.borrow().errors.clone()
+ }
+
+ pub fn clear_errors(&self) {
+ self.inner.borrow_mut().errors.clear()
+ }
+}
+
+impl LocalesProvider for TestEnvironment {
+ type Iter = std::vec::IntoIter<LanguageIdentifier>;
+
+ fn locales(&self) -> Self::Iter {
+ self.inner.borrow().locales.clone().into_iter()
+ }
+}
+
+impl ErrorReporter for TestEnvironment {
+ fn report_errors(&self, errors: Vec<L10nRegistryError>) {
+ match self.inner.borrow().error_strategy {
+ ErrorStrategy::Panic => {
+ panic!("Errors: {:#?}", errors);
+ }
+ ErrorStrategy::Report => {
+ #[cfg(test)] // Don't let printing affect benchmarks
+ eprintln!("Errors: {:#?}", errors);
+ }
+ ErrorStrategy::Nothing => {}
+ }
+ self.inner.borrow_mut().errors.extend(errors);
+ }
+}
diff --git a/intl/l10n/rust/l10nregistry-tests/src/solver/mod.rs b/intl/l10n/rust/l10nregistry-tests/src/solver/mod.rs
new file mode 100644
index 0000000000..68f566250e
--- /dev/null
+++ b/intl/l10n/rust/l10nregistry-tests/src/solver/mod.rs
@@ -0,0 +1,38 @@
+mod scenarios;
+
+pub use scenarios::get_scenarios;
+
+/// Define a testing scenario.
+pub struct Scenario {
+ /// Name of the scenario.
+ pub name: String,
+ /// Number of resources.
+ pub width: usize,
+ /// Number of sources.
+ pub depth: usize,
+ /// Vector of resources, containing a vector of sources, with true indicating
+ /// whether the resource is present in that source.
+ pub values: Vec<Vec<bool>>,
+ /// Vector of solutions, each containing a vector of resources, with the index
+ /// indicating from which source the resource is chosen.
+ /// TODO(issue#17): This field is currently unused!
+ pub solutions: Vec<Vec<usize>>,
+}
+
+impl Scenario {
+ pub fn new<S: ToString>(
+ name: S,
+ width: usize,
+ depth: usize,
+ values: Vec<Vec<bool>>,
+ solutions: Vec<Vec<usize>>,
+ ) -> Self {
+ Self {
+ name: name.to_string(),
+ width,
+ depth,
+ values,
+ solutions,
+ }
+ }
+}
diff --git a/intl/l10n/rust/l10nregistry-tests/src/solver/scenarios.rs b/intl/l10n/rust/l10nregistry-tests/src/solver/scenarios.rs
new file mode 100644
index 0000000000..8addec979b
--- /dev/null
+++ b/intl/l10n/rust/l10nregistry-tests/src/solver/scenarios.rs
@@ -0,0 +1,151 @@
+use super::*;
+
+pub fn get_scenarios() -> Vec<Scenario> {
+ vec![
+ Scenario::new("no-sources", 1, 0, vec![], vec![]),
+ Scenario::new("no-resources", 1, 0, vec![vec![true]], vec![]),
+ Scenario::new("no-keys", 0, 1, vec![], vec![]),
+ Scenario::new(
+ "one-res-two-sources",
+ 1,
+ 2,
+ vec![vec![true, true]],
+ vec![vec![0], vec![1]],
+ ),
+ Scenario::new(
+ "two-res-two-sources",
+ 2,
+ 2,
+ vec![vec![false, true], vec![true, false]],
+ vec![vec![1, 0]],
+ ),
+ Scenario::new(
+ "small",
+ 3,
+ 2,
+ vec![vec![true, true], vec![true, true], vec![true, true]],
+ vec![
+ vec![0, 0, 0],
+ vec![0, 0, 1],
+ vec![0, 1, 0],
+ vec![0, 1, 1],
+ vec![1, 0, 0],
+ vec![1, 0, 1],
+ vec![1, 1, 0],
+ vec![1, 1, 1],
+ ],
+ ),
+ Scenario::new(
+ "incomplete",
+ 3,
+ 2,
+ vec![vec![true, false], vec![false, true], vec![true, true]],
+ vec![vec![0, 1, 0], vec![0, 1, 1]],
+ ),
+ Scenario::new(
+ "preferences",
+ 19,
+ 2,
+ vec![
+ vec![true, false],
+ vec![true, false],
+ vec![true, false],
+ vec![true, false],
+ vec![true, false],
+ vec![true, false],
+ vec![true, false],
+ vec![true, false],
+ vec![true, false],
+ vec![true, false],
+ vec![true, false],
+ vec![true, false],
+ vec![true, false],
+ vec![true, false],
+ vec![true, false],
+ vec![true, false],
+ vec![false, true],
+ vec![false, true],
+ vec![false, true],
+ ],
+ vec![vec![
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1,
+ ]],
+ ),
+ Scenario::new(
+ "langpack",
+ 3,
+ 4,
+ vec![
+ vec![true, true, true, true],
+ vec![true, true, true, true],
+ vec![true, true, true, true],
+ ],
+ vec![
+ vec![0, 0, 0],
+ vec![0, 0, 1],
+ vec![0, 0, 2],
+ vec![0, 0, 3],
+ vec![0, 1, 0],
+ vec![0, 1, 1],
+ vec![0, 1, 2],
+ vec![0, 1, 3],
+ vec![0, 2, 0],
+ vec![0, 2, 1],
+ vec![0, 2, 2],
+ vec![0, 2, 3],
+ vec![0, 3, 0],
+ vec![0, 3, 1],
+ vec![0, 3, 2],
+ vec![0, 3, 3],
+ vec![1, 0, 0],
+ vec![1, 0, 1],
+ vec![1, 0, 2],
+ vec![1, 0, 3],
+ vec![1, 1, 0],
+ vec![1, 1, 1],
+ vec![1, 1, 2],
+ vec![1, 1, 3],
+ vec![1, 2, 0],
+ vec![1, 2, 1],
+ vec![1, 2, 2],
+ vec![1, 2, 3],
+ vec![1, 3, 0],
+ vec![1, 3, 1],
+ vec![1, 3, 2],
+ vec![1, 3, 3],
+ vec![2, 0, 0],
+ vec![2, 0, 1],
+ vec![2, 0, 2],
+ vec![2, 0, 3],
+ vec![2, 1, 0],
+ vec![2, 1, 1],
+ vec![2, 1, 2],
+ vec![2, 1, 3],
+ vec![2, 2, 0],
+ vec![2, 2, 1],
+ vec![2, 2, 2],
+ vec![2, 2, 3],
+ vec![2, 3, 0],
+ vec![2, 3, 1],
+ vec![2, 3, 2],
+ vec![2, 3, 3],
+ vec![3, 0, 0],
+ vec![3, 0, 1],
+ vec![3, 0, 2],
+ vec![3, 0, 3],
+ vec![3, 1, 0],
+ vec![3, 1, 1],
+ vec![3, 1, 2],
+ vec![3, 1, 3],
+ vec![3, 2, 0],
+ vec![3, 2, 1],
+ vec![3, 2, 2],
+ vec![3, 2, 3],
+ vec![3, 3, 0],
+ vec![3, 3, 1],
+ vec![3, 3, 2],
+ vec![3, 3, 3],
+ ],
+ ),
+ ]
+}
diff --git a/intl/l10n/rust/l10nregistry-tests/tests/localization.rs b/intl/l10n/rust/l10nregistry-tests/tests/localization.rs
new file mode 100644
index 0000000000..1362fc030e
--- /dev/null
+++ b/intl/l10n/rust/l10nregistry-tests/tests/localization.rs
@@ -0,0 +1,201 @@
+use std::borrow::Cow;
+
+use fluent_fallback::{
+ env::LocalesProvider,
+ types::{L10nKey, ResourceId},
+ Localization,
+};
+use l10nregistry_tests::{
+ FileSource, MockBundleAdapter, RegistrySetup, TestEnvironment, TestFileFetcher,
+};
+use serial_test::serial;
+use unic_langid::{langid, LanguageIdentifier};
+
+type L10nRegistry = l10nregistry::registry::L10nRegistry<TestEnvironment, MockBundleAdapter>;
+
+static LOCALES: &[LanguageIdentifier] = &[langid!("pl"), langid!("en-US")];
+static mut FILE_FETCHER: Option<TestFileFetcher> = None;
+static mut L10N_REGISTRY: Option<L10nRegistry> = None;
+
+const FTL_RESOURCE: &str = "toolkit/updates/history.ftl";
+const L10N_ID_PL_EN: (&str, Option<&str>) = ("history-title", Some("Historia aktualizacji"));
+const L10N_ID_MISSING: (&str, Option<&str>) = ("missing-id", None);
+const L10N_ID_ONLY_EN: (&str, Option<&str>) = (
+ "history-intro",
+ Some("The following updates have been installed"),
+);
+
+fn get_file_fetcher() -> &'static TestFileFetcher {
+ let fetcher: &mut Option<TestFileFetcher> = unsafe { &mut FILE_FETCHER };
+
+ fetcher.get_or_insert_with(|| TestFileFetcher::new())
+}
+
+fn get_l10n_registry() -> &'static L10nRegistry {
+ let reg: &mut Option<L10nRegistry> = unsafe { &mut L10N_REGISTRY };
+
+ reg.get_or_insert_with(|| {
+ let fetcher = get_file_fetcher();
+ let setup = RegistrySetup::new(
+ "test",
+ vec![
+ FileSource::new(
+ "toolkit",
+ None,
+ get_app_locales().to_vec(),
+ "toolkit/{locale}/",
+ ),
+ FileSource::new(
+ "browser",
+ None,
+ get_app_locales().to_vec(),
+ "browser/{locale}/",
+ ),
+ ],
+ get_app_locales().to_vec(),
+ );
+ fetcher.get_registry_and_environment(setup).1
+ })
+}
+
+fn get_app_locales() -> &'static [LanguageIdentifier] {
+ LOCALES
+}
+
+struct LocalesService;
+
+impl LocalesProvider for LocalesService {
+ type Iter = std::vec::IntoIter<LanguageIdentifier>;
+
+ fn locales(&self) -> Self::Iter {
+ get_app_locales().to_vec().into_iter()
+ }
+}
+
+fn sync_localization(
+ reg: &'static L10nRegistry,
+ res_ids: Vec<ResourceId>,
+) -> Localization<L10nRegistry, LocalesService> {
+ Localization::with_env(res_ids, true, LocalesService, reg.clone())
+}
+
+fn async_localization(
+ reg: &'static L10nRegistry,
+ res_ids: Vec<ResourceId>,
+) -> Localization<L10nRegistry, LocalesService> {
+ Localization::with_env(res_ids, false, LocalesService, reg.clone())
+}
+
+fn setup_sync_test() -> Localization<L10nRegistry, LocalesService> {
+ sync_localization(get_l10n_registry(), vec![FTL_RESOURCE.into()])
+}
+
+fn setup_async_test() -> Localization<L10nRegistry, LocalesService> {
+ async_localization(get_l10n_registry(), vec![FTL_RESOURCE.into()])
+}
+
+#[test]
+#[serial]
+fn localization_format_value_sync() {
+ let loc = setup_sync_test();
+ let bundles = loc.bundles();
+ let mut errors = vec![];
+
+ for query in &[L10N_ID_PL_EN, L10N_ID_MISSING, L10N_ID_ONLY_EN] {
+ let value = bundles
+ .format_value_sync(query.0, None, &mut errors)
+ .unwrap();
+ assert_eq!(value, query.1.map(|s| Cow::Borrowed(s)));
+ }
+
+ assert_eq!(errors.len(), 4);
+}
+
+#[test]
+#[serial]
+fn localization_format_values_sync() {
+ let loc = setup_sync_test();
+ let bundles = loc.bundles();
+ let mut errors = vec![];
+
+ let ids = &[L10N_ID_PL_EN, L10N_ID_MISSING, L10N_ID_ONLY_EN];
+ let keys = ids
+ .iter()
+ .map(|query| L10nKey {
+ id: query.0.into(),
+ args: None,
+ })
+ .collect::<Vec<_>>();
+
+ let values = bundles.format_values_sync(&keys, &mut errors).unwrap();
+
+ assert_eq!(values.len(), ids.len());
+
+ for (value, query) in values.iter().zip(ids) {
+ if let Some(expected) = query.1 {
+ assert_eq!(*value, Some(Cow::Borrowed(expected)));
+ }
+ }
+ assert_eq!(errors.len(), 4);
+}
+
+#[tokio::test]
+#[serial]
+async fn localization_format_value_async() {
+ let loc = setup_async_test();
+ let bundles = loc.bundles();
+ let mut errors = vec![];
+
+ for query in &[L10N_ID_PL_EN, L10N_ID_MISSING, L10N_ID_ONLY_EN] {
+ let value = bundles.format_value(query.0, None, &mut errors).await;
+ if let Some(expected) = query.1 {
+ assert_eq!(value, Some(Cow::Borrowed(expected)));
+ }
+ }
+}
+
+#[tokio::test]
+#[serial]
+async fn localization_format_values_async() {
+ let loc = setup_async_test();
+ let bundles = loc.bundles();
+ let mut errors = vec![];
+
+ let ids = &[L10N_ID_PL_EN, L10N_ID_MISSING, L10N_ID_ONLY_EN];
+ let keys = ids
+ .iter()
+ .map(|query| L10nKey {
+ id: query.0.into(),
+ args: None,
+ })
+ .collect::<Vec<_>>();
+
+ let values = bundles.format_values(&keys, &mut errors).await;
+
+ assert_eq!(values.len(), ids.len());
+
+ for (value, query) in values.iter().zip(ids) {
+ if let Some(expected) = query.1 {
+ assert_eq!(*value, Some(Cow::Borrowed(expected)));
+ }
+ }
+}
+
+#[tokio::test]
+#[serial]
+async fn localization_upgrade() {
+ let mut loc = setup_sync_test();
+ let bundles = loc.bundles();
+ let mut errors = vec![];
+ let value = bundles
+ .format_value_sync(L10N_ID_PL_EN.0, None, &mut errors)
+ .unwrap();
+ assert_eq!(value, L10N_ID_PL_EN.1.map(|s| Cow::Borrowed(s)));
+
+ loc.set_async();
+ let bundles = loc.bundles();
+ let value = bundles
+ .format_value(L10N_ID_PL_EN.0, None, &mut errors)
+ .await;
+ assert_eq!(value, L10N_ID_PL_EN.1.map(|s| Cow::Borrowed(s)));
+}
diff --git a/intl/l10n/rust/l10nregistry-tests/tests/registry.rs b/intl/l10n/rust/l10nregistry-tests/tests/registry.rs
new file mode 100644
index 0000000000..72d6e6ed8b
--- /dev/null
+++ b/intl/l10n/rust/l10nregistry-tests/tests/registry.rs
@@ -0,0 +1,304 @@
+use l10nregistry_tests::{FileSource, RegistrySetup, TestFileFetcher};
+use unic_langid::LanguageIdentifier;
+
+static FTL_RESOURCE_TOOLKIT: &str = "toolkit/global/textActions.ftl";
+static FTL_RESOURCE_BROWSER: &str = "branding/brand.ftl";
+
+#[test]
+fn test_get_sources_for_resource() {
+ let en_us: LanguageIdentifier = "en-US".parse().unwrap();
+ let setup = RegistrySetup::new(
+ "test",
+ vec![
+ FileSource::new("toolkit", None, vec![en_us.clone()], "toolkit/{locale}/"),
+ FileSource::new("browser", None, vec![en_us.clone()], "browser/{locale}/"),
+ ],
+ vec![en_us.clone()],
+ );
+ let fetcher = TestFileFetcher::new();
+ let (_, reg) = fetcher.get_registry_and_environment(setup);
+
+ {
+ let metasources = reg
+ .try_borrow_metasources()
+ .expect("Unable to borrow metasources.");
+
+ let toolkit = metasources.file_source_by_name(0, "toolkit").unwrap();
+ let browser = metasources.file_source_by_name(0, "browser").unwrap();
+ let toolkit_resource_id = FTL_RESOURCE_TOOLKIT.into();
+
+ let mut i = metasources.get_sources_for_resource(0, &en_us, &toolkit_resource_id);
+
+ assert_eq!(i.next(), Some(toolkit));
+ assert_eq!(i.next(), Some(browser));
+ assert_eq!(i.next(), None);
+
+ assert!(browser
+ .fetch_file_sync(&en_us, &FTL_RESOURCE_TOOLKIT.into(), false)
+ .is_none());
+
+ let mut i = metasources.get_sources_for_resource(0, &en_us, &toolkit_resource_id);
+ assert_eq!(i.next(), Some(toolkit));
+ assert_eq!(i.next(), None);
+
+ assert!(toolkit
+ .fetch_file_sync(&en_us, &FTL_RESOURCE_TOOLKIT.into(), false)
+ .is_some());
+
+ let mut i = metasources.get_sources_for_resource(0, &en_us, &toolkit_resource_id);
+ assert_eq!(i.next(), Some(toolkit));
+ assert_eq!(i.next(), None);
+ }
+}
+
+#[test]
+fn test_generate_bundles_for_lang_sync() {
+ let en_us: LanguageIdentifier = "en-US".parse().unwrap();
+ let setup = RegistrySetup::new(
+ "test",
+ vec![
+ FileSource::new("toolkit", None, vec![en_us.clone()], "toolkit/{locale}/"),
+ FileSource::new("browser", None, vec![en_us.clone()], "browser/{locale}/"),
+ ],
+ vec![en_us.clone()],
+ );
+ let fetcher = TestFileFetcher::new();
+ let (_, reg) = fetcher.get_registry_and_environment(setup);
+
+ let paths = vec![FTL_RESOURCE_TOOLKIT.into(), FTL_RESOURCE_BROWSER.into()];
+ let mut i = reg.generate_bundles_for_lang_sync(en_us.clone(), paths);
+
+ assert!(i.next().is_some());
+ assert!(i.next().is_none());
+}
+
+#[test]
+fn test_generate_bundles_sync() {
+ let en_us: LanguageIdentifier = "en-US".parse().unwrap();
+ let setup = RegistrySetup::new(
+ "test",
+ vec![
+ FileSource::new("toolkit", None, vec![en_us.clone()], "toolkit/{locale}/"),
+ FileSource::new("browser", None, vec![en_us.clone()], "browser/{locale}/"),
+ ],
+ vec![en_us.clone()],
+ );
+ let fetcher = TestFileFetcher::new();
+ let (_, reg) = fetcher.get_registry_and_environment(setup);
+
+ let paths = vec![FTL_RESOURCE_TOOLKIT.into(), FTL_RESOURCE_BROWSER.into()];
+ let lang_ids = vec![en_us];
+ let mut i = reg.generate_bundles_sync(lang_ids.into_iter(), paths);
+
+ assert!(i.next().is_some());
+ assert!(i.next().is_none());
+}
+
+#[tokio::test]
+async fn test_generate_bundles_for_lang() {
+ use futures::stream::StreamExt;
+
+ let en_us: LanguageIdentifier = "en-US".parse().unwrap();
+ let setup = RegistrySetup::new(
+ "test",
+ vec![
+ FileSource::new("toolkit", None, vec![en_us.clone()], "toolkit/{locale}/"),
+ FileSource::new("browser", None, vec![en_us.clone()], "browser/{locale}/"),
+ ],
+ vec![en_us.clone()],
+ );
+ let fetcher = TestFileFetcher::new();
+ let (_, reg) = fetcher.get_registry_and_environment(setup);
+
+ let paths = vec![FTL_RESOURCE_TOOLKIT.into(), FTL_RESOURCE_BROWSER.into()];
+ let mut i = reg
+ .generate_bundles_for_lang(en_us, paths)
+ .expect("Failed to get GenerateBundles.");
+
+ assert!(i.next().await.is_some());
+ assert!(i.next().await.is_none());
+}
+
+#[tokio::test]
+async fn test_generate_bundles() {
+ use futures::stream::StreamExt;
+
+ let en_us: LanguageIdentifier = "en-US".parse().unwrap();
+ let setup = RegistrySetup::new(
+ "test",
+ vec![
+ FileSource::new("toolkit", None, vec![en_us.clone()], "toolkit/{locale}/"),
+ FileSource::new("browser", None, vec![en_us.clone()], "browser/{locale}/"),
+ ],
+ vec![en_us.clone()],
+ );
+ let fetcher = TestFileFetcher::new();
+ let (_, reg) = fetcher.get_registry_and_environment(setup);
+
+ let paths = vec![FTL_RESOURCE_TOOLKIT.into(), FTL_RESOURCE_BROWSER.into()];
+ let langs = vec![en_us];
+ let mut i = reg
+ .generate_bundles(langs.into_iter(), paths)
+ .expect("Failed to get GenerateBundles.");
+
+ assert!(i.next().await.is_some());
+ assert!(i.next().await.is_none());
+}
+
+#[test]
+fn test_manage_sources() {
+ let en_us: LanguageIdentifier = "en-US".parse().unwrap();
+ let setup = RegistrySetup::new(
+ "test",
+ vec![
+ FileSource::new("toolkit", None, vec![en_us.clone()], "toolkit/{locale}/"),
+ FileSource::new("browser", None, vec![en_us.clone()], "browser/{locale}/"),
+ ],
+ vec![en_us.clone()],
+ );
+ let fetcher = TestFileFetcher::new();
+ let (_, reg) = fetcher.get_registry_and_environment(setup);
+
+ let lang_ids = vec![en_us];
+
+ let paths = vec![FTL_RESOURCE_TOOLKIT.into(), FTL_RESOURCE_BROWSER.into()];
+
+ let mut i = reg.generate_bundles_sync(lang_ids.clone().into_iter(), paths);
+
+ assert!(i.next().is_some());
+ assert!(i.next().is_none());
+
+ reg.clone()
+ .remove_sources(vec!["toolkit"])
+ .expect("Failed to remove a source.");
+
+ let paths = vec![FTL_RESOURCE_TOOLKIT.into(), FTL_RESOURCE_BROWSER.into()];
+ let mut i = reg.generate_bundles_sync(lang_ids.clone().into_iter(), paths);
+ assert!(i.next().is_none());
+
+ let paths = vec![FTL_RESOURCE_BROWSER.into()];
+ let mut i = reg.generate_bundles_sync(lang_ids.clone().into_iter(), paths);
+ assert!(i.next().is_some());
+ assert!(i.next().is_none());
+
+ reg.register_sources(vec![fetcher.get_test_file_source(
+ "toolkit",
+ None,
+ lang_ids.clone(),
+ "browser/{locale}/",
+ )])
+ .expect("Failed to register a source.");
+
+ let paths = vec![FTL_RESOURCE_TOOLKIT.into(), FTL_RESOURCE_BROWSER.into()];
+ let mut i = reg.generate_bundles_sync(lang_ids.clone().into_iter(), paths);
+ assert!(i.next().is_none());
+
+ reg.update_sources(vec![fetcher.get_test_file_source(
+ "toolkit",
+ None,
+ lang_ids.clone(),
+ "toolkit/{locale}/",
+ )])
+ .expect("Failed to update a source.");
+
+ let paths = vec![FTL_RESOURCE_TOOLKIT.into(), FTL_RESOURCE_BROWSER.into()];
+ let mut i = reg.generate_bundles_sync(lang_ids.clone().into_iter(), paths);
+ assert!(i.next().is_some());
+ assert!(i.next().is_none());
+}
+
+#[test]
+fn test_generate_bundles_with_metasources_sync() {
+ let en_us: LanguageIdentifier = "en-US".parse().unwrap();
+ let setup = RegistrySetup::new(
+ "test",
+ vec![
+ FileSource::new(
+ "toolkit",
+ Some("app"),
+ vec![en_us.clone()],
+ "toolkit/{locale}/",
+ ),
+ FileSource::new(
+ "browser",
+ Some("app"),
+ vec![en_us.clone()],
+ "browser/{locale}/",
+ ),
+ FileSource::new(
+ "toolkit",
+ Some("langpack"),
+ vec![en_us.clone()],
+ "toolkit/{locale}/",
+ ),
+ FileSource::new(
+ "browser",
+ Some("langpack"),
+ vec![en_us.clone()],
+ "browser/{locale}/",
+ ),
+ ],
+ vec![en_us.clone()],
+ );
+ let fetcher = TestFileFetcher::new();
+ let (_, reg) = fetcher.get_registry_and_environment(setup);
+
+ let paths = vec![FTL_RESOURCE_TOOLKIT.into(), FTL_RESOURCE_BROWSER.into()];
+ let lang_ids = vec![en_us];
+ let mut i = reg.generate_bundles_sync(lang_ids.into_iter(), paths);
+
+ assert!(i.next().is_some());
+ assert!(i.next().is_some());
+ assert!(i.next().is_none());
+}
+
+#[tokio::test]
+async fn test_generate_bundles_with_metasources() {
+ use futures::stream::StreamExt;
+
+ let en_us: LanguageIdentifier = "en-US".parse().unwrap();
+
+ let setup = RegistrySetup::new(
+ "test",
+ vec![
+ FileSource::new(
+ "toolkit",
+ Some("app"),
+ vec![en_us.clone()],
+ "toolkit/{locale}/",
+ ),
+ FileSource::new(
+ "browser",
+ Some("app"),
+ vec![en_us.clone()],
+ "browser/{locale}/",
+ ),
+ FileSource::new(
+ "toolkit",
+ Some("langpack"),
+ vec![en_us.clone()],
+ "toolkit/{locale}/",
+ ),
+ FileSource::new(
+ "browser",
+ Some("langpack"),
+ vec![en_us.clone()],
+ "browser/{locale}/",
+ ),
+ ],
+ vec![en_us.clone()],
+ );
+
+ let fetcher = TestFileFetcher::new();
+ let (_, reg) = fetcher.get_registry_and_environment(setup);
+
+ let paths = vec![FTL_RESOURCE_TOOLKIT.into(), FTL_RESOURCE_BROWSER.into()];
+ let langs = vec![en_us];
+ let mut i = reg
+ .generate_bundles(langs.into_iter(), paths)
+ .expect("Failed to get GenerateBundles.");
+
+ assert!(i.next().await.is_some());
+ assert!(i.next().await.is_some());
+ assert!(i.next().await.is_none());
+}
diff --git a/intl/l10n/rust/l10nregistry-tests/tests/scenarios_async.rs b/intl/l10n/rust/l10nregistry-tests/tests/scenarios_async.rs
new file mode 100644
index 0000000000..cdbf373d84
--- /dev/null
+++ b/intl/l10n/rust/l10nregistry-tests/tests/scenarios_async.rs
@@ -0,0 +1,109 @@
+use fluent_bundle::FluentArgs;
+use fluent_fallback::Localization;
+use fluent_testing::get_scenarios;
+use l10nregistry::fluent::FluentBundle;
+use l10nregistry::registry::BundleAdapter;
+use l10nregistry_tests::{RegistrySetup, TestFileFetcher};
+
+#[derive(Clone)]
+struct ScenarioBundleAdapter {}
+
+impl BundleAdapter for ScenarioBundleAdapter {
+ fn adapt_bundle(&self, bundle: &mut FluentBundle) {
+ bundle.set_use_isolating(false);
+ bundle
+ .add_function("PLATFORM", |_positional, _named| "linux".into())
+ .expect("Failed to add a function to the bundle.");
+ }
+}
+
+#[tokio::test]
+async fn scenarios_async() {
+ use fluent_testing::scenarios::structs::Scenario;
+ let fetcher = TestFileFetcher::new();
+
+ let scenarios = get_scenarios();
+
+ let adapter = ScenarioBundleAdapter {};
+
+ let cannot_produce_bundle = |scenario: &Scenario| {
+ scenario
+ .queries
+ .iter()
+ .any(|query| query.exceptional_context.blocks_bundle_generation())
+ };
+
+ for scenario in scenarios {
+ println!("scenario: {}", scenario.name);
+ let setup: RegistrySetup = (&scenario).into();
+ let (env, reg) = fetcher.get_registry_and_environment_with_adapter(setup, adapter.clone());
+
+ let loc = Localization::with_env(scenario.res_ids.clone(), false, env.clone(), reg);
+ let bundles = loc.bundles();
+ let no_bundles = cannot_produce_bundle(&scenario);
+
+ let mut errors = vec![];
+
+ for query in scenario.queries.iter() {
+ let errors_start_len = errors.len();
+
+ let args = query.input.args.as_ref().map(|args| {
+ let mut result = FluentArgs::new();
+ for arg in args.as_slice() {
+ result.set(arg.id.clone(), arg.value.clone());
+ }
+ result
+ });
+
+ if let Some(output) = &query.output {
+ if let Some(value) = &output.value {
+ let v = bundles
+ .format_value(&query.input.id, args.as_ref(), &mut errors)
+ .await;
+ if no_bundles || query.exceptional_context.causes_failed_value_lookup() {
+ assert!(v.is_none());
+ if no_bundles {
+ continue;
+ }
+ } else {
+ assert_eq!(v.unwrap(), value.as_str())
+ }
+ }
+ }
+
+ if query.exceptional_context.causes_reported_format_error() {
+ assert!(
+ errors.len() > errors_start_len,
+ "expected reported errors for query {:#?}",
+ query
+ );
+ } else {
+ assert_eq!(
+ errors.len(),
+ errors_start_len,
+ "expected no reported errors for query {:#?}",
+ query
+ );
+ }
+ }
+
+ if scenario
+ .queries
+ .iter()
+ .any(|query| query.exceptional_context.missing_required_resource())
+ {
+ assert!(
+ !env.errors().is_empty(),
+ "expected errors for scenario {{ {} }}, but found none",
+ scenario.name
+ );
+ } else {
+ assert!(
+ env.errors().is_empty(),
+ "expected no errors for scenario {{ {} }}, but found {:#?}",
+ scenario.name,
+ env.errors()
+ );
+ }
+ }
+}
diff --git a/intl/l10n/rust/l10nregistry-tests/tests/scenarios_sync.rs b/intl/l10n/rust/l10nregistry-tests/tests/scenarios_sync.rs
new file mode 100644
index 0000000000..ce13b241fe
--- /dev/null
+++ b/intl/l10n/rust/l10nregistry-tests/tests/scenarios_sync.rs
@@ -0,0 +1,107 @@
+use fluent_bundle::FluentArgs;
+use fluent_fallback::Localization;
+use fluent_testing::get_scenarios;
+use l10nregistry::fluent::FluentBundle;
+use l10nregistry::registry::BundleAdapter;
+use l10nregistry_tests::{RegistrySetup, TestFileFetcher};
+
+#[derive(Clone)]
+struct ScenarioBundleAdapter {}
+
+impl BundleAdapter for ScenarioBundleAdapter {
+ fn adapt_bundle(&self, bundle: &mut FluentBundle) {
+ bundle.set_use_isolating(false);
+ bundle
+ .add_function("PLATFORM", |_positional, _named| "linux".into())
+ .expect("Failed to add a function to the bundle.");
+ }
+}
+
+#[test]
+fn scenarios_sync() {
+ use fluent_testing::scenarios::structs::Scenario;
+ let fetcher = TestFileFetcher::new();
+
+ let scenarios = get_scenarios();
+
+ let adapter = ScenarioBundleAdapter {};
+
+ let cannot_produce_bundle = |scenario: &Scenario| {
+ scenario
+ .queries
+ .iter()
+ .any(|query| query.exceptional_context.blocks_bundle_generation())
+ };
+
+ for scenario in scenarios {
+ println!("scenario: {}", scenario.name);
+ let setup: RegistrySetup = (&scenario).into();
+ let (env, reg) = fetcher.get_registry_and_environment_with_adapter(setup, adapter.clone());
+
+ let loc = Localization::with_env(scenario.res_ids.clone(), true, env.clone(), reg);
+ let bundles = loc.bundles();
+ let no_bundles = cannot_produce_bundle(&scenario);
+
+ let mut errors = vec![];
+
+ for query in scenario.queries.iter() {
+ let errors_start_len = errors.len();
+
+ let args = query.input.args.as_ref().map(|args| {
+ let mut result = FluentArgs::new();
+ for arg in args.as_slice() {
+ result.set(arg.id.clone(), arg.value.clone());
+ }
+ result
+ });
+
+ if let Some(output) = &query.output {
+ if let Some(value) = &output.value {
+ let v = bundles.format_value_sync(&query.input.id, args.as_ref(), &mut errors);
+ if no_bundles || query.exceptional_context.causes_failed_value_lookup() {
+ assert!(v.is_err() || v.unwrap().is_none());
+ if no_bundles {
+ continue;
+ }
+ } else {
+ assert_eq!(v.unwrap().unwrap(), value.as_str())
+ }
+ }
+ }
+
+ if query.exceptional_context.causes_reported_format_error() {
+ assert!(
+ errors.len() > errors_start_len,
+ "expected reported errors for query {:#?}",
+ query
+ );
+ } else {
+ assert_eq!(
+ errors.len(),
+ errors_start_len,
+ "expected no reported errors for query {:#?}",
+ query
+ );
+ }
+ }
+
+ if scenario
+ .queries
+ .iter()
+ .any(|query| query.exceptional_context.missing_required_resource())
+ {
+ assert!(
+ !env.errors().is_empty(),
+ "expected errors for scenario {{ {} }}, but found none",
+ scenario.name
+ );
+ } else {
+ assert!(
+ env.errors().is_empty(),
+ "expected no errors for scenario {{ {} }}, but found {:#?}",
+ scenario.name,
+ env.errors()
+ );
+ }
+ }
+}
diff --git a/intl/l10n/rust/l10nregistry-tests/tests/source.rs b/intl/l10n/rust/l10nregistry-tests/tests/source.rs
new file mode 100644
index 0000000000..63104db8eb
--- /dev/null
+++ b/intl/l10n/rust/l10nregistry-tests/tests/source.rs
@@ -0,0 +1,305 @@
+use fluent_fallback::types::{ResourceType, ToResourceId};
+use futures::future::join_all;
+use l10nregistry_tests::TestFileFetcher;
+use unic_langid::LanguageIdentifier;
+
+static FTL_RESOURCE_PRESENT: &str = "toolkit/global/textActions.ftl";
+static FTL_RESOURCE_MISSING: &str = "missing.ftl";
+
+#[test]
+fn test_fetch_sync() {
+ let fetcher = TestFileFetcher::new();
+ let en_us: LanguageIdentifier = "en-US".parse().unwrap();
+
+ let fs1 =
+ fetcher.get_test_file_source("toolkit", None, vec![en_us.clone()], "toolkit/{locale}/");
+
+ let file = fs1.fetch_file_sync(&en_us, &FTL_RESOURCE_PRESENT.into(), false);
+ assert!(file.is_some());
+ assert!(!file.is_none());
+ assert!(!file.is_required_and_missing());
+
+ let file = fs1.fetch_file_sync(
+ &en_us,
+ &FTL_RESOURCE_PRESENT.to_resource_id(ResourceType::Required),
+ false,
+ );
+ assert!(file.is_some());
+ assert!(!file.is_none());
+ assert!(!file.is_required_and_missing());
+
+ let file = fs1.fetch_file_sync(
+ &en_us,
+ &FTL_RESOURCE_PRESENT.to_resource_id(ResourceType::Optional),
+ false,
+ );
+ assert!(file.is_some());
+ assert!(!file.is_none());
+ assert!(!file.is_required_and_missing());
+
+ let file = fs1.fetch_file_sync(&en_us, &FTL_RESOURCE_MISSING.into(), false);
+ assert!(!file.is_some());
+ assert!(file.is_none());
+ assert!(file.is_required_and_missing());
+
+ let file = fs1.fetch_file_sync(
+ &en_us,
+ &FTL_RESOURCE_MISSING.to_resource_id(ResourceType::Required),
+ false,
+ );
+ assert!(!file.is_some());
+ assert!(file.is_none());
+ assert!(file.is_required_and_missing());
+
+ let file = fs1.fetch_file_sync(
+ &en_us,
+ &FTL_RESOURCE_MISSING.to_resource_id(ResourceType::Optional),
+ false,
+ );
+ assert!(!file.is_some());
+ assert!(file.is_none());
+ assert!(!file.is_required_and_missing());
+}
+
+#[tokio::test]
+async fn test_fetch_async() {
+ let fetcher = TestFileFetcher::new();
+ let en_us: LanguageIdentifier = "en-US".parse().unwrap();
+
+ let fs1 =
+ fetcher.get_test_file_source("toolkit", None, vec![en_us.clone()], "toolkit/{locale}/");
+
+ let file = fs1.fetch_file(&en_us, &FTL_RESOURCE_PRESENT.into()).await;
+ assert!(file.is_some());
+ assert!(!file.is_none());
+ assert!(!file.is_required_and_missing());
+
+ let file = fs1
+ .fetch_file(
+ &en_us,
+ &FTL_RESOURCE_PRESENT.to_resource_id(ResourceType::Required),
+ )
+ .await;
+ assert!(file.is_some());
+ assert!(!file.is_none());
+ assert!(!file.is_required_and_missing());
+
+ let file = fs1
+ .fetch_file(
+ &en_us,
+ &FTL_RESOURCE_PRESENT.to_resource_id(ResourceType::Optional),
+ )
+ .await;
+ assert!(file.is_some());
+ assert!(!file.is_none());
+ assert!(!file.is_required_and_missing());
+
+ let file = fs1.fetch_file(&en_us, &FTL_RESOURCE_MISSING.into()).await;
+ assert!(!file.is_some());
+ assert!(file.is_none());
+ assert!(file.is_required_and_missing());
+
+ let file = fs1
+ .fetch_file(
+ &en_us,
+ &FTL_RESOURCE_MISSING.to_resource_id(ResourceType::Required),
+ )
+ .await;
+ assert!(!file.is_some());
+ assert!(file.is_none());
+ assert!(file.is_required_and_missing());
+
+ let file = fs1
+ .fetch_file(
+ &en_us,
+ &FTL_RESOURCE_MISSING.to_resource_id(ResourceType::Optional),
+ )
+ .await;
+ assert!(!file.is_some());
+ assert!(file.is_none());
+ assert!(!file.is_required_and_missing());
+}
+
+#[tokio::test]
+async fn test_fetch_sync_2_async() {
+ let fetcher = TestFileFetcher::new();
+ let en_us: LanguageIdentifier = "en-US".parse().unwrap();
+
+ let fs1 =
+ fetcher.get_test_file_source("toolkit", None, vec![en_us.clone()], "toolkit/{locale}/");
+
+ assert!(fs1
+ .fetch_file_sync(&en_us, &FTL_RESOURCE_PRESENT.into(), false)
+ .is_some());
+ assert!(fs1
+ .fetch_file(&en_us, &FTL_RESOURCE_PRESENT.into())
+ .await
+ .is_some());
+ assert!(fs1
+ .fetch_file_sync(&en_us, &FTL_RESOURCE_PRESENT.into(), false)
+ .is_some());
+}
+
+#[tokio::test]
+async fn test_fetch_async_2_sync() {
+ let fetcher = TestFileFetcher::new();
+ let en_us: LanguageIdentifier = "en-US".parse().unwrap();
+
+ let fs1 =
+ fetcher.get_test_file_source("toolkit", None, vec![en_us.clone()], "toolkit/{locale}/");
+
+ assert!(fs1
+ .fetch_file(&en_us, &FTL_RESOURCE_PRESENT.into())
+ .await
+ .is_some());
+ assert!(fs1
+ .fetch_file_sync(&en_us, &FTL_RESOURCE_PRESENT.into(), false)
+ .is_some());
+}
+
+#[test]
+fn test_fetch_has_value_required_sync() {
+ let fetcher = TestFileFetcher::new();
+ let en_us: LanguageIdentifier = "en-US".parse().unwrap();
+ let path = FTL_RESOURCE_PRESENT.into();
+ let path_missing = FTL_RESOURCE_MISSING.into();
+
+ let fs1 =
+ fetcher.get_test_file_source("toolkit", None, vec![en_us.clone()], "toolkit/{locale}/");
+
+ assert_eq!(fs1.has_file(&en_us, &path), None);
+ assert!(fs1.fetch_file_sync(&en_us, &path, false).is_some());
+ assert_eq!(fs1.has_file(&en_us, &path), Some(true));
+
+ assert_eq!(fs1.has_file(&en_us, &path_missing), None);
+ assert!(fs1.fetch_file_sync(&en_us, &path_missing, false).is_none());
+ assert_eq!(fs1.has_file(&en_us, &path_missing), Some(false));
+}
+
+#[test]
+fn test_fetch_has_value_optional_sync() {
+ let fetcher = TestFileFetcher::new();
+ let en_us: LanguageIdentifier = "en-US".parse().unwrap();
+ let path = FTL_RESOURCE_PRESENT.to_resource_id(ResourceType::Optional);
+ let path_missing = FTL_RESOURCE_MISSING.to_resource_id(ResourceType::Optional);
+
+ let fs1 =
+ fetcher.get_test_file_source("toolkit", None, vec![en_us.clone()], "toolkit/{locale}/");
+
+ assert_eq!(fs1.has_file(&en_us, &path), None);
+ assert!(fs1.fetch_file_sync(&en_us, &path, false).is_some());
+ assert_eq!(fs1.has_file(&en_us, &path), Some(true));
+
+ assert_eq!(fs1.has_file(&en_us, &path_missing), None);
+ assert!(fs1.fetch_file_sync(&en_us, &path_missing, false).is_none());
+ assert_eq!(fs1.has_file(&en_us, &path_missing), Some(false));
+}
+
+#[tokio::test]
+async fn test_fetch_has_value_required_async() {
+ let fetcher = TestFileFetcher::new();
+ let en_us: LanguageIdentifier = "en-US".parse().unwrap();
+ let path = FTL_RESOURCE_PRESENT.into();
+ let path_missing = FTL_RESOURCE_MISSING.into();
+
+ let fs1 =
+ fetcher.get_test_file_source("toolkit", None, vec![en_us.clone()], "toolkit/{locale}/");
+
+ assert_eq!(fs1.has_file(&en_us, &path), None);
+ assert!(fs1.fetch_file(&en_us, &path).await.is_some());
+ println!("Completed");
+ assert_eq!(fs1.has_file(&en_us, &path), Some(true));
+
+ assert_eq!(fs1.has_file(&en_us, &path_missing), None);
+
+ assert!(fs1.fetch_file(&en_us, &path_missing).await.is_none());
+ assert!(fs1
+ .fetch_file(&en_us, &path_missing)
+ .await
+ .is_required_and_missing());
+
+ assert_eq!(fs1.has_file(&en_us, &path_missing), Some(false));
+
+ assert!(fs1.fetch_file_sync(&en_us, &path_missing, false).is_none());
+ assert!(fs1
+ .fetch_file_sync(&en_us, &path_missing, false)
+ .is_required_and_missing());
+}
+
+#[tokio::test]
+async fn test_fetch_has_value_optional_async() {
+ let fetcher = TestFileFetcher::new();
+ let en_us: LanguageIdentifier = "en-US".parse().unwrap();
+ let path = FTL_RESOURCE_PRESENT.to_resource_id(ResourceType::Optional);
+ let path_missing = FTL_RESOURCE_MISSING.to_resource_id(ResourceType::Optional);
+
+ let fs1 =
+ fetcher.get_test_file_source("toolkit", None, vec![en_us.clone()], "toolkit/{locale}/");
+
+ assert_eq!(fs1.has_file(&en_us, &path), None);
+ assert!(fs1.fetch_file(&en_us, &path).await.is_some());
+ println!("Completed");
+ assert_eq!(fs1.has_file(&en_us, &path), Some(true));
+
+ assert_eq!(fs1.has_file(&en_us, &path_missing), None);
+ assert!(fs1.fetch_file(&en_us, &path_missing).await.is_none());
+ assert!(!fs1
+ .fetch_file(&en_us, &path_missing)
+ .await
+ .is_required_and_missing());
+
+ assert_eq!(fs1.has_file(&en_us, &path_missing), Some(false));
+
+ assert!(fs1.fetch_file_sync(&en_us, &path_missing, false).is_none());
+ assert!(!fs1
+ .fetch_file_sync(&en_us, &path_missing, false)
+ .is_required_and_missing());
+}
+
+#[tokio::test]
+async fn test_fetch_async_consecutive() {
+ let fetcher = TestFileFetcher::new();
+ let en_us: LanguageIdentifier = "en-US".parse().unwrap();
+
+ let fs1 =
+ fetcher.get_test_file_source("toolkit", None, vec![en_us.clone()], "toolkit/{locale}/");
+
+ let results = join_all(vec![
+ fs1.fetch_file(&en_us, &FTL_RESOURCE_PRESENT.into()),
+ fs1.fetch_file(&en_us, &FTL_RESOURCE_PRESENT.into()),
+ ])
+ .await;
+ assert!(results[0].is_some());
+ assert!(results[1].is_some());
+
+ assert!(fs1
+ .fetch_file(&en_us, &FTL_RESOURCE_PRESENT.into())
+ .await
+ .is_some());
+}
+
+#[test]
+fn test_indexed() {
+ let fetcher = TestFileFetcher::new();
+ let en_us: LanguageIdentifier = "en-US".parse().unwrap();
+ let path = FTL_RESOURCE_PRESENT;
+ let path_missing = FTL_RESOURCE_MISSING;
+
+ let fs1 = fetcher.get_test_file_source_with_index(
+ "toolkit",
+ None,
+ vec![en_us.clone()],
+ "toolkit/{locale}/",
+ vec!["toolkit/en-US/toolkit/global/textActions.ftl"],
+ );
+
+ assert_eq!(fs1.has_file(&en_us, &path.into()), Some(true));
+ assert!(fs1.fetch_file_sync(&en_us, &path.into(), false).is_some());
+ assert_eq!(fs1.has_file(&en_us, &path.into()), Some(true));
+
+ assert_eq!(fs1.has_file(&en_us, &path_missing.into()), Some(false));
+ assert!(fs1
+ .fetch_file_sync(&en_us, &path_missing.into(), false)
+ .is_none());
+ assert_eq!(fs1.has_file(&en_us, &path_missing.into()), Some(false));
+}
diff --git a/intl/l10n/rust/l10nregistry-tests/tests/tokio.rs b/intl/l10n/rust/l10nregistry-tests/tests/tokio.rs
new file mode 100644
index 0000000000..0f404c3a08
--- /dev/null
+++ b/intl/l10n/rust/l10nregistry-tests/tests/tokio.rs
@@ -0,0 +1,65 @@
+use l10nregistry_tests::TestFileFetcher;
+use unic_langid::LanguageIdentifier;
+
+static FTL_RESOURCE_PRESENT: &str = "toolkit/global/textActions.ftl";
+static FTL_RESOURCE_MISSING: &str = "missing.ftl";
+
+#[tokio::test]
+async fn file_source_fetch() {
+ let fetcher = TestFileFetcher::new();
+ let en_us: LanguageIdentifier = "en-US".parse().unwrap();
+ let fs1 =
+ fetcher.get_test_file_source("toolkit", None, vec![en_us.clone()], "toolkit/{locale}/");
+
+ let file = fs1.fetch_file(&en_us, &FTL_RESOURCE_PRESENT.into()).await;
+ assert!(file.is_some());
+}
+
+#[tokio::test]
+async fn file_source_fetch_missing() {
+ let fetcher = TestFileFetcher::new();
+ let en_us: LanguageIdentifier = "en-US".parse().unwrap();
+ let fs1 =
+ fetcher.get_test_file_source("toolkit", None, vec![en_us.clone()], "toolkit/{locale}/");
+
+ let file = fs1.fetch_file(&en_us, &FTL_RESOURCE_MISSING.into()).await;
+ assert!(file.is_none());
+}
+
+#[tokio::test]
+async fn file_source_already_loaded() {
+ let fetcher = TestFileFetcher::new();
+ let en_us: LanguageIdentifier = "en-US".parse().unwrap();
+ let fs1 =
+ fetcher.get_test_file_source("toolkit", None, vec![en_us.clone()], "toolkit/{locale}/");
+
+ let file = fs1.fetch_file(&en_us, &FTL_RESOURCE_PRESENT.into()).await;
+ assert!(file.is_some());
+ let file = fs1.fetch_file(&en_us, &FTL_RESOURCE_PRESENT.into()).await;
+ assert!(file.is_some());
+}
+
+#[tokio::test]
+async fn file_source_concurrent() {
+ let fetcher = TestFileFetcher::new();
+ let en_us: LanguageIdentifier = "en-US".parse().unwrap();
+ let fs1 =
+ fetcher.get_test_file_source("toolkit", None, vec![en_us.clone()], "toolkit/{locale}/");
+
+ let file1 = fs1.fetch_file(&en_us, &FTL_RESOURCE_PRESENT.into());
+ let file2 = fs1.fetch_file(&en_us, &FTL_RESOURCE_PRESENT.into());
+ assert!(file1.await.is_some());
+ assert!(file2.await.is_some());
+}
+
+#[test]
+fn file_source_sync_after_async_fail() {
+ let fetcher = TestFileFetcher::new();
+ let en_us: LanguageIdentifier = "en-US".parse().unwrap();
+ let fs1 =
+ fetcher.get_test_file_source("toolkit", None, vec![en_us.clone()], "toolkit/{locale}/");
+
+ let _ = fs1.fetch_file(&en_us, &FTL_RESOURCE_PRESENT.into());
+ let file2 = fs1.fetch_file_sync(&en_us, &FTL_RESOURCE_PRESENT.into(), true);
+ assert!(file2.is_some());
+}