From e4bb67f518068807e4f65eb00d137ae656055103 Mon Sep 17 00:00:00 2001 From: KnifeLemon Date: Tue, 3 Nov 2026 14:25:48 +0900 Subject: [PATCH] test: make the OFD, spreadsheet and preset-editor tests locale-independent On a German system these tests failed although the code under test was right: the OFD test packages were built with culture-formatted numbers ("3,5" instead of "3.5"), the LibreOffice cross-check compared text Calc had formatted with its own locale's separators, and the two preset-editor tests checked labels that follow the regional format. Build the OFD markup with the invariant culture, compare the ODS-by-LibreOffice text without separators (and allow the XLS column width the same tolerance, since LibreOffice measures Korean text with its locale's East Asian font), and pin the culture in the two preset-editor tests. Co-authored-by: jojofox0815 <125126720+jojofox0815@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 --- tests/Filee.App.Tests/RenderTests.cs | 13 +++++++++++ tests/Filee.Engines.Tests/OfdTests.cs | 13 ++++++----- .../SpreadsheetFormatTests.cs | 22 +++++++++++++------ 3 files changed, 35 insertions(+), 13 deletions(-) diff --git a/tests/Filee.App.Tests/RenderTests.cs b/tests/Filee.App.Tests/RenderTests.cs index e871baf2a..52879e0c9 100644 --- a/tests/Filee.App.Tests/RenderTests.cs +++ b/tests/Filee.App.Tests/RenderTests.cs @@ -1,6 +1,7 @@ // Renders real windows headlessly (Skia) to catch XAML / binding errors and to produce screenshots // in bin/.../screenshots for visual review. +using System.Globalization; using Avalonia.Controls; using Avalonia.Controls.Primitives; using Avalonia.Headless; @@ -1608,6 +1609,7 @@ public void Preset_editor_keeps_rotation_visible_for_pdf_and_drops_it_for_target public void Preset_editor_saves_every_frame_only_for_png_and_jpg() { TestServices.EnsureInitialized("en"); + using var culture = new CultureScope("en-US"); // "100,000": numbers follow the regional format var loc = AppHost.Get(); var preset = new Filee.Core.Presets.Preset { TargetFormat = "jpg" }; @@ -1635,6 +1637,7 @@ public void Preset_editor_saves_every_frame_only_for_png_and_jpg() public void Preset_editor_shows_playback_speed_only_for_targets_that_keep_animations() { TestServices.EnsureInitialized("ko"); + using var culture = new CultureScope("ko-KR"); // "0.25×": numbers follow the regional format var loc = AppHost.Get(); var preset = new Filee.Core.Presets.Preset { TargetFormat = "gif", Name = "Slow GIF" }; var vm = new PresetEditorViewModel(preset, loc); @@ -2788,6 +2791,16 @@ internal static void Pump() } } + /// The regional format until disposed: numbers in labels follow it, not the UI language. + private sealed class CultureScope : IDisposable + { + private readonly CultureInfo _before = CultureInfo.CurrentCulture; + + public CultureScope(string name) => CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo(name); + + public void Dispose() => CultureInfo.CurrentCulture = _before; + } + internal static void Save(TopLevel window, string name) { // Disposed at the end, so the frame stays alive while Skia encodes it: a collection in between (the idle memory diff --git a/tests/Filee.Engines.Tests/OfdTests.cs b/tests/Filee.Engines.Tests/OfdTests.cs index 59963eedb..b778653f0 100644 --- a/tests/Filee.Engines.Tests/OfdTests.cs +++ b/tests/Filee.Engines.Tests/OfdTests.cs @@ -3,6 +3,7 @@ // with the frame and labels, values on the page, QR code and seal pictures), a WPS document (page size only in // Document.xml, decimal colours, wrapped body text over two pages), and hostile packages. +using System.Globalization; using System.IO.Compression; using System.Text; using System.Xml.Linq; @@ -65,10 +66,10 @@ private static string MakeInvoice(string folder) var template = new StringBuilder(); void Label(string text, double x, double y, double size = 3) { - template.Append($"""{text}"""); + template.Append(string.Create(CultureInfo.InvariantCulture, $"""{text}""")); } void Rule(double x0, double y0, double x1, double y1) => - template.Append($"""M 0 0 L {x1 - x0} {y1 - y0}"""); + template.Append(string.Create(CultureInfo.InvariantCulture, $"""M 0 0 L {x1 - x0} {y1 - y0}""")); Label("电子发票(普通发票)", 75, 12, 6); Label("发票号码:", 140, 18); Label("开票日期:", 140, 24); @@ -112,7 +113,7 @@ void Rule(double x0, double y0, double x1, double y1) => var page = new StringBuilder(); void Value(string text, double x, double y, int font = 4, double size = 3.5, string extra = "") => - page.Append($"""{text}"""); + page.Append(string.Create(CultureInfo.InvariantCulture, $"""{text}""")); page.Append(""""""); Value("25442000000123456789", 155.5, 18, 5, extra: " DeltaX=\"g 19 2.1\""); Value("2025年03月18日", 155.5, 24); @@ -260,7 +261,7 @@ private static string MakeWps(string folder) Text(zip, "Doc_0/Res.xml", $""""""); string Line(string text, double x, double y, int font = 3, double size = 5.6, string extra = "") => - $"""{text}"""; + string.Create(CultureInfo.InvariantCulture, $"""{text}"""); var first = new StringBuilder(); first.Append(Line("医院信息化建设方案", 67.15, 40, 4, 8.5, " Weight=\"700\"")); // A paragraph of three lines on a 156.8 mm measure (28 characters): the first indented by two characters. @@ -334,7 +335,7 @@ private static string MakeOfd(string folder, string page, string template = "", } private static string TextObject(string text, double x, double y, double size = 3.175, int font = 3, string code = "", double width = 60) => - $"""{text}"""; + string.Create(CultureInfo.InvariantCulture, $"""{text}"""); [Fact] public void A_block_drawn_by_one_text_code_keeps_its_lines_and_a_short_delta_list_goes_on() @@ -509,7 +510,7 @@ public async Task Columns_of_a_list_keep_their_rows_level_and_the_page_its_heigh { var template = new StringBuilder(); void Rule(double x0, double y0, double x1, double y1) => - template.Append($"""M 0 0 L {x1 - x0} {y1 - y0}"""); + template.Append(string.Create(CultureInfo.InvariantCulture, $"""M 0 0 L {x1 - x0} {y1 - y0}""")); Rule(5, 30, 205, 30); Rule(5, 37, 205, 37); Rule(5, 110, 205, 110); diff --git a/tests/Filee.Engines.Tests/SpreadsheetFormatTests.cs b/tests/Filee.Engines.Tests/SpreadsheetFormatTests.cs index 738fce0d6..9dd0e4301 100644 --- a/tests/Filee.Engines.Tests/SpreadsheetFormatTests.cs +++ b/tests/Filee.Engines.Tests/SpreadsheetFormatTests.cs @@ -5,6 +5,7 @@ using System.IO.Compression; using System.Text; +using System.Text.RegularExpressions; using System.Xml.Linq; using Filee.Core.Conversion; using Filee.Core.Presets; @@ -598,22 +599,29 @@ public async Task LibreOffice_reads_our_ods_and_we_read_its_ods_and_xls() foreach (var (name, book) in books) { var sales = book.Sheets[0]; + // An ODS by LibreOffice keeps the text Calc showed (text:p), with the separators of Calc's locale + // ("1.350.000원", "12,8%" on a German system): compare those without the separators. + Func shown = name == "ODS by LibreOffice" ? WithoutSeparators : text => text; Assert.True(new[] { "매출", "메모", "넓은 표" }.SequenceEqual(book.Sheets.Select(s => s.Name)), name); - Assert.True(sales.Get(2, 1)!.Text == "1,350,000원", $"{name}: {sales.Get(2, 1)!.Text}"); - Assert.True(sales.Get(2, 3)!.Text == "12.8%", $"{name}: {sales.Get(2, 3)!.Text}"); + Assert.True(shown(sales.Get(2, 1)!.Text) == shown("1,350,000원"), $"{name}: {sales.Get(2, 1)!.Text}"); + Assert.True(shown(sales.Get(2, 3)!.Text) == shown("12.8%"), $"{name}: {sales.Get(2, 3)!.Text}"); // LibreOffice shows built-in dates in its own locale's order: compare the value. Assert.True(sales.Get(2, 2)!.Value == October1, $"{name}: {sales.Get(2, 2)!.Value}"); - Assert.True(sales.Get(4, 1)!.Text == "2,220,000원", $"{name}: {sales.Get(4, 1)!.Text}"); + Assert.True(shown(sales.Get(4, 1)!.Text) == shown("2,220,000원"), $"{name}: {sales.Get(4, 1)!.Text}"); Assert.True(new CellRange(0, 0, 0, 3) == Assert.Single(sales.Merges), name); Assert.True(sales.Get(1, 0)!.Style.Align == CellAlign.Center, name); - // LibreOffice measures character widths with its own fonts, so widths through ODS move by up to a fifth; - // XLS keeps Excel's own unit. - var tolerance = name.StartsWith("XLS", StringComparison.Ordinal) ? 0.01 : 0.25; - Assert.True(Math.Abs(sales.ColumnWidth(0) - 14) <= 14 * tolerance, $"{name}: {sales.ColumnWidth(0)}"); + // LibreOffice measures character widths with its own fonts, so widths it writes move by up to a fifth. XLS + // keeps Excel's unit, but for Korean text LibreOffice measures with its locale's default East Asian font + // (Microsoft YaHei on a German system: 15.27 for 14). The ratio of the columns stays. + Assert.True(Math.Abs(sales.ColumnWidth(0) - 14) <= 14 * 0.25, $"{name}: {sales.ColumnWidth(0)}"); Assert.True(Math.Abs(sales.ColumnWidth(0) / sales.ColumnWidth(1) - 14.0 / 12) < 0.02, $"{name}: {sales.ColumnWidth(0)} / {sales.ColumnWidth(1)}"); } } + /// "1.350.000원" and "1,350,000원" as "1350000원", "12,8 %" and "12.8%" as "12.8%". + private static string WithoutSeparators(string text) => + Regex.Replace(Regex.Replace(text, @"(?<=\d)[.,\u00A0\u202F' ](?=\d{3}(?!\d))", ""), @"(?<=\d),(?=\d)", ".").Replace(" %", "%"); + private async Task LibreOfficeAsync(IConverter libreOffice, string input, string from, string to) { var output = Path.Combine(Path.GetDirectoryName(input)!, $"{Path.GetFileNameWithoutExtension(input)}.lo-{from}.{to}");