[wasm][wbt] Split WBT classes to speed up CI job execution. (#89926)
authorIlona Tomkowicz <32700855+ilonatommy@users.noreply.github.com>
Fri, 4 Aug 2023 21:18:47 +0000 (23:18 +0200)
committerGitHub <noreply@github.com>
Fri, 4 Aug 2023 21:18:47 +0000 (17:18 -0400)
IcuSharding and BuildPublish on Blazor always take the longest. I split them more or less into equal test count per class (~20 in icu, ~10 in build publish).

This PR changes the times on wbt linux CI when we split into BuldPublish and BuildPublishDefaultTemplate:
Build time: ~1,5min -> ~1,5min
Run time: ~53min -> ~45min
Original time values taken from build [20230802.154](https://dev.azure.com/dnceng-public/public/_build/results?buildId=361726) with 495 301 tests.

It's fine but the BuildPublish is the only test that keeps running till the end, around 30th minute all the other wbt are done already, so trying the 3-file split approach:
- BuildPublishTestsFromWasmTemplate,
- BuildPublishTestsFromBlazorTemplate (the same test set as BuildPublishDefaultTemplate, just renamed to better describe the contents) and
- BuildPublish,

each <10 tests.
Build time: ~1,5min -> ~1,5min
Run time: ~53min -> 53 min - it seems 3 files split did not work as expected, @radical, it might be an outliner ~~but in this situation it does not look worthy to merge the changes~~.
[build 20230804.3](https://dev.azure.com/dnceng-public/public/_build/results?buildId=363212&view=logs&jobId=9c08ee28-c5cd-54b8-6ae3-9fcb797291cc&j=0b2e5ab0-105d-5e21-8497-262338385634)
Okay, on the re-run it's already fine, [35 min as expected](https://dev.azure.com/dnceng-public/public/_build/results?buildId=363553&view=logs&jobId=10714551-d8dc-5291-8d0a-07fdfc20529c&j=1fa93050-f528-55d3-a351-f8bf9ce5adbf&t=a9f1a437-3b59-5900-1137-ec6cae9a530c).

On windows it changed ~60min -> 42 min.

eng/testing/scenarios/BuildWasmAppsJobsList.txt
src/mono/wasm/Wasm.Build.Tests/Blazor/BuildPublishTests.cs
src/mono/wasm/Wasm.Build.Tests/Blazor/MiscTests3.cs [new file with mode: 0644]
src/mono/wasm/Wasm.Build.Tests/Blazor/SimpleRunTests.cs [new file with mode: 0644]
src/mono/wasm/Wasm.Build.Tests/IcuShardingTests.cs [new file with mode: 0644]
src/mono/wasm/Wasm.Build.Tests/IcuShardingTests2.cs [new file with mode: 0644]
src/mono/wasm/Wasm.Build.Tests/IcuTests.cs
src/mono/wasm/Wasm.Build.Tests/IcuTestsBase.cs [new file with mode: 0644]

index cc16b81..1c975f6 100644 (file)
@@ -6,15 +6,19 @@ Wasm.Build.NativeRebuild.Tests.SimpleSourceChangeRebuildTest
 Wasm.Build.Templates.Tests.NativeBuildTests
 Wasm.Build.Tests.Blazor.AppsettingsTests
 Wasm.Build.Tests.Blazor.BuildPublishTests
+Wasm.Build.Tests.Blazor.SimpleRunTests
 Wasm.Build.Tests.Blazor.CleanTests
 Wasm.Build.Tests.Blazor.MiscTests
 Wasm.Build.Tests.Blazor.MiscTests2
+Wasm.Build.Tests.Blazor.MiscTests3
 Wasm.Build.Tests.Blazor.NativeTests
 Wasm.Build.Tests.Blazor.NoopNativeRebuildTest
 Wasm.Build.Tests.BuildPublishTests
 Wasm.Build.Tests.ConfigSrcTests
 Wasm.Build.Tests.HybridGlobalizationTests
 Wasm.Build.Tests.IcuShardingTests
+Wasm.Build.Tests.IcuShardingTests2
+Wasm.Build.Tests.IcuTests
 Wasm.Build.Tests.InvariantGlobalizationTests
 Wasm.Build.Tests.InvariantTimezoneTests
 Wasm.Build.Tests.MainWithArgsTests
index 9313a9c..08d1310 100644 (file)
@@ -99,182 +99,4 @@ public class BuildPublishTests : BlazorWasmTestBase
     //// publish again, no AOT
     //BlazorPublish(new BlazorBuildOptions(id, config, NativeFilesType.Relinked);
     //}
-
-    [Theory]
-    [InlineData("Debug", /*build*/true, /*publish*/false)]
-    [InlineData("Debug", /*build*/false, /*publish*/true)]
-    [InlineData("Debug", /*build*/true, /*publish*/true)]
-    [InlineData("Release", /*build*/true, /*publish*/false)]
-    [InlineData("Release", /*build*/false, /*publish*/true)]
-    [InlineData("Release", /*build*/true, /*publish*/true)]
-    [ActiveIssue("https://github.com/dotnet/runtime/issues/87877", TestPlatforms.Windows)]
-    public async Task WithDllImportInMainAssembly(string config, bool build, bool publish)
-    {
-        // Based on https://github.com/dotnet/runtime/issues/59255
-        string id = $"blz_dllimp_{config}_{s_unicodeChar}";
-        if (build && publish)
-            id += "build_then_publish";
-        else if (build)
-            id += "build";
-        else
-            id += "publish";
-
-        string projectFile = CreateProjectWithNativeReference(id);
-        string nativeSource = @"
-            #include <stdio.h>
-
-            extern ""C"" {
-                int cpp_add(int a, int b) {
-                    return a + b;
-                }
-            }";
-
-        File.WriteAllText(Path.Combine(_projectDir!, "mylib.cpp"), nativeSource);
-
-        string myDllImportCs = @$"
-            using System.Runtime.InteropServices;
-            namespace {id};
-
-            public static class MyDllImports
-            {{
-                [DllImport(""mylib"")]
-                public static extern int cpp_add(int a, int b);
-            }}";
-
-        File.WriteAllText(Path.Combine(_projectDir!, "Pages", "MyDllImport.cs"), myDllImportCs);
-
-        AddItemsPropertiesToProject(projectFile, extraItems: @"<NativeFileReference Include=""mylib.cpp"" />");
-        BlazorAddRazorButton("cpp_add", """
-            var result = MyDllImports.cpp_add(10, 12);
-            outputText = $"{result}";
-        """);
-
-        if (build)
-            BlazorBuild(new BlazorBuildOptions(id, config, NativeFilesType.Relinked));
-
-        if (publish)
-            BlazorPublish(new BlazorBuildOptions(id, config, NativeFilesType.Relinked, ExpectRelinkDirWhenPublishing: build));
-
-        BlazorRunOptions runOptions = new() { Config = config, Test = TestDllImport };
-        if (publish)
-            await BlazorRunForPublishWithWebServer(runOptions);
-        else
-            await BlazorRunForBuildWithDotnetRun(runOptions);
-
-        async Task TestDllImport(IPage page)
-        {
-            await page.Locator("text=\"cpp_add\"").ClickAsync();
-            var txt = await page.Locator("p[role='test']").InnerHTMLAsync();
-            Assert.Equal("Output: 22", txt);
-        }
-    }
-
-    [Fact]
-    public void BugRegression_60479_WithRazorClassLib()
-    {
-        string id = $"blz_razor_lib_top_{GetRandomId()}";
-        InitBlazorWasmProjectDir(id);
-
-        string wasmProjectDir = Path.Combine(_projectDir!, "wasm");
-        string wasmProjectFile = Path.Combine(wasmProjectDir, "wasm.csproj");
-        Directory.CreateDirectory(wasmProjectDir);
-        new DotNetCommand(s_buildEnv, _testOutput, useDefaultArgs: false)
-                .WithWorkingDirectory(wasmProjectDir)
-                .WithEnvironmentVariable("NUGET_PACKAGES", _nugetPackagesDir)
-                .ExecuteWithCapturedOutput("new blazorwasm")
-                .EnsureSuccessful();
-
-
-        string razorProjectDir = Path.Combine(_projectDir!, "RazorClassLibrary");
-        Directory.CreateDirectory(razorProjectDir);
-        new DotNetCommand(s_buildEnv, _testOutput, useDefaultArgs: false)
-                .WithWorkingDirectory(razorProjectDir)
-                .WithEnvironmentVariable("NUGET_PACKAGES", _nugetPackagesDir)
-                .ExecuteWithCapturedOutput("new razorclasslib")
-                .EnsureSuccessful();
-
-        string razorClassLibraryFileName = $"RazorClassLibrary{ProjectProviderBase.WasmAssemblyExtension}";
-        AddItemsPropertiesToProject(wasmProjectFile, extraItems: @$"
-            <ProjectReference Include=""..\\RazorClassLibrary\\RazorClassLibrary.csproj"" />
-            <BlazorWebAssemblyLazyLoad Include=""{razorClassLibraryFileName}"" />
-        ");
-
-        _projectDir = wasmProjectDir;
-        string config = "Release";
-        // No relinking, no AOT
-        BlazorBuild(new BlazorBuildOptions(id, config, NativeFilesType.FromRuntimePack));
-
-        // will relink
-        BlazorPublish(new BlazorBuildOptions(id, config, NativeFilesType.Relinked, ExpectRelinkDirWhenPublishing: true));
-
-        // publish/wwwroot/_framework/blazor.boot.json
-        string frameworkDir = FindBlazorBinFrameworkDir(config, forPublish: true);
-        string bootJson = Path.Combine(frameworkDir, "blazor.boot.json");
-
-        Assert.True(File.Exists(bootJson), $"Could not find {bootJson}");
-        var jdoc = JsonDocument.Parse(File.ReadAllText(bootJson));
-        if (!jdoc.RootElement.TryGetProperty("resources", out JsonElement resValue) ||
-            !resValue.TryGetProperty("lazyAssembly", out JsonElement lazyVal))
-        {
-            throw new XunitException($"Could not find resources.lazyAssembly object in {bootJson}");
-        }
-
-        Assert.Contains(razorClassLibraryFileName, lazyVal.EnumerateObject().Select(jp => jp.Name));
-    }
-
-    [ConditionalTheory(typeof(BuildTestBase), nameof(IsUsingWorkloads))]
-    [InlineData("Debug")]
-    [InlineData("Release")]
-    public async Task BlazorBuildRunTest(string config)
-    {
-        string id = $"blazor_{config}_{GetRandomId()}";
-        string projectFile = CreateWasmTemplateProject(id, "blazorwasm");
-
-        BlazorBuild(new BlazorBuildOptions(id, config, NativeFilesType.FromRuntimePack));
-        await BlazorRunForBuildWithDotnetRun(new BlazorRunOptions() { Config = config });
-    }
-
-    [ConditionalTheory(typeof(BuildTestBase), nameof(IsUsingWorkloads))]
-    [InlineData("Debug", false)]
-    [InlineData("Debug", true)]
-    [InlineData("Release", false)]
-    [InlineData("Release", true)]
-    public async Task BlazorPublishRunTest(string config, bool aot)
-    {
-        string id = $"blazor_{config}_{GetRandomId()}";
-        string projectFile = CreateWasmTemplateProject(id, "blazorwasm");
-        if (aot)
-            AddItemsPropertiesToProject(projectFile, "<RunAOTCompilation>true</RunAOTCompilation>");
-
-        BlazorPublish(new BlazorBuildOptions(
-            id,
-            config,
-            aot ? NativeFilesType.AOT
-                : (config == "Release" ? NativeFilesType.Relinked : NativeFilesType.FromRuntimePack)));
-        await BlazorRunForPublishWithWebServer(new BlazorRunOptions() { Config = config });
-    }
-
-    private void BlazorAddRazorButton(string buttonText, string customCode, string methodName = "test", string razorPage = "Pages/Counter.razor")
-    {
-        string additionalCode = $$"""
-            <p role="{{methodName}}">Output: @outputText</p>
-            <button class="btn btn-primary" @onclick="{{methodName}}">{{buttonText}}</button>
-
-            @code {
-                private string outputText = string.Empty;
-                public void {{methodName}}()
-                {
-                    {{customCode}}
-                }
-            }
-        """;
-
-        // find blazor's Counter.razor
-        string counterRazorPath = Path.Combine(_projectDir!, razorPage);
-        if (!File.Exists(counterRazorPath))
-            throw new FileNotFoundException($"Could not find {counterRazorPath}");
-
-        string oldContent = File.ReadAllText(counterRazorPath);
-        File.WriteAllText(counterRazorPath, oldContent + additionalCode);
-    }
 }
diff --git a/src/mono/wasm/Wasm.Build.Tests/Blazor/MiscTests3.cs b/src/mono/wasm/Wasm.Build.Tests/Blazor/MiscTests3.cs
new file mode 100644 (file)
index 0000000..10717b3
--- /dev/null
@@ -0,0 +1,170 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.IO;
+using System.Linq;
+using System.Text.Json;
+using System.Threading.Tasks;
+using Xunit;
+using Xunit.Abstractions;
+using Xunit.Sdk;
+using Microsoft.Playwright;
+
+#nullable enable
+
+namespace Wasm.Build.Tests.Blazor;
+
+public class MiscTests3 : BlazorWasmTestBase
+{
+    public MiscTests3(ITestOutputHelper output, SharedBuildPerTestClassFixture buildContext)
+        : base(output, buildContext)
+    {
+        _enablePerTestCleanup = true;
+    }
+
+    [Theory]
+    [InlineData("Debug", /*build*/true, /*publish*/false)]
+    [InlineData("Debug", /*build*/false, /*publish*/true)]
+    [InlineData("Debug", /*build*/true, /*publish*/true)]
+    [InlineData("Release", /*build*/true, /*publish*/false)]
+    [InlineData("Release", /*build*/false, /*publish*/true)]
+    [InlineData("Release", /*build*/true, /*publish*/true)]
+    [ActiveIssue("https://github.com/dotnet/runtime/issues/87877", TestPlatforms.Windows)]
+    public async Task WithDllImportInMainAssembly(string config, bool build, bool publish)
+    {
+        // Based on https://github.com/dotnet/runtime/issues/59255
+        string id = $"blz_dllimp_{config}_{s_unicodeChar}";
+        if (build && publish)
+            id += "build_then_publish";
+        else if (build)
+            id += "build";
+        else
+            id += "publish";
+
+        string projectFile = CreateProjectWithNativeReference(id);
+        string nativeSource = @"
+            #include <stdio.h>
+
+            extern ""C"" {
+                int cpp_add(int a, int b) {
+                    return a + b;
+                }
+            }";
+
+        File.WriteAllText(Path.Combine(_projectDir!, "mylib.cpp"), nativeSource);
+
+        string myDllImportCs = @$"
+            using System.Runtime.InteropServices;
+            namespace {id};
+
+            public static class MyDllImports
+            {{
+                [DllImport(""mylib"")]
+                public static extern int cpp_add(int a, int b);
+            }}";
+
+        File.WriteAllText(Path.Combine(_projectDir!, "Pages", "MyDllImport.cs"), myDllImportCs);
+
+        AddItemsPropertiesToProject(projectFile, extraItems: @"<NativeFileReference Include=""mylib.cpp"" />");
+        BlazorAddRazorButton("cpp_add", """
+            var result = MyDllImports.cpp_add(10, 12);
+            outputText = $"{result}";
+        """);
+
+        if (build)
+            BlazorBuild(new BlazorBuildOptions(id, config, NativeFilesType.Relinked));
+
+        if (publish)
+            BlazorPublish(new BlazorBuildOptions(id, config, NativeFilesType.Relinked, ExpectRelinkDirWhenPublishing: build));
+
+        BlazorRunOptions runOptions = new() { Config = config, Test = TestDllImport };
+        if (publish)
+            await BlazorRunForPublishWithWebServer(runOptions);
+        else
+            await BlazorRunForBuildWithDotnetRun(runOptions);
+
+        async Task TestDllImport(IPage page)
+        {
+            await page.Locator("text=\"cpp_add\"").ClickAsync();
+            var txt = await page.Locator("p[role='test']").InnerHTMLAsync();
+            Assert.Equal("Output: 22", txt);
+        }
+    }
+
+    [Fact]
+    public void BugRegression_60479_WithRazorClassLib()
+    {
+        string id = $"blz_razor_lib_top_{GetRandomId()}";
+        InitBlazorWasmProjectDir(id);
+
+        string wasmProjectDir = Path.Combine(_projectDir!, "wasm");
+        string wasmProjectFile = Path.Combine(wasmProjectDir, "wasm.csproj");
+        Directory.CreateDirectory(wasmProjectDir);
+        new DotNetCommand(s_buildEnv, _testOutput, useDefaultArgs: false)
+                .WithWorkingDirectory(wasmProjectDir)
+                .WithEnvironmentVariable("NUGET_PACKAGES", _nugetPackagesDir)
+                .ExecuteWithCapturedOutput("new blazorwasm")
+                .EnsureSuccessful();
+
+
+        string razorProjectDir = Path.Combine(_projectDir!, "RazorClassLibrary");
+        Directory.CreateDirectory(razorProjectDir);
+        new DotNetCommand(s_buildEnv, _testOutput, useDefaultArgs: false)
+                .WithWorkingDirectory(razorProjectDir)
+                .WithEnvironmentVariable("NUGET_PACKAGES", _nugetPackagesDir)
+                .ExecuteWithCapturedOutput("new razorclasslib")
+                .EnsureSuccessful();
+
+        string razorClassLibraryFileName = $"RazorClassLibrary{ProjectProviderBase.WasmAssemblyExtension}";
+        AddItemsPropertiesToProject(wasmProjectFile, extraItems: @$"
+            <ProjectReference Include=""..\\RazorClassLibrary\\RazorClassLibrary.csproj"" />
+            <BlazorWebAssemblyLazyLoad Include=""{razorClassLibraryFileName}"" />
+        ");
+
+        _projectDir = wasmProjectDir;
+        string config = "Release";
+        // No relinking, no AOT
+        BlazorBuild(new BlazorBuildOptions(id, config, NativeFilesType.FromRuntimePack));
+
+        // will relink
+        BlazorPublish(new BlazorBuildOptions(id, config, NativeFilesType.Relinked, ExpectRelinkDirWhenPublishing: true));
+
+        // publish/wwwroot/_framework/blazor.boot.json
+        string frameworkDir = FindBlazorBinFrameworkDir(config, forPublish: true);
+        string bootJson = Path.Combine(frameworkDir, "blazor.boot.json");
+
+        Assert.True(File.Exists(bootJson), $"Could not find {bootJson}");
+        var jdoc = JsonDocument.Parse(File.ReadAllText(bootJson));
+        if (!jdoc.RootElement.TryGetProperty("resources", out JsonElement resValue) ||
+            !resValue.TryGetProperty("lazyAssembly", out JsonElement lazyVal))
+        {
+            throw new XunitException($"Could not find resources.lazyAssembly object in {bootJson}");
+        }
+
+        Assert.Contains(razorClassLibraryFileName, lazyVal.EnumerateObject().Select(jp => jp.Name));
+    }
+
+    private void BlazorAddRazorButton(string buttonText, string customCode, string methodName = "test", string razorPage = "Pages/Counter.razor")
+    {
+        string additionalCode = $$"""
+            <p role="{{methodName}}">Output: @outputText</p>
+            <button class="btn btn-primary" @onclick="{{methodName}}">{{buttonText}}</button>
+
+            @code {
+                private string outputText = string.Empty;
+                public void {{methodName}}()
+                {
+                    {{customCode}}
+                }
+            }
+        """;
+
+        // find blazor's Counter.razor
+        string counterRazorPath = Path.Combine(_projectDir!, razorPage);
+        if (!File.Exists(counterRazorPath))
+            throw new FileNotFoundException($"Could not find {counterRazorPath}");
+
+        string oldContent = File.ReadAllText(counterRazorPath);
+        File.WriteAllText(counterRazorPath, oldContent + additionalCode);
+    }
+}
diff --git a/src/mono/wasm/Wasm.Build.Tests/Blazor/SimpleRunTests.cs b/src/mono/wasm/Wasm.Build.Tests/Blazor/SimpleRunTests.cs
new file mode 100644 (file)
index 0000000..3a8968c
--- /dev/null
@@ -0,0 +1,56 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.IO;
+using System.Linq;
+using System.Text.Json;
+using System.Threading.Tasks;
+using Xunit;
+using Xunit.Abstractions;
+using Xunit.Sdk;
+using Microsoft.Playwright;
+
+#nullable enable
+
+namespace Wasm.Build.Tests.Blazor;
+
+public class SimpleRunTests : BlazorWasmTestBase
+{
+    public SimpleRunTests(ITestOutputHelper output, SharedBuildPerTestClassFixture buildContext)
+        : base(output, buildContext)
+    {
+        _enablePerTestCleanup = true;
+    }
+
+    [ConditionalTheory(typeof(BuildTestBase), nameof(IsUsingWorkloads))]
+    [InlineData("Debug")]
+    [InlineData("Release")]
+    public async Task BlazorBuildRunTest(string config)
+    {
+        string id = $"blazor_{config}_{GetRandomId()}";
+        string projectFile = CreateWasmTemplateProject(id, "blazorwasm");
+
+        BlazorBuild(new BlazorBuildOptions(id, config, NativeFilesType.FromRuntimePack));
+        await BlazorRunForBuildWithDotnetRun(new BlazorRunOptions() { Config = config });
+    }
+
+    [ConditionalTheory(typeof(BuildTestBase), nameof(IsUsingWorkloads))]
+    [InlineData("Debug", false)]
+    [InlineData("Debug", true)]
+    [InlineData("Release", false)]
+    [InlineData("Release", true)]
+    public async Task BlazorPublishRunTest(string config, bool aot)
+    {
+        string id = $"blazor_{config}_{GetRandomId()}";
+        string projectFile = CreateWasmTemplateProject(id, "blazorwasm");
+        if (aot)
+            AddItemsPropertiesToProject(projectFile, "<RunAOTCompilation>true</RunAOTCompilation>");
+
+        BlazorPublish(new BlazorBuildOptions(
+            id,
+            config,
+            aot ? NativeFilesType.AOT
+                : (config == "Release" ? NativeFilesType.Relinked : NativeFilesType.FromRuntimePack)));
+        await BlazorRunForPublishWithWebServer(new BlazorRunOptions() { Config = config });
+    }
+}
diff --git a/src/mono/wasm/Wasm.Build.Tests/IcuShardingTests.cs b/src/mono/wasm/Wasm.Build.Tests/IcuShardingTests.cs
new file mode 100644 (file)
index 0000000..a14e644
--- /dev/null
@@ -0,0 +1,63 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.IO;
+using Xunit;
+using Xunit.Abstractions;
+using Xunit.Sdk;
+using System.Collections.Generic;
+
+#nullable enable
+
+namespace Wasm.Build.Tests;
+
+public class IcuShardingTests : IcuTestsBase
+{
+    public IcuShardingTests(ITestOutputHelper output, SharedBuildPerTestClassFixture buildContext)
+        : base(output, buildContext) { }
+
+    public static IEnumerable<object?[]> IcuExpectedAndMissingCustomShardTestData(bool aot, RunHost host)
+        => ConfigWithAOTData(aot)
+            .Multiply(
+                new object[] { s_customIcuPath, s_customIcuTestedLocales, false },
+                new object[] { s_customIcuPath, s_customIcuTestedLocales, true })
+            .WithRunHosts(host)
+            .UnwrapItemsAsArrays();
+
+    public static IEnumerable<object?[]> IcuExpectedAndMissingAutomaticShardTestData(bool aot)
+        => ConfigWithAOTData(aot)
+            .Multiply(
+                new object[] { "fr-FR", GetEfigsTestedLocales(SundayNames.French)},
+                new object[] { "ja-JP", GetCjkTestedLocales(SundayNames.Japanese) },
+                new object[] { "sk-SK", GetNocjkTestedLocales(SundayNames.Slovak) })
+            .WithRunHosts(BuildTestBase.s_hostsForOSLocaleSensitiveTests)
+            .UnwrapItemsAsArrays();
+
+    [Theory]
+    [MemberData(nameof(IcuExpectedAndMissingCustomShardTestData), parameters: new object[] { false, RunHost.NodeJS | RunHost.Chrome })]
+    [MemberData(nameof(IcuExpectedAndMissingCustomShardTestData), parameters: new object[] { true, RunHost.NodeJS | RunHost.Chrome })]
+    public void CustomIcuShard(BuildArgs buildArgs, string shardName, string testedLocales, bool onlyPredefinedCultures, RunHost host, string id) =>
+        TestIcuShards(buildArgs, shardName, testedLocales, host, id, onlyPredefinedCultures);
+
+    [Theory]
+    [MemberData(nameof(IcuExpectedAndMissingAutomaticShardTestData), parameters: new object[] { false })]
+    [MemberData(nameof(IcuExpectedAndMissingAutomaticShardTestData), parameters: new object[] { true })]
+    public void AutomaticShardSelectionDependingOnEnvLocale(BuildArgs buildArgs, string environmentLocale, string testedLocales, RunHost host, string id)
+    {
+        string projectName = $"automatic_shard_{environmentLocale}_{buildArgs.Config}_{buildArgs.AOT}";
+        bool dotnetWasmFromRuntimePack = !(buildArgs.AOT || buildArgs.Config == "Release");
+
+        buildArgs = buildArgs with { ProjectName = projectName };
+        buildArgs = ExpandBuildArgs(buildArgs);
+
+        string programText = GetProgramText(testedLocales);
+        _testOutput.WriteLine($"----- Program: -----{Environment.NewLine}{programText}{Environment.NewLine}-------");
+        (_, string output) = BuildProject(buildArgs,
+                        id: id,
+                        new BuildProjectOptions(
+                            InitProject: () => File.WriteAllText(Path.Combine(_projectDir!, "Program.cs"), programText),
+                            DotnetWasmFromRuntimePack: dotnetWasmFromRuntimePack));
+        string runOutput = RunAndTestWasmApp(buildArgs, buildDir: _projectDir, expectedExitCode: 42, host: host, id: id, environmentLocale: environmentLocale);
+    }
+}
diff --git a/src/mono/wasm/Wasm.Build.Tests/IcuShardingTests2.cs b/src/mono/wasm/Wasm.Build.Tests/IcuShardingTests2.cs
new file mode 100644 (file)
index 0000000..c04d5c1
--- /dev/null
@@ -0,0 +1,39 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.IO;
+using Xunit;
+using Xunit.Abstractions;
+using Xunit.Sdk;
+using System.Collections.Generic;
+
+#nullable enable
+
+namespace Wasm.Build.Tests;
+
+public class IcuShardingTests2 : IcuTestsBase
+{
+    public IcuShardingTests2(ITestOutputHelper output, SharedBuildPerTestClassFixture buildContext)
+        : base(output, buildContext) { }
+
+    public static IEnumerable<object?[]> IcuExpectedAndMissingShardFromRuntimePackTestData(bool aot, RunHost host)
+        => ConfigWithAOTData(aot)
+            .Multiply(
+                new object[] { "icudt.dat",
+                                $@"new Locale[] {{
+                                    new Locale(""en-GB"", ""{SundayNames.English}""), new Locale(""zh-CN"", ""{SundayNames.Chinese}""), new Locale(""sk-SK"", ""{SundayNames.Slovak}""),
+                                    new Locale(""xx-yy"", null) }}" },
+                new object[] { "icudt_EFIGS.dat", GetEfigsTestedLocales() },
+                new object[] { "icudt_CJK.dat", GetCjkTestedLocales() },
+                new object[] { "icudt_no_CJK.dat", GetNocjkTestedLocales() })
+            .WithRunHosts(host)
+            .UnwrapItemsAsArrays();
+
+
+    [Theory]
+    [MemberData(nameof(IcuExpectedAndMissingShardFromRuntimePackTestData), parameters: new object[] { false, RunHost.NodeJS | RunHost.Chrome })]
+    [MemberData(nameof(IcuExpectedAndMissingShardFromRuntimePackTestData), parameters: new object[] { true, RunHost.NodeJS | RunHost.Chrome })]
+    public void DefaultAvailableIcuShardsFromRuntimePack(BuildArgs buildArgs, string shardName, string testedLocales, RunHost host, string id) =>
+        TestIcuShards(buildArgs, shardName, testedLocales, host, id);
+}
\ No newline at end of file
index 73c3d49..c7a18a4 100644 (file)
@@ -12,74 +12,11 @@ using System.Collections.Generic;
 
 namespace Wasm.Build.Tests;
 
-public class IcuTests : TestMainJsTestBase
+public class IcuTests : IcuTestsBase
 {
     public IcuTests(ITestOutputHelper output, SharedBuildPerTestClassFixture buildContext)
         : base(output, buildContext) { }
 
-    // custom file contains only locales "cy-GB", "is-IS", "bs-BA", "lb-LU" and fallback locale: "en-US":
-    private static string s_customIcuPath = Path.Combine(BuildEnvironment.TestAssetsPath, "icudt_custom.dat");
-    public record SundayNames {
-        public static string English = "Sunday";
-        public static string French = "dimanche";
-        public static string Spanish = "domingo";
-        public static string Chinese = "星期日";
-        public static string Japanese = "日曜日";
-        public static string Slovak = "nedeľa";
-    }
-
-    private const string FallbackSundayNameEnUS = "Sunday";
-
-    private static readonly string s_customIcuTestedLocales = $@"new Locale[] {{
-        new Locale(""cy-GB"",  ""Dydd Sul""), new Locale(""is-IS"",  ""sunnudagur""), new Locale(""bs-BA"",  ""nedjelja""), new Locale(""lb-LU"",  ""Sonndeg""),
-        new Locale(""fr-FR"", null), new Locale(""hr-HR"", null), new Locale(""ko-KR"", null)
-    }}";
-    private static string GetEfigsTestedLocales(string fallbackSundayName=FallbackSundayNameEnUS) =>  $@"new Locale[] {{
-        new Locale(""en-US"", ""{SundayNames.English}""), new Locale(""fr-FR"", ""{SundayNames.French}""), new Locale(""es-ES"", ""{SundayNames.Spanish}""),
-        new Locale(""pl-PL"", ""{fallbackSundayName}""), new Locale(""ko-KR"", ""{fallbackSundayName}""), new Locale(""cs-CZ"", ""{fallbackSundayName}"")
-    }}";
-    private static string GetCjkTestedLocales(string fallbackSundayName=FallbackSundayNameEnUS) =>  $@"new Locale[] {{
-        new Locale(""en-GB"", ""{SundayNames.English}""), new Locale(""zh-CN"", ""{SundayNames.Chinese}""), new Locale(""ja-JP"", ""{SundayNames.Japanese}""),
-        new Locale(""fr-FR"", ""{fallbackSundayName}""), new Locale(""hr-HR"", ""{fallbackSundayName}""), new Locale(""it-IT"", ""{fallbackSundayName}"")
-    }}";
-    private static string GetNocjkTestedLocales(string fallbackSundayName=FallbackSundayNameEnUS) =>  $@"new Locale[] {{
-        new Locale(""en-AU"", ""{SundayNames.English}""), new Locale(""fr-FR"", ""{SundayNames.French}""), new Locale(""sk-SK"", ""{SundayNames.Slovak}""),
-        new Locale(""ja-JP"", ""{fallbackSundayName}""), new Locale(""ko-KR"", ""{fallbackSundayName}""), new Locale(""zh-CN"", ""{fallbackSundayName}"")
-    }}";
-    private static readonly string s_fullIcuTestedLocales = $@"new Locale[] {{
-        new Locale(""en-GB"", ""{SundayNames.English}""), new Locale(""sk-SK"", ""{SundayNames.Slovak}""), new Locale(""zh-CN"", ""{SundayNames.Chinese}"")
-    }}";
-
-    public static IEnumerable<object?[]> IcuExpectedAndMissingCustomShardTestData(bool aot, RunHost host)
-        => ConfigWithAOTData(aot)
-            .Multiply(
-                new object[] { s_customIcuPath, s_customIcuTestedLocales, false },
-                new object[] { s_customIcuPath, s_customIcuTestedLocales, true })
-            .WithRunHosts(host)
-            .UnwrapItemsAsArrays();
-
-    public static IEnumerable<object?[]> IcuExpectedAndMissingShardFromRuntimePackTestData(bool aot, RunHost host)
-        => ConfigWithAOTData(aot)
-            .Multiply(
-                new object[] { "icudt.dat",
-                                $@"new Locale[] {{
-                                    new Locale(""en-GB"", ""{SundayNames.English}""), new Locale(""zh-CN"", ""{SundayNames.Chinese}""), new Locale(""sk-SK"", ""{SundayNames.Slovak}""),
-                                    new Locale(""xx-yy"", null) }}" },
-                new object[] { "icudt_EFIGS.dat", GetEfigsTestedLocales() },
-                new object[] { "icudt_CJK.dat", GetCjkTestedLocales() },
-                new object[] { "icudt_no_CJK.dat", GetNocjkTestedLocales() })
-            .WithRunHosts(host)
-            .UnwrapItemsAsArrays();
-
-    public static IEnumerable<object?[]> IcuExpectedAndMissingAutomaticShardTestData(bool aot)
-        => ConfigWithAOTData(aot)
-            .Multiply(
-                new object[] { "fr-FR", GetEfigsTestedLocales(SundayNames.French)},
-                new object[] { "ja-JP", GetCjkTestedLocales(SundayNames.Japanese) },
-                new object[] { "sk-SK", GetNocjkTestedLocales(SundayNames.Slovak) })
-            .WithRunHosts(BuildTestBase.s_hostsForOSLocaleSensitiveTests)
-            .UnwrapItemsAsArrays();
-
     public static IEnumerable<object?[]> FullIcuWithInvariantTestData(bool aot, RunHost host)
         => ConfigWithAOTData(aot)
             .Multiply(
@@ -99,115 +36,6 @@ public class IcuTests : TestMainJsTestBase
             .WithRunHosts(host)
             .UnwrapItemsAsArrays();
 
-    private static string GetProgramText(string testedLocales, bool onlyPredefinedCultures=false, string fallbackSundayName=FallbackSundayNameEnUS) => $@"
-        #nullable enable
-
-        using System;
-        using System.Globalization;
-
-        Console.WriteLine($""Current culture: '{{CultureInfo.CurrentCulture.Name}}'"");
-
-        string fallbackSundayName = ""{fallbackSundayName}"";
-        bool onlyPredefinedCultures = {(onlyPredefinedCultures ? "true" : "false")};
-        Locale[] localesToTest = {testedLocales};
-
-        bool fail = false;
-        foreach (var testLocale in localesToTest)
-        {{
-            bool expectMissing = string.IsNullOrEmpty(testLocale.SundayName);
-            bool ctorShouldFail = expectMissing && onlyPredefinedCultures;
-            CultureInfo culture;
-
-            try
-            {{
-                culture = new CultureInfo(testLocale.Code);
-                if (ctorShouldFail)
-                {{
-                    Console.WriteLine($""CultureInfo..ctor did not throw an exception for {{testLocale.Code}} as was expected."");
-                    fail = true;
-                    continue;
-                }}
-            }}
-            catch(CultureNotFoundException cnfe) when (ctorShouldFail && cnfe.Message.Contains($""{{testLocale.Code}} is an invalid culture identifier.""))
-            {{
-                Console.WriteLine($""{{testLocale.Code}}: Success. .ctor failed as expected."");
-                continue;
-            }}
-
-            string expectedSundayName = (expectMissing && !onlyPredefinedCultures)
-                                            ? fallbackSundayName
-                                            : testLocale.SundayName;
-            var actualLocalizedSundayName = culture.DateTimeFormat.GetDayName(new DateTime(2000,01,02).DayOfWeek);
-            if (expectedSundayName != actualLocalizedSundayName)
-            {{
-                Console.WriteLine($""Error: incorrect localized value for Sunday in locale {{testLocale.Code}}. Expected '{{expectedSundayName}}' but got '{{actualLocalizedSundayName}}'."");
-                fail = true;
-                continue;
-            }}
-            Console.WriteLine($""{{testLocale.Code}}: Success. Sunday name: {{actualLocalizedSundayName}}"");
-        }}
-        return fail ? -1 : 42;
-
-        public record Locale(string Code, string? SundayName);
-        ";
-
-    private void TestIcuShards(BuildArgs buildArgs, string shardName, string testedLocales, RunHost host, string id, bool onlyPredefinedCultures=false)
-    {
-        string projectName = $"shard_{Path.GetFileName(shardName)}_{buildArgs.Config}_{buildArgs.AOT}";
-        bool dotnetWasmFromRuntimePack = !(buildArgs.AOT || buildArgs.Config == "Release");
-
-        buildArgs = buildArgs with { ProjectName = projectName };
-        string extraProperties = onlyPredefinedCultures ?
-            $"<WasmIcuDataFileName>{shardName}</WasmIcuDataFileName><PredefinedCulturesOnly>true</PredefinedCulturesOnly>" :
-            $"<WasmIcuDataFileName>{shardName}</WasmIcuDataFileName>";
-        buildArgs = ExpandBuildArgs(buildArgs, extraProperties: extraProperties);
-
-        string programText = GetProgramText(testedLocales, onlyPredefinedCultures);
-        _testOutput.WriteLine($"----- Program: -----{Environment.NewLine}{programText}{Environment.NewLine}-------");
-        (_, string output) = BuildProject(buildArgs,
-                        id: id,
-                        new BuildProjectOptions(
-                            InitProject: () => File.WriteAllText(Path.Combine(_projectDir!, "Program.cs"), programText),
-                            DotnetWasmFromRuntimePack: dotnetWasmFromRuntimePack,
-                            GlobalizationMode: GlobalizationMode.PredefinedIcu,
-                            PredefinedIcudt: shardName));
-
-        string runOutput = RunAndTestWasmApp(buildArgs, buildDir: _projectDir, expectedExitCode: 42, host: host, id: id);
-    }
-
-    [Theory]
-    [MemberData(nameof(IcuExpectedAndMissingCustomShardTestData), parameters: new object[] { false, RunHost.NodeJS | RunHost.Chrome })]
-    [MemberData(nameof(IcuExpectedAndMissingCustomShardTestData), parameters: new object[] { true, RunHost.NodeJS | RunHost.Chrome })]
-    public void CustomIcuShard(BuildArgs buildArgs, string shardName, string testedLocales, bool onlyPredefinedCultures, RunHost host, string id) =>
-        TestIcuShards(buildArgs, shardName, testedLocales, host, id, onlyPredefinedCultures);
-
-    [Theory]
-    [MemberData(nameof(IcuExpectedAndMissingShardFromRuntimePackTestData), parameters: new object[] { false,RunHost.NodeJS | RunHost.Chrome })]
-    [MemberData(nameof(IcuExpectedAndMissingShardFromRuntimePackTestData), parameters: new object[] { true, RunHost.NodeJS | RunHost.Chrome })]
-    public void DefaultAvailableIcuShardsFromRuntimePack(BuildArgs buildArgs, string shardName, string testedLocales, RunHost host, string id) =>
-        TestIcuShards(buildArgs, shardName, testedLocales, host, id);
-
-    [Theory]
-    [MemberData(nameof(IcuExpectedAndMissingAutomaticShardTestData), parameters: new object[] { false })]
-    [MemberData(nameof(IcuExpectedAndMissingAutomaticShardTestData), parameters: new object[] { true })]
-    public void AutomaticShardSelectionDependingOnEnvLocale(BuildArgs buildArgs, string environmentLocale, string testedLocales, RunHost host, string id)
-    {
-        string projectName = $"automatic_shard_{environmentLocale}_{buildArgs.Config}_{buildArgs.AOT}";
-        bool dotnetWasmFromRuntimePack = !(buildArgs.AOT || buildArgs.Config == "Release");
-
-        buildArgs = buildArgs with { ProjectName = projectName };
-        buildArgs = ExpandBuildArgs(buildArgs);
-
-        string programText = GetProgramText(testedLocales);
-        _testOutput.WriteLine($"----- Program: -----{Environment.NewLine}{programText}{Environment.NewLine}-------");
-        (_, string output) = BuildProject(buildArgs,
-                        id: id,
-                        new BuildProjectOptions(
-                            InitProject: () => File.WriteAllText(Path.Combine(_projectDir!, "Program.cs"), programText),
-                            DotnetWasmFromRuntimePack: dotnetWasmFromRuntimePack));
-        string runOutput = RunAndTestWasmApp(buildArgs, buildDir: _projectDir, expectedExitCode: 42, host: host, id: id, environmentLocale: environmentLocale);
-    }
-
     [Theory]
     [MemberData(nameof(FullIcuWithInvariantTestData), parameters: new object[] { false, RunHost.NodeJS | RunHost.Chrome })]
     [MemberData(nameof(FullIcuWithInvariantTestData), parameters: new object[] { true, RunHost.NodeJS | RunHost.Chrome })]
@@ -232,19 +60,15 @@ public class IcuTests : TestMainJsTestBase
     }
 
     [Theory]
-    [MemberData(nameof(FullIcuWithICustomIcuTestData), parameters: new object[] { false, false, RunHost.NodeJS | RunHost.Chrome })] 
-    [MemberData(nameof(FullIcuWithICustomIcuTestData), parameters: new object[] { true, true, RunHost.NodeJS | RunHost.Chrome })]
-    public void FullIcuFromRuntimePackOrCustomIcu(BuildArgs buildArgs, bool fullIcu, bool useCustomFile, RunHost host, string id)
+    [MemberData(nameof(FullIcuWithICustomIcuTestData), parameters: new object[] { false, RunHost.NodeJS | RunHost.Chrome })]
+    [MemberData(nameof(FullIcuWithICustomIcuTestData), parameters: new object[] { true, RunHost.NodeJS | RunHost.Chrome })]
+    public void FullIcuFromRuntimePackWithCustomIcu(BuildArgs buildArgs, bool fullIcu, RunHost host, string id)
     {
         string projectName = $"fullIcuCustom_{fullIcu}_{buildArgs.Config}_{buildArgs.AOT}";
         bool dotnetWasmFromRuntimePack = !(buildArgs.AOT || buildArgs.Config == "Release");
 
         buildArgs = buildArgs with { ProjectName = projectName };
-        string customFile = useCustomFile ? "icudt_nonexisting.dat" : s_customIcuPath;
-        buildArgs = ExpandBuildArgs(
-            buildArgs, 
-            extraProperties: 
-                $"<WasmIcuDataFileName>{customFile}</WasmIcuDataFileName><WasmIncludeFullIcuData>{fullIcu}</WasmIncludeFullIcuData>");
+        buildArgs = ExpandBuildArgs(buildArgs, extraProperties: $"<WasmIcuDataFileName>{s_customIcuPath}</WasmIcuDataFileName><WasmIncludeFullIcuData>{fullIcu}</WasmIncludeFullIcuData>");
 
         string testedLocales = fullIcu ? s_fullIcuTestedLocales : s_customIcuTestedLocales;
         string programText = GetProgramText(testedLocales);
@@ -256,7 +80,7 @@ public class IcuTests : TestMainJsTestBase
                             DotnetWasmFromRuntimePack: dotnetWasmFromRuntimePack,
                             GlobalizationMode: fullIcu ? GlobalizationMode.FullIcu : GlobalizationMode.PredefinedIcu,
                             PredefinedIcudt: fullIcu ? "" : s_customIcuPath));
-        if (useCustomFile)
+        if (fullIcu)
             Assert.Contains("$(WasmIcuDataFileName) has no effect when $(WasmIncludeFullIcuData) is set to true.", output);
 
         string runOutput = RunAndTestWasmApp(buildArgs, buildDir: _projectDir, expectedExitCode: 42, host: host, id: id);
@@ -283,7 +107,7 @@ public class IcuTests : TestMainJsTestBase
         }
         else
         {
-            Assert.Contains($"Custom ICU file name in path $(WasmIcuDataFileName)={customIcu} has to start with 'icudt'.", output);
+            Assert.Contains($"Custom ICU file name in path $(WasmIcuDataFileName)={customIcu} must start with 'icudt'.", output);
         }
     }
 }
diff --git a/src/mono/wasm/Wasm.Build.Tests/IcuTestsBase.cs b/src/mono/wasm/Wasm.Build.Tests/IcuTestsBase.cs
new file mode 100644 (file)
index 0000000..1e0b5f1
--- /dev/null
@@ -0,0 +1,128 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.IO;
+using Xunit.Abstractions;
+using Xunit.Sdk;
+
+#nullable enable
+
+namespace Wasm.Build.Tests;
+
+public abstract class IcuTestsBase : TestMainJsTestBase
+{
+    public IcuTestsBase(ITestOutputHelper output, SharedBuildPerTestClassFixture buildContext)
+        : base(output, buildContext) { }
+
+    private const string _fallbackSundayNameEnUS = "Sunday";
+
+    protected record SundayNames
+    {
+        public static string English = "Sunday";
+        public static string French = "dimanche";
+        public static string Spanish = "domingo";
+        public static string Chinese = "星期日";
+        public static string Japanese = "日曜日";
+        public static string Slovak = "nedeľa";
+    }
+
+    // custom file contains only locales "cy-GB", "is-IS", "bs-BA", "lb-LU" and fallback locale: "en-US":
+    protected static string s_customIcuPath = Path.Combine(BuildEnvironment.TestAssetsPath, "icudt_custom.dat");
+
+    protected static readonly string s_customIcuTestedLocales = $@"new Locale[] {{
+        new Locale(""cy-GB"",  ""Dydd Sul""), new Locale(""is-IS"",  ""sunnudagur""), new Locale(""bs-BA"",  ""nedjelja""), new Locale(""lb-LU"",  ""Sonndeg""),
+        new Locale(""fr-FR"", null), new Locale(""hr-HR"", null), new Locale(""ko-KR"", null)
+    }}";
+    protected static string GetEfigsTestedLocales(string fallbackSundayName=_fallbackSundayNameEnUS) =>  $@"new Locale[] {{
+        new Locale(""en-US"", ""{SundayNames.English}""), new Locale(""fr-FR"", ""{SundayNames.French}""), new Locale(""es-ES"", ""{SundayNames.Spanish}""),
+        new Locale(""pl-PL"", ""{fallbackSundayName}""), new Locale(""ko-KR"", ""{fallbackSundayName}""), new Locale(""cs-CZ"", ""{fallbackSundayName}"")
+    }}";
+    protected static string GetCjkTestedLocales(string fallbackSundayName=_fallbackSundayNameEnUS) =>  $@"new Locale[] {{
+        new Locale(""en-GB"", ""{SundayNames.English}""), new Locale(""zh-CN"", ""{SundayNames.Chinese}""), new Locale(""ja-JP"", ""{SundayNames.Japanese}""),
+        new Locale(""fr-FR"", ""{fallbackSundayName}""), new Locale(""hr-HR"", ""{fallbackSundayName}""), new Locale(""it-IT"", ""{fallbackSundayName}"")
+    }}";
+    protected static string GetNocjkTestedLocales(string fallbackSundayName=_fallbackSundayNameEnUS) =>  $@"new Locale[] {{
+        new Locale(""en-AU"", ""{SundayNames.English}""), new Locale(""fr-FR"", ""{SundayNames.French}""), new Locale(""sk-SK"", ""{SundayNames.Slovak}""),
+        new Locale(""ja-JP"", ""{fallbackSundayName}""), new Locale(""ko-KR"", ""{fallbackSundayName}""), new Locale(""zh-CN"", ""{fallbackSundayName}"")
+    }}";
+    protected static readonly string s_fullIcuTestedLocales = $@"new Locale[] {{
+        new Locale(""en-GB"", ""{SundayNames.English}""), new Locale(""sk-SK"", ""{SundayNames.Slovak}""), new Locale(""zh-CN"", ""{SundayNames.Chinese}"")
+    }}";
+
+    protected string GetProgramText(string testedLocales, bool onlyPredefinedCultures=false, string fallbackSundayName=_fallbackSundayNameEnUS) => $@"
+        #nullable enable
+
+        using System;
+        using System.Globalization;
+
+        Console.WriteLine($""Current culture: '{{CultureInfo.CurrentCulture.Name}}'"");
+
+        string fallbackSundayName = ""{fallbackSundayName}"";
+        bool onlyPredefinedCultures = {(onlyPredefinedCultures ? "true" : "false")};
+        Locale[] localesToTest = {testedLocales};
+
+        bool fail = false;
+        foreach (var testLocale in localesToTest)
+        {{
+            bool expectMissing = string.IsNullOrEmpty(testLocale.SundayName);
+            bool ctorShouldFail = expectMissing && onlyPredefinedCultures;
+            CultureInfo culture;
+
+            try
+            {{
+                culture = new CultureInfo(testLocale.Code);
+                if (ctorShouldFail)
+                {{
+                    Console.WriteLine($""CultureInfo..ctor did not throw an exception for {{testLocale.Code}} as was expected."");
+                    fail = true;
+                    continue;
+                }}
+            }}
+            catch(CultureNotFoundException cnfe) when (ctorShouldFail && cnfe.Message.Contains($""{{testLocale.Code}} is an invalid culture identifier.""))
+            {{
+                Console.WriteLine($""{{testLocale.Code}}: Success. .ctor failed as expected."");
+                continue;
+            }}
+
+            string expectedSundayName = (expectMissing && !onlyPredefinedCultures)
+                                            ? fallbackSundayName
+                                            : testLocale.SundayName;
+            var actualLocalizedSundayName = culture.DateTimeFormat.GetDayName(new DateTime(2000,01,02).DayOfWeek);
+            if (expectedSundayName != actualLocalizedSundayName)
+            {{
+                Console.WriteLine($""Error: incorrect localized value for Sunday in locale {{testLocale.Code}}. Expected '{{expectedSundayName}}' but got '{{actualLocalizedSundayName}}'."");
+                fail = true;
+                continue;
+            }}
+            Console.WriteLine($""{{testLocale.Code}}: Success. Sunday name: {{actualLocalizedSundayName}}"");
+        }}
+        return fail ? -1 : 42;
+
+        public record Locale(string Code, string? SundayName);
+        ";
+
+    protected void TestIcuShards(BuildArgs buildArgs, string shardName, string testedLocales, RunHost host, string id, bool onlyPredefinedCultures=false)
+    {
+        string projectName = $"shard_{Path.GetFileName(shardName)}_{buildArgs.Config}_{buildArgs.AOT}";
+        bool dotnetWasmFromRuntimePack = !(buildArgs.AOT || buildArgs.Config == "Release");
+
+        buildArgs = buildArgs with { ProjectName = projectName };
+        string extraProperties = onlyPredefinedCultures ?
+            $"<WasmIcuDataFileName>{shardName}</WasmIcuDataFileName><PredefinedCulturesOnly>true</PredefinedCulturesOnly>" :
+            $"<WasmIcuDataFileName>{shardName}</WasmIcuDataFileName>";
+        buildArgs = ExpandBuildArgs(buildArgs, extraProperties: extraProperties);
+
+        string programText = GetProgramText(testedLocales, onlyPredefinedCultures);
+        _testOutput.WriteLine($"----- Program: -----{Environment.NewLine}{programText}{Environment.NewLine}-------");
+        (_, string output) = BuildProject(buildArgs,
+                        id: id,
+                        new BuildProjectOptions(
+                            InitProject: () => File.WriteAllText(Path.Combine(_projectDir!, "Program.cs"), programText),
+                            DotnetWasmFromRuntimePack: dotnetWasmFromRuntimePack,
+                            GlobalizationMode: GlobalizationMode.PredefinedIcu,
+                            PredefinedIcudt: shardName));
+
+        string runOutput = RunAndTestWasmApp(buildArgs, buildDir: _projectDir, expectedExitCode: 42, host: host, id: id);
+    }
+}
\ No newline at end of file