From ffa088d4679d465841ba76c0cab7948996c3f9eb Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sat, 21 Mar 2026 23:09:48 +0100 Subject: [PATCH] tests: deduplicate locale availability checks into shared is_locale_available() --- tests/by-util/test_date.rs | 47 +--------- tests/by-util/test_sort.rs | 168 ++++++++++++++++++++++++---------- tests/uutests/src/lib/util.rs | 13 +++ 3 files changed, 140 insertions(+), 88 deletions(-) diff --git a/tests/by-util/test_date.rs b/tests/by-util/test_date.rs index db4637e62..a6139848c 100644 --- a/tests/by-util/test_date.rs +++ b/tests/by-util/test_date.rs @@ -13,6 +13,8 @@ use regex::Regex; #[cfg(all(unix, not(target_os = "macos")))] use uucore::process::geteuid; use uutests::util::TestScenario; +#[cfg(unix)] +use uutests::util::is_locale_available; use uutests::{at_and_ucmd, new_ucmd, util_name}; #[test] @@ -1840,20 +1842,7 @@ fn test_date_parenthesis_vs_other_special_chars() { fn test_date_iranian_locale_solar_hijri_calendar() { // Test Iranian locale uses Solar Hijri calendar // Verify the Solar Hijri calendar is used in the Iranian locale - use std::process::Command; - - // Check if Iranian locale is available - let locale_check = Command::new("locale") - .env("LC_ALL", "fa_IR.UTF-8") - .arg("charmap") - .output(); - - let locale_available = match locale_check { - Ok(output) => String::from_utf8_lossy(&output.stdout).trim() == "UTF-8", - Err(_) => false, - }; - - if !locale_available { + if !is_locale_available("fa_IR.UTF-8") { println!("Skipping Iranian locale test - fa_IR.UTF-8 locale not available"); return; } @@ -1920,20 +1909,7 @@ fn test_date_iranian_locale_solar_hijri_calendar() { fn test_date_ethiopian_locale_calendar() { // Test Ethiopian locale uses Ethiopian calendar // Verify the Ethiopian calendar is used in the Ethiopian locale - use std::process::Command; - - // Check if Ethiopian locale is available - let locale_check = Command::new("locale") - .env("LC_ALL", "am_ET.UTF-8") - .arg("charmap") - .output(); - - let locale_available = match locale_check { - Ok(output) => String::from_utf8_lossy(&output.stdout).trim() == "UTF-8", - Err(_) => false, - }; - - if !locale_available { + if !is_locale_available("am_ET.UTF-8") { println!("Skipping Ethiopian locale test - am_ET.UTF-8 locale not available"); return; } @@ -2000,20 +1976,7 @@ fn test_date_ethiopian_locale_calendar() { fn test_date_thai_locale_solar_calendar() { // Test Thai locale uses Thai solar calendar // Verify the Thai solar calendar is used with the Thai locale - use std::process::Command; - - // Check if Thai locale is available - let locale_check = Command::new("locale") - .env("LC_ALL", "th_TH.UTF-8") - .arg("charmap") - .output(); - - let locale_available = match locale_check { - Ok(output) => String::from_utf8_lossy(&output.stdout).trim() == "UTF-8", - Err(_) => false, - }; - - if !locale_available { + if !is_locale_available("th_TH.UTF-8") { println!("Skipping Thai locale test - th_TH.UTF-8 locale not available"); return; } diff --git a/tests/by-util/test_sort.rs b/tests/by-util/test_sort.rs index 7657e79ba..b0e7602fd 100644 --- a/tests/by-util/test_sort.rs +++ b/tests/by-util/test_sort.rs @@ -3,7 +3,7 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore (words) ints (linux) NOFILE dfgi +// spell-checker:ignore (words) ints (linux) NOFILE dfgi abmon avril #![allow(clippy::cast_possible_wrap)] use std::env; @@ -15,6 +15,8 @@ use std::time::Duration; use uutests::at_and_ucmd; use uutests::new_ucmd; use uutests::util::TestScenario; +#[cfg(unix)] +use uutests::util::is_locale_available; fn test_helper(file_name: &str, possible_args: &[&str]) { for args in possible_args { @@ -603,44 +605,106 @@ fn test_month_default2() { } } -#[test] -fn test_month_sort_french_locale() { - // Test locale-aware month sorting (mirrors GNU sort-month.sh test). - // Uses French UTF-8 locale where abbreviated months are: - // spell-checker:disable-next-line - // janv. févr. mars avril mai juin juil. août sept. oct. nov. déc. - let result = new_ucmd!() - .env("LC_ALL", "fr_FR.UTF-8") - .arg("-M") - // spell-checker:disable-next-line - .pipe_in("mai\ndéc.\njanv.\njuin\nfévr.\nmars\navril\njuil.\naoût\nsept.\noct.\nnov.\n") - .run(); - - // Skip if locale is not available on the system - if result.succeeded() { - assert_eq!( - result.stdout_str(), - "janv.\nfévr.\nmars\navril\nmai\njuin\njuil.\naoût\nsept.\noct.\nnov.\ndéc.\n", - ); +/// Query the system for abbreviated month names via `locale abmon`. +/// Returns a vector of 12 month abbreviations in order (Jan..Dec), +/// or None if the command fails or returns unexpected output. +#[cfg(any(target_vendor = "apple", target_os = "openbsd"))] +fn get_system_abmon(locale: &str) -> Option> { + let output = Command::new("locale") + .env("LC_ALL", locale) + .arg("abmon") + .output() + .ok()?; + if !output.status.success() { + return None; + } + let text = String::from_utf8(output.stdout).ok()?; + let months: Vec = text + .trim() + .split(';') + .map(String::from) + .filter(|m| !m.is_empty()) + .collect(); + if months.len() == 12 { + Some(months) + } else { + None } } -#[test] -fn test_month_sort_hungarian_locale() { - // Hungarian abbreviated months: jan febr márc ápr máj jún júl aug szept okt nov dec - let result = new_ucmd!() - .env("LC_ALL", "hu_HU.UTF-8") - .arg("-M") - // spell-checker:disable-next-line - .pipe_in("máj\ndec\njan\njún\nfebr\nmárc\nápr\njúl\naug\nszept\nokt\nnov\n") - .run(); +/// Build shuffled input and sorted expected output from month names. +#[cfg(any(target_vendor = "apple", target_os = "openbsd"))] +fn month_sort_input_expected(months: &[String]) -> (String, String) { + // Shuffled order: May, Dec, Jan, Jun, Feb, Mar, Apr, Jul, Aug, Sep, Oct, Nov + let shuffle_order = [4, 11, 0, 5, 1, 2, 3, 6, 7, 8, 9, 10]; + let input = shuffle_order + .iter() + .map(|&i| months[i].as_str()) + .collect::>() + .join("\n") + + "\n"; + let expected = months.join("\n") + "\n"; + (input, expected) +} - if result.succeeded() { - assert_eq!( - result.stdout_str(), - "jan\nfebr\nmárc\nápr\nmáj\njún\njúl\naug\nszept\nokt\nnov\ndec\n", - ); +#[test] +#[cfg(unix)] +fn test_month_sort_french_locale() { + let locale = "fr_FR.UTF-8"; + if !is_locale_available(locale) { + return; } + // spell-checker:disable + // On macOS/OpenBSD, abbreviated month names vary across OS versions (different CLDR data), + // so we query the system dynamically. On other platforms, glibc values are stable. + #[cfg(any(target_vendor = "apple", target_os = "openbsd"))] + let (input, expected) = { + let Some(months) = get_system_abmon(locale) else { + return; + }; + month_sort_input_expected(&months) + }; + #[cfg(not(any(target_vendor = "apple", target_os = "openbsd")))] + let (input, expected) = ( + "mai\ndéc.\njanv.\njuin\nfévr.\nmars\navril\njuil.\naoût\nsept.\noct.\nnov.\n".to_string(), + "janv.\nfévr.\nmars\navril\nmai\njuin\njuil.\naoût\nsept.\noct.\nnov.\ndéc.\n".to_string(), + ); + // spell-checker:enable + new_ucmd!() + .env("LC_ALL", locale) + .arg("-M") + .pipe_in(input) + .succeeds() + .stdout_is(expected); +} + +#[test] +#[cfg(unix)] +fn test_month_sort_hungarian_locale() { + let locale = "hu_HU.UTF-8"; + if !is_locale_available(locale) { + return; + } + // spell-checker:disable + #[cfg(any(target_vendor = "apple", target_os = "openbsd"))] + let (input, expected) = { + let Some(months) = get_system_abmon(locale) else { + return; + }; + month_sort_input_expected(&months) + }; + #[cfg(not(any(target_vendor = "apple", target_os = "openbsd")))] + let (input, expected) = ( + "máj\ndec\njan\njún\nfebr\nmárc\nápr\njúl\naug\nszept\nokt\nnov\n".to_string(), + "jan\nfebr\nmárc\nápr\nmáj\njún\njúl\naug\nszept\nokt\nnov\ndec\n".to_string(), + ); + // spell-checker:enable + new_ucmd!() + .env("LC_ALL", locale) + .arg("-M") + .pipe_in(input) + .succeeds() + .stdout_is(expected); } /// Test that embedded blanks in month names cause a non-match (GNU compat). @@ -698,20 +762,32 @@ fn test_month_sort_french_embedded_blanks() { } #[test] +#[cfg(unix)] fn test_month_sort_japanese_locale() { - // Japanese abbreviated months have leading spaces: " 1月" " 2月" ... "10月" "11月" "12月" - let result = new_ucmd!() - .env("LC_ALL", "ja_JP.UTF-8") - .arg("-M") - .pipe_in("5月\n12月\n1月\n6月\n2月\n3月\n4月\n7月\n8月\n9月\n10月\n11月\n") - .run(); - - if result.succeeded() { - assert_eq!( - result.stdout_str(), - "1月\n2月\n3月\n4月\n5月\n6月\n7月\n8月\n9月\n10月\n11月\n12月\n", - ); + let locale = "ja_JP.UTF-8"; + if !is_locale_available(locale) { + return; } + // On macOS/OpenBSD, abbreviated month names may differ, so query dynamically. + #[cfg(any(target_vendor = "apple", target_os = "openbsd"))] + let (input, expected) = { + let Some(months) = get_system_abmon(locale) else { + return; + }; + month_sort_input_expected(&months) + }; + // Japanese abbreviated months are numeric (1月..12月) on glibc + #[cfg(not(any(target_vendor = "apple", target_os = "openbsd")))] + let (input, expected) = ( + "5月\n12月\n1月\n6月\n2月\n3月\n4月\n7月\n8月\n9月\n10月\n11月\n".to_string(), + "1月\n2月\n3月\n4月\n5月\n6月\n7月\n8月\n9月\n10月\n11月\n12月\n".to_string(), + ); + new_ucmd!() + .env("LC_ALL", locale) + .arg("-M") + .pipe_in(input) + .succeeds() + .stdout_is(expected); } #[test] diff --git a/tests/uutests/src/lib/util.rs b/tests/uutests/src/lib/util.rs index 162584007..6e5e9690d 100644 --- a/tests/uutests/src/lib/util.rs +++ b/tests/uutests/src/lib/util.rs @@ -100,6 +100,19 @@ pub fn is_ci() -> bool { env::var("CI").is_ok_and(|s| s.eq_ignore_ascii_case("true")) } +/// Check if a locale is available on the system by verifying that +/// `locale charmap` returns `"UTF-8"` when `LC_ALL` is set to the given locale. +#[cfg(unix)] +pub fn is_locale_available(locale: &str) -> bool { + use std::process::Command; + Command::new("locale") + .env("LC_ALL", locale) + .arg("charmap") + .output() + .map(|o| String::from_utf8_lossy(&o.stdout).trim() == "UTF-8") + .unwrap_or(false) +} + /// Read a test scenario fixture, returning its bytes fn read_scenario_fixture>(tmpd: Option<&Rc>, file_rel_path: S) -> Vec { let tmpdir_path = tmpd.as_ref().unwrap().as_ref().path();