From 01aa3d96bb2160144f167b1065e081521d133b48 Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz <32700855+ilonatommy@users.noreply.github.com> Date: Fri, 27 Sep 2024 12:19:14 +0200 Subject: [PATCH] [wasm] Change ICU `WasmBuildTests` to use `wasmconsole` and `wasmbrowser` templates (#108271) * Clean up icu tests (automatic mode). * Block the failing test, mark problems with issue links. --- .../AssertWasmSdkBundleOptions.cs | 4 +- .../Blazor/BlazorBuildOptions.cs | 2 +- .../Blazor/BlazorWasmProjectProvider.cs | 2 +- .../Blazor/IcuShardingTests.cs | 12 +- .../wasm/Wasm.Build.Tests/BrowserRunner.cs | 10 +- .../Wasm.Build.Tests/BuildProjectOptions.cs | 2 +- .../wasm/Wasm.Build.Tests/BuildTestBase.cs | 5 +- .../Common/AssertBundleOptionsBase.cs | 2 +- .../AssertTestMainJsAppBundleOptions.cs | 4 +- .../Common/GlobalizationMode.cs | 2 +- .../wasm/Wasm.Build.Tests/IcuShardingTests.cs | 70 ++++---- .../Wasm.Build.Tests/IcuShardingTests2.cs | 36 ++-- src/mono/wasm/Wasm.Build.Tests/IcuTests.cs | 161 ++++++++++-------- .../wasm/Wasm.Build.Tests/IcuTestsBase.cs | 111 ++++++++++-- .../Wasm.Build.Tests/ProjectProviderBase.cs | 14 +- .../Templates/WasmTemplateTests.cs | 117 +++---------- .../Templates/WasmTemplateTestsBase.cs | 125 ++++++++++++++ .../TestMainJsProjectProvider.cs | 2 +- .../WasmSdkBasedProjectProvider.cs | 2 +- 19 files changed, 417 insertions(+), 266 deletions(-) create mode 100644 src/mono/wasm/Wasm.Build.Tests/Templates/WasmTemplateTestsBase.cs diff --git a/src/mono/wasm/Wasm.Build.Tests/AssertWasmSdkBundleOptions.cs b/src/mono/wasm/Wasm.Build.Tests/AssertWasmSdkBundleOptions.cs index f83eaf616958f..b3d0f1a18caa7 100644 --- a/src/mono/wasm/Wasm.Build.Tests/AssertWasmSdkBundleOptions.cs +++ b/src/mono/wasm/Wasm.Build.Tests/AssertWasmSdkBundleOptions.cs @@ -11,7 +11,7 @@ public record AssertWasmSdkBundleOptions( bool IsPublish, string TargetFramework, string BinFrameworkDir, - string? PredefinedIcudt, + string? CustomIcuFile, GlobalizationMode GlobalizationMode = GlobalizationMode.Sharded, string BootJsonFileName = "blazor.boot.json", NativeFilesType ExpectedFileType = NativeFilesType.FromRuntimePack, @@ -25,7 +25,7 @@ public record AssertWasmSdkBundleOptions( IsPublish: IsPublish, TargetFramework: TargetFramework, BinFrameworkDir: BinFrameworkDir, - PredefinedIcudt: PredefinedIcudt, + CustomIcuFile: CustomIcuFile, GlobalizationMode: GlobalizationMode, ExpectedFileType: ExpectedFileType, RuntimeType: RuntimeType, diff --git a/src/mono/wasm/Wasm.Build.Tests/Blazor/BlazorBuildOptions.cs b/src/mono/wasm/Wasm.Build.Tests/Blazor/BlazorBuildOptions.cs index 025ea7e38eef8..b3111d60e6a8b 100644 --- a/src/mono/wasm/Wasm.Build.Tests/Blazor/BlazorBuildOptions.cs +++ b/src/mono/wasm/Wasm.Build.Tests/Blazor/BlazorBuildOptions.cs @@ -18,7 +18,7 @@ public record BlazorBuildOptions bool ExpectFingerprintOnDotnetJs = false, RuntimeVariant RuntimeType = RuntimeVariant.SingleThreaded, GlobalizationMode GlobalizationMode = GlobalizationMode.Sharded, - string PredefinedIcudt = "", + string CustomIcuFile = "", bool AssertAppBundle = true, string? BinFrameworkDir = null ); diff --git a/src/mono/wasm/Wasm.Build.Tests/Blazor/BlazorWasmProjectProvider.cs b/src/mono/wasm/Wasm.Build.Tests/Blazor/BlazorWasmProjectProvider.cs index ea6cc51fc6c9f..6f0a28f311e22 100644 --- a/src/mono/wasm/Wasm.Build.Tests/Blazor/BlazorWasmProjectProvider.cs +++ b/src/mono/wasm/Wasm.Build.Tests/Blazor/BlazorWasmProjectProvider.cs @@ -20,7 +20,7 @@ public void AssertBundle(BlazorBuildOptions options) TargetFramework: options.TargetFramework, BinFrameworkDir: options.BinFrameworkDir ?? FindBinFrameworkDir(options.Config, options.IsPublish, options.TargetFramework), GlobalizationMode: options.GlobalizationMode, - PredefinedIcudt: options.PredefinedIcudt, + CustomIcuFile: options.CustomIcuFile, ExpectFingerprintOnDotnetJs: options.ExpectFingerprintOnDotnetJs, ExpectedFileType: options.ExpectedFileType, RuntimeType: options.RuntimeType, diff --git a/src/mono/wasm/Wasm.Build.Tests/Blazor/IcuShardingTests.cs b/src/mono/wasm/Wasm.Build.Tests/Blazor/IcuShardingTests.cs index aa19657015185..9a6ba4e09ae8d 100644 --- a/src/mono/wasm/Wasm.Build.Tests/Blazor/IcuShardingTests.cs +++ b/src/mono/wasm/Wasm.Build.Tests/Blazor/IcuShardingTests.cs @@ -32,8 +32,8 @@ public async Task CustomIcuFileFromRuntimePack(string config, string fileName) id, config, WarnAsError: true, - GlobalizationMode: GlobalizationMode.PredefinedIcu, - PredefinedIcudt: fileName + GlobalizationMode: GlobalizationMode.Custom, + CustomIcuFile: fileName ); AddItemsPropertiesToProject( projectFile, @@ -65,8 +65,8 @@ public void NonExistingCustomFileAssertError(string config, string fileName, boo id, config, WarnAsError: false, - GlobalizationMode: GlobalizationMode.PredefinedIcu, - PredefinedIcudt: fileName + GlobalizationMode: GlobalizationMode.Custom, + CustomIcuFile: fileName )); } catch (XunitException ex) @@ -104,8 +104,8 @@ public async Task CustomFileNotFromRuntimePackAbsolutePath(string config) id, config, WarnAsError: false, - GlobalizationMode: GlobalizationMode.PredefinedIcu, - PredefinedIcudt: IcuTestsBase.CustomIcuPath + GlobalizationMode: GlobalizationMode.Custom, + CustomIcuFile: IcuTestsBase.CustomIcuPath )); await BlazorRunForBuildWithDotnetRun(new BlazorRunOptions() { Config = config }); } diff --git a/src/mono/wasm/Wasm.Build.Tests/BrowserRunner.cs b/src/mono/wasm/Wasm.Build.Tests/BrowserRunner.cs index 7234bd908c070..3d2dda1515a71 100644 --- a/src/mono/wasm/Wasm.Build.Tests/BrowserRunner.cs +++ b/src/mono/wasm/Wasm.Build.Tests/BrowserRunner.cs @@ -104,12 +104,13 @@ public async Task SpawnBrowserAsync( string browserUrl, bool headless = true, int timeout = 10000, - int maxRetries = 3 + int maxRetries = 3, + string language = "en-US" ) { var url = new Uri(browserUrl); Playwright = await Microsoft.Playwright.Playwright.CreateAsync(); // codespaces: ignore certificate error -> Microsoft.Playwright.PlaywrightException : net::ERR_CERT_AUTHORITY_INVALID - string[] chromeArgs = new[] { $"--explicitly-allowed-ports={url.Port}", "--ignore-certificate-errors" }; + string[] chromeArgs = new[] { $"--explicitly-allowed-ports={url.Port}", "--ignore-certificate-errors", $"--lang={language}" }; _testOutput.WriteLine($"Launching chrome ('{s_chromePath.Value}') via playwright with args = {string.Join(',', chromeArgs)}"); int attempt = 0; @@ -146,14 +147,15 @@ public async Task RunAsync( ToolCommand cmd, string args, bool headless = true, + string language = "en-US", Action? onConsoleMessage = null, Action? onServerMessage = null, Action? onError = null, Func? modifyBrowserUrl = null) { var urlString = await StartServerAndGetUrlAsync(cmd, args, onServerMessage); - var browser = await SpawnBrowserAsync(urlString, headless); - var context = await browser.NewContextAsync(); + var browser = await SpawnBrowserAsync(urlString, headless, language: language); + var context = await browser.NewContextAsync(new BrowserNewContextOptions { Locale = language }); return await RunAsync(context, urlString, headless, onConsoleMessage, onError, modifyBrowserUrl); } diff --git a/src/mono/wasm/Wasm.Build.Tests/BuildProjectOptions.cs b/src/mono/wasm/Wasm.Build.Tests/BuildProjectOptions.cs index c86c4cda6e7f9..d0bd02a2289a5 100644 --- a/src/mono/wasm/Wasm.Build.Tests/BuildProjectOptions.cs +++ b/src/mono/wasm/Wasm.Build.Tests/BuildProjectOptions.cs @@ -13,7 +13,7 @@ public record BuildProjectOptions Action? InitProject = null, bool? DotnetWasmFromRuntimePack = null, GlobalizationMode GlobalizationMode = GlobalizationMode.Sharded, - string? PredefinedIcudt = null, + string? CustomIcuFile = null, bool UseCache = true, bool ExpectSuccess = true, bool AssertAppBundle = true, diff --git a/src/mono/wasm/Wasm.Build.Tests/BuildTestBase.cs b/src/mono/wasm/Wasm.Build.Tests/BuildTestBase.cs index 1f120016ef6aa..50535fee0a330 100644 --- a/src/mono/wasm/Wasm.Build.Tests/BuildTestBase.cs +++ b/src/mono/wasm/Wasm.Build.Tests/BuildTestBase.cs @@ -630,8 +630,9 @@ public static int Main() public record BuildArgs(string ProjectName, string Config, bool AOT, - string ProjectFileContents, - string? ExtraBuildArgs); + string Id, + string? ExtraBuildArgs, + string? ProjectFileContents=null); public record BuildProduct(string ProjectDir, string LogFile, bool Result, string BuildOutput); public enum NativeFilesType { FromRuntimePack, Relinked, AOT }; diff --git a/src/mono/wasm/Wasm.Build.Tests/Common/AssertBundleOptionsBase.cs b/src/mono/wasm/Wasm.Build.Tests/Common/AssertBundleOptionsBase.cs index e985ebf458938..bf5301a53c762 100644 --- a/src/mono/wasm/Wasm.Build.Tests/Common/AssertBundleOptionsBase.cs +++ b/src/mono/wasm/Wasm.Build.Tests/Common/AssertBundleOptionsBase.cs @@ -12,7 +12,7 @@ public abstract record AssertBundleOptionsBase( bool IsPublish, string TargetFramework, string BinFrameworkDir, - string? PredefinedIcudt, + string? CustomIcuFile, string BundleDirName = "wwwroot", GlobalizationMode GlobalizationMode = GlobalizationMode.Sharded, string BootJsonFileName = "blazor.boot.json", diff --git a/src/mono/wasm/Wasm.Build.Tests/Common/AssertTestMainJsAppBundleOptions.cs b/src/mono/wasm/Wasm.Build.Tests/Common/AssertTestMainJsAppBundleOptions.cs index a7b028164d3cc..3f0188b06e20f 100644 --- a/src/mono/wasm/Wasm.Build.Tests/Common/AssertTestMainJsAppBundleOptions.cs +++ b/src/mono/wasm/Wasm.Build.Tests/Common/AssertTestMainJsAppBundleOptions.cs @@ -10,7 +10,7 @@ public record AssertTestMainJsAppBundleOptions( bool IsPublish, string TargetFramework, string BinFrameworkDir, - string? PredefinedIcudt, + string? CustomIcuFile, string ProjectName, string MainJS, GlobalizationMode GlobalizationMode = GlobalizationMode.Sharded, @@ -28,7 +28,7 @@ public record AssertTestMainJsAppBundleOptions( IsPublish: IsPublish, TargetFramework: TargetFramework, BinFrameworkDir: BinFrameworkDir, - PredefinedIcudt: PredefinedIcudt, + CustomIcuFile: CustomIcuFile, GlobalizationMode: GlobalizationMode, ExpectedFileType: ExpectedFileType, RuntimeType: RuntimeType, diff --git a/src/mono/wasm/Wasm.Build.Tests/Common/GlobalizationMode.cs b/src/mono/wasm/Wasm.Build.Tests/Common/GlobalizationMode.cs index 0b2130a944d42..250dbe7dc5b6b 100644 --- a/src/mono/wasm/Wasm.Build.Tests/Common/GlobalizationMode.cs +++ b/src/mono/wasm/Wasm.Build.Tests/Common/GlobalizationMode.cs @@ -9,6 +9,6 @@ public enum GlobalizationMode Sharded, // chosen based on locale Invariant, // no icu FullIcu, // full icu data: icudt.dat is loaded - PredefinedIcu, // user set WasmIcuDataFileName/BlazorIcuDataFileName value and we are loading that file + Custom, // user set WasmIcuDataFileName/BlazorIcuDataFileName value and we are loading that file Hybrid // reduced icu, missing data is provided by platform-native functions (web api for wasm) }; diff --git a/src/mono/wasm/Wasm.Build.Tests/IcuShardingTests.cs b/src/mono/wasm/Wasm.Build.Tests/IcuShardingTests.cs index dbaeba4bb1165..c4fdf83966bb8 100644 --- a/src/mono/wasm/Wasm.Build.Tests/IcuShardingTests.cs +++ b/src/mono/wasm/Wasm.Build.Tests/IcuShardingTests.cs @@ -2,11 +2,13 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Collections.Generic; using System.IO; +using System.Linq; +using System.Threading.Tasks; using Xunit; using Xunit.Abstractions; using Xunit.Sdk; -using System.Collections.Generic; #nullable enable @@ -17,47 +19,35 @@ public class IcuShardingTests : IcuTestsBase public IcuShardingTests(ITestOutputHelper output, SharedBuildPerTestClassFixture buildContext) : base(output, buildContext) { } - public static IEnumerable IcuExpectedAndMissingCustomShardTestData(bool aot, RunHost host) - => ConfigWithAOTData(aot) - .Multiply( - new object[] { CustomIcuPath, s_customIcuTestedLocales, false }, - new object[] { CustomIcuPath, s_customIcuTestedLocales, true }) - .WithRunHosts(host) - .UnwrapItemsAsArrays(); - - public static IEnumerable 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 IcuExpectedAndMissingCustomShardTestData(string config) => + from templateType in templateTypes + from aot in boolOptions + from onlyPredefinedCultures in boolOptions + // isOnlyPredefinedCultures = true fails with wasmbrowser: https://github.com/dotnet/runtime/issues/108272 + where !(onlyPredefinedCultures && templateType == "wasmbrowser") + select new object[] { config, templateType, aot, CustomIcuPath, s_customIcuTestedLocales, onlyPredefinedCultures }; + + public static IEnumerable IcuExpectedAndMissingAutomaticShardTestData(string config) + { + var locales = new Dictionary + { + { "fr-FR", GetEfigsTestedLocales(SundayNames.French) }, + { "ja-JP", GetCjkTestedLocales(SundayNames.Japanese) }, + { "sk-SK", GetNocjkTestedLocales(SundayNames.Slovak) } + }; + // "wasmconsole": https://github.com/dotnet/runtime/issues/82593 + return from aot in boolOptions + from locale in locales + select new object[] { config, "wasmbrowser", aot, locale.Key, locale.Value }; + } [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); + [MemberData(nameof(IcuExpectedAndMissingCustomShardTestData), parameters: new object[] { "Release" })] + public async Task CustomIcuShard(string config, string templateType, bool aot, string customIcuPath, string customLocales, bool onlyPredefinedCultures) => + await TestIcuShards(config, templateType, aot, customIcuPath, customLocales, GlobalizationMode.Custom, 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); - } + [MemberData(nameof(IcuExpectedAndMissingAutomaticShardTestData), parameters: new object[] { "Release" })] + public async Task AutomaticShardSelectionDependingOnEnvLocale(string config, string templateType, bool aot, string environmentLocale, string testedLocales) => + await BuildAndRunIcuTest(config, templateType, aot, testedLocales, GlobalizationMode.Sharded, language: environmentLocale); } diff --git a/src/mono/wasm/Wasm.Build.Tests/IcuShardingTests2.cs b/src/mono/wasm/Wasm.Build.Tests/IcuShardingTests2.cs index c04d5c1114dc0..38acd3f848566 100644 --- a/src/mono/wasm/Wasm.Build.Tests/IcuShardingTests2.cs +++ b/src/mono/wasm/Wasm.Build.Tests/IcuShardingTests2.cs @@ -3,6 +3,8 @@ using System; using System.IO; +using System.Linq; +using System.Threading.Tasks; using Xunit; using Xunit.Abstractions; using Xunit.Sdk; @@ -17,23 +19,27 @@ public class IcuShardingTests2 : IcuTestsBase public IcuShardingTests2(ITestOutputHelper output, SharedBuildPerTestClassFixture buildContext) : base(output, buildContext) { } - public static IEnumerable IcuExpectedAndMissingShardFromRuntimePackTestData(bool aot, RunHost host) - => ConfigWithAOTData(aot) - .Multiply( - new object[] { "icudt.dat", - $@"new Locale[] {{ + public static IEnumerable IcuExpectedAndMissingShardFromRuntimePackTestData(string config) + { + var locales = new Dictionary + { + { "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(); - + { "icudt_EFIGS.dat", GetEfigsTestedLocales() }, + { "icudt_CJK.dat", GetCjkTestedLocales() }, + { "icudt_no_CJK.dat", GetNocjkTestedLocales() } + }; + return + // "wasmconsole": https://github.com/dotnet/runtime/issues/82593 + // from templateType in templateTypes + from aot in boolOptions + from locale in locales + select new object[] { config, "wasmbrowser", aot, locale.Key, locale.Value }; + } [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); + [MemberData(nameof(IcuExpectedAndMissingShardFromRuntimePackTestData), parameters: new object[] { "Release" })] + public async Task DefaultAvailableIcuShardsFromRuntimePack(string config, string templateType, bool aot, string shardName, string testedLocales) => + await TestIcuShards(config, templateType, aot, shardName, testedLocales, GlobalizationMode.Custom); } \ No newline at end of file diff --git a/src/mono/wasm/Wasm.Build.Tests/IcuTests.cs b/src/mono/wasm/Wasm.Build.Tests/IcuTests.cs index 0f58d24ac9f3d..6c0e2c5ae9c55 100644 --- a/src/mono/wasm/Wasm.Build.Tests/IcuTests.cs +++ b/src/mono/wasm/Wasm.Build.Tests/IcuTests.cs @@ -3,6 +3,8 @@ using System; using System.IO; +using System.Linq; +using System.Threading.Tasks; using Xunit; using Xunit.Abstractions; using Xunit.Sdk; @@ -17,97 +19,106 @@ public class IcuTests : IcuTestsBase public IcuTests(ITestOutputHelper output, SharedBuildPerTestClassFixture buildContext) : base(output, buildContext) { } - public static IEnumerable FullIcuWithInvariantTestData(bool aot, RunHost host) - => ConfigWithAOTData(aot) - .Multiply( - // in invariant mode, all locales should be missing - new object[] { true, true, "Array.Empty()" }, - new object[] { true, false, "Array.Empty()" }, - new object[] { false, false, GetEfigsTestedLocales() }, - new object[] { false, true, s_fullIcuTestedLocales}) - .WithRunHosts(host) - .UnwrapItemsAsArrays(); + public static IEnumerable FullIcuWithICustomIcuTestData(string config) => + from templateType in templateTypes + from aot in boolOptions + from fullIcu in boolOptions + select new object[] { config, templateType, aot, fullIcu }; - public static IEnumerable FullIcuWithICustomIcuTestData(bool aot, RunHost host) - => ConfigWithAOTData(aot) - .Multiply( - new object[] { true }, - new object[] { false }) - .WithRunHosts(host) - .UnwrapItemsAsArrays(); - - [Theory] - [MemberData(nameof(FullIcuWithInvariantTestData), parameters: new object[] { false, RunHost.NodeJS | RunHost.Chrome })] - [MemberData(nameof(FullIcuWithInvariantTestData), parameters: new object[] { true, RunHost.NodeJS | RunHost.Chrome })] - public void FullIcuFromRuntimePackWithInvariant(BuildArgs buildArgs, bool invariant, bool fullIcu, string testedLocales, RunHost host, string id) + public static IEnumerable FullIcuWithInvariantTestData(string config) { - string projectName = $"fullIcuInvariant_{fullIcu}_{invariant}_{buildArgs.Config}_{buildArgs.AOT}"; - bool dotnetWasmFromRuntimePack = !(buildArgs.AOT || buildArgs.Config == "Release"); - - buildArgs = buildArgs with { ProjectName = projectName }; - buildArgs = ExpandBuildArgs(buildArgs, extraProperties: $"{invariant}{fullIcu}"); - - 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, - GlobalizationMode: invariant ? GlobalizationMode.Invariant : fullIcu ? GlobalizationMode.FullIcu : GlobalizationMode.Sharded)); - - string runOutput = RunAndTestWasmApp(buildArgs, buildDir: _projectDir, expectedExitCode: 42, host: host, id: id); + var locales = new object[][] + { + // in invariant mode, all locales should be missing + new object[] { true, true, "Array.Empty()" }, + new object[] { true, false, "Array.Empty()" }, + new object[] { false, false, GetEfigsTestedLocales() }, + new object[] { false, true, s_fullIcuTestedLocales } + }; + return from templateType in templateTypes + from aot in boolOptions + from locale in locales + select new object[] { config, templateType, aot, locale[0], locale[1], locale[2] }; } - [Theory] - [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) + public static IEnumerable IncorrectIcuTestData(string config) { - string projectName = $"fullIcuCustom_{fullIcu}_{buildArgs.Config}_{buildArgs.AOT}"; - bool dotnetWasmFromRuntimePack = !(buildArgs.AOT || buildArgs.Config == "Release"); + var customFiles = new Dictionary + { + { "icudtNonExisting.dat", true }, + { "incorrectName.dat", false } + }; + return from templateType in templateTypes + from customFile in customFiles + select new object[] { config, templateType, customFile.Key, customFile.Value }; + } + - buildArgs = buildArgs with { ProjectName = projectName }; - buildArgs = ExpandBuildArgs(buildArgs, extraProperties: $"{CustomIcuPath}{fullIcu}"); + [Theory] + [MemberData(nameof(FullIcuWithInvariantTestData), parameters: new object[] { "Release" })] + public async Task FullIcuFromRuntimePackWithInvariant(string config, string templateType, bool aot, bool invariant, bool fullIcu, string testedLocales) => + await BuildAndRunIcuTest( + config, + templateType, + aot, + testedLocales, + globalizationMode: invariant ? GlobalizationMode.Invariant : fullIcu ? GlobalizationMode.FullIcu : GlobalizationMode.Sharded, + extraProperties: + // https://github.com/dotnet/runtime/issues/94133: "wasmbrowser" should use WasmIncludeFullIcuData, not BlazorWebAssemblyLoadAllGlobalizationData + templateType == "wasmconsole" ? + $"{invariant}{fullIcu}{aot}" : + $"{invariant}{fullIcu}{aot}"); + [Theory] + [MemberData(nameof(FullIcuWithICustomIcuTestData), parameters: new object[] { "Release" })] + public async Task FullIcuFromRuntimePackWithCustomIcu(string config, string templateType, bool aot, bool fullIcu) + { + bool isBrowser = templateType == "wasmbrowser"; + string customIcuProperty = isBrowser ? "BlazorIcuDataFileName" : "WasmIcuDataFileName"; + string fullIcuProperty = isBrowser ? "BlazorWebAssemblyLoadAllGlobalizationData" : "WasmIncludeFullIcuData"; + string extraProperties = $"<{customIcuProperty}>{CustomIcuPath}<{fullIcuProperty}>{fullIcu}{aot}"; + string testedLocales = fullIcu ? s_fullIcuTestedLocales : s_customIcuTestedLocales; - 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, - GlobalizationMode: fullIcu ? GlobalizationMode.FullIcu : GlobalizationMode.PredefinedIcu, - PredefinedIcudt: fullIcu ? "" : CustomIcuPath)); + GlobalizationMode globalizationMode = fullIcu ? GlobalizationMode.FullIcu : GlobalizationMode.Custom; + string customIcuFile = fullIcu ? "" : CustomIcuPath; + string output = await BuildAndRunIcuTest(config, templateType, aot, testedLocales, globalizationMode, extraProperties, icuFileName: customIcuFile); 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); + Assert.Contains($"$({customIcuProperty}) has no effect when $({fullIcuProperty}) is set to true.", output); } [Theory] - [BuildAndRun(host: RunHost.None, parameters: new object[] { "icudtNonExisting.dat", true })] - [BuildAndRun(host: RunHost.None, parameters: new object[] { "incorrectName.dat", false })] - public void NonExistingCustomFileAssertError(BuildArgs buildArgs, string customFileName, bool isFilenameCorrect, string id) - { - string projectName = $"invalidCustomIcu_{buildArgs.Config}_{buildArgs.AOT}"; - buildArgs = buildArgs with { ProjectName = projectName }; - string customIcu = Path.Combine(BuildEnvironment.TestAssetsPath, customFileName); - buildArgs = ExpandBuildArgs(buildArgs, extraProperties: $"{customIcu}"); - - (_, string output) = BuildProject(buildArgs, - id: id, - new BuildProjectOptions( - InitProject: () => File.WriteAllText(Path.Combine(_projectDir!, "Program.cs"), s_mainReturns42), - ExpectSuccess: false)); - if (isFilenameCorrect) + [MemberData(nameof(IncorrectIcuTestData), parameters: new object[] { "Release" })] + public void NonExistingCustomFileAssertError(string config, string templateType, string customIcu, bool isFilenameFormCorrect) + { + bool isBrowser = templateType == "wasmbrowser"; + string customIcuProperty = isBrowser ? "BlazorIcuDataFileName" : "WasmIcuDataFileName"; + string extraProperties = $"<{customIcuProperty}>{customIcu}"; + + (BuildArgs buildArgs, string projectFile) = CreateIcuProject( + config, templateType, aot: false, "Array.Empty()", extraProperties); + string output = BuildIcuTest( + buildArgs, + isBrowser, + GlobalizationMode.Custom, + customIcu, + expectSuccess: false, + assertAppBundle: false); + + if (isBrowser) { - Assert.Contains($"File in location $(WasmIcuDataFileName)={customIcu} cannot be found neither when used as absolute path nor a relative runtime pack path.", output); + if (isFilenameFormCorrect) + { + Assert.Contains($"Could not find $({customIcuProperty})={customIcu}, or when used as a path relative to the runtime pack", output); + } + else + { + Assert.Contains($"File name in $({customIcuProperty}) has to start with 'icudt'.", output); + } } else { - Assert.Contains($"Custom ICU file name in path $(WasmIcuDataFileName)={customIcu} must start with 'icudt'.", output); + // https://github.com/dotnet/runtime/issues/102743: console apps should also require "icudt" at the beginning, unify it + Assert.Contains($"File in location $({customIcuProperty})={customIcu} cannot be found neither when used as absolute path nor a relative runtime pack path.", output); } } } diff --git a/src/mono/wasm/Wasm.Build.Tests/IcuTestsBase.cs b/src/mono/wasm/Wasm.Build.Tests/IcuTestsBase.cs index 0c1f5b1356227..3aefcdd0762f6 100644 --- a/src/mono/wasm/Wasm.Build.Tests/IcuTestsBase.cs +++ b/src/mono/wasm/Wasm.Build.Tests/IcuTestsBase.cs @@ -3,6 +3,8 @@ using System; using System.IO; +using System.Collections.Generic; +using System.Threading.Tasks; using Xunit.Abstractions; using Xunit.Sdk; @@ -10,12 +12,14 @@ namespace Wasm.Build.Tests; -public abstract class IcuTestsBase : TestMainJsTestBase +public abstract class IcuTestsBase : WasmTemplateTestsBase { public IcuTestsBase(ITestOutputHelper output, SharedBuildPerTestClassFixture buildContext) : base(output, buildContext) { } private const string _fallbackSundayNameEnUS = "Sunday"; + protected static string[] templateTypes = { "wasmconsole", "wasmbrowser" }; + protected static bool[] boolOptions = { false, true }; protected record SundayNames { @@ -87,7 +91,7 @@ protected string GetProgramText(string testedLocales, bool onlyPredefinedCulture string expectedSundayName = (expectMissing && !onlyPredefinedCultures) ? fallbackSundayName - : testLocale.SundayName; + : testLocale.SundayName ?? fallbackSundayName; var actualLocalizedSundayName = culture.DateTimeFormat.GetDayName(new DateTime(2000,01,02).DayOfWeek); if (expectedSundayName != actualLocalizedSundayName) {{ @@ -102,28 +106,105 @@ protected string GetProgramText(string testedLocales, bool onlyPredefinedCulture public record Locale(string Code, string? SundayName); "; - protected void TestIcuShards(BuildArgs buildArgs, string shardName, string testedLocales, RunHost host, string id, bool onlyPredefinedCultures=false) + protected async Task TestIcuShards(string config, string templateType, bool aot, string shardName, string testedLocales, GlobalizationMode globalizationMode, 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 }; + bool isBrowser = templateType == "wasmbrowser"; + string icuProperty = isBrowser ? "BlazorIcuDataFileName" : "WasmIcuDataFileName"; // https://github.com/dotnet/runtime/issues/94133 // by default, we remove resource strings from an app. ICU tests are checking exception messages contents -> resource string keys are not enough - string extraProperties = $"{shardName}false"; + string extraProperties = $"<{icuProperty}>{shardName}false{aot}"; if (onlyPredefinedCultures) extraProperties = $"{extraProperties}true"; - buildArgs = ExpandBuildArgs(buildArgs, extraProperties: extraProperties); + await BuildAndRunIcuTest(config, templateType, aot, testedLocales, globalizationMode, extraProperties, onlyPredefinedCultures, icuFileName: shardName); + } + protected (BuildArgs buildArgs, string projectFile) CreateIcuProject( + string config, + string templateType, + bool aot, + string testedLocales, + string extraProperties = "", + bool onlyPredefinedCultures=false) + { + string id = $"icu_{config}_{aot}_{GetRandomId()}"; + string projectFile = CreateWasmTemplateProject(id, templateType); + string projectDirectory = Path.GetDirectoryName(projectFile)!; + string projectName = Path.GetFileNameWithoutExtension(projectFile); + var buildArgs = new BuildArgs(projectName, config, aot, id, null); + buildArgs = ExpandBuildArgs(buildArgs); + AddItemsPropertiesToProject(projectFile, extraProperties: extraProperties); + + string programPath = Path.Combine(projectDirectory, "Program.cs"); string programText = GetProgramText(testedLocales, onlyPredefinedCultures); + File.WriteAllText(programPath, programText); _testOutput.WriteLine($"----- Program: -----{Environment.NewLine}{programText}{Environment.NewLine}-------"); - (_, string output) = BuildProject(buildArgs, - id: id, + + bool isBrowser = templateType == "wasmbrowser"; + string mainPath = isBrowser ? Path.Combine("wwwroot", "main.js") : "main.mjs"; + var replacements = isBrowser ? new Dictionary { + { "runMain", "runMainAndExit" }, + { ".create()", ".withConsoleForwarding().withElementOnExit().withExitCodeLogging().create()" } + } : new Dictionary { + { ".create()", ".withConsoleForwarding().withElementOnExit().withExitCodeLogging().create()" } + }; + UpdateProjectFile(mainPath, replacements); + RemoveContentsFromProjectFile(mainPath, ".create();", "await runMainAndExit();"); + return (buildArgs, projectFile); + } + + protected string BuildIcuTest( + BuildArgs buildArgs, + bool isBrowser, + GlobalizationMode globalizationMode, + string icuFileName = "", + bool expectSuccess = true, + bool assertAppBundle = true) + { + bool dotnetWasmFromRuntimePack = !(buildArgs.AOT || buildArgs.Config == "Release"); + (string _, string buildOutput) = BuildTemplateProject(buildArgs, + id: buildArgs.Id, new BuildProjectOptions( - InitProject: () => File.WriteAllText(Path.Combine(_projectDir!, "Program.cs"), programText), DotnetWasmFromRuntimePack: dotnetWasmFromRuntimePack, - GlobalizationMode: GlobalizationMode.PredefinedIcu, - PredefinedIcudt: shardName)); + CreateProject: false, + HasV8Script: false, + MainJS: isBrowser ? "main.js" : "main.mjs", + Publish: true, + TargetFramework: BuildTestBase.DefaultTargetFramework, + UseCache: false, + IsBrowserProject: isBrowser, + GlobalizationMode: globalizationMode, + CustomIcuFile: icuFileName, + ExpectSuccess: expectSuccess, + AssertAppBundle: assertAppBundle + )); + return buildOutput; + } - string runOutput = RunAndTestWasmApp(buildArgs, buildDir: _projectDir, expectedExitCode: 42, host: host, id: id); + protected async Task BuildAndRunIcuTest( + string config, + string templateType, + bool aot, + string testedLocales, + GlobalizationMode globalizationMode, + string extraProperties = "", + bool onlyPredefinedCultures=false, + string language = "en-US", + string icuFileName = "") + { + try + { + bool isBrowser = templateType == "wasmbrowser"; + (BuildArgs buildArgs, string projectFile) = CreateIcuProject( + config, templateType, aot, testedLocales, extraProperties, onlyPredefinedCultures); + string buildOutput = BuildIcuTest(buildArgs, isBrowser, globalizationMode, icuFileName); + string runOutput = isBrowser ? + await RunBrowser(buildArgs.Config, projectFile, language) : + RunConsole(buildArgs, language: language); + return $"{buildOutput}\n{runOutput}"; + } + catch(Exception ex) + { + Console.WriteLine($"Exception: {ex}; _testOutput={_testOutput}"); + throw; + } } } diff --git a/src/mono/wasm/Wasm.Build.Tests/ProjectProviderBase.cs b/src/mono/wasm/Wasm.Build.Tests/ProjectProviderBase.cs index 69b6d07ff2d6c..6f440be448144 100644 --- a/src/mono/wasm/Wasm.Build.Tests/ProjectProviderBase.cs +++ b/src/mono/wasm/Wasm.Build.Tests/ProjectProviderBase.cs @@ -364,12 +364,12 @@ public void AssertIcuAssets(AssertBundleOptionsBase assertOptions, BootJsonData expected.Add("icudt_hybrid.dat"); expected.Add("segmentation-rules.json"); break; - case GlobalizationMode.PredefinedIcu: - if (string.IsNullOrEmpty(assertOptions.PredefinedIcudt)) - throw new ArgumentException("WasmBuildTest is invalid, value for predefinedIcudt is required when GlobalizationMode=PredefinedIcu."); + case GlobalizationMode.Custom: + if (string.IsNullOrEmpty(assertOptions.CustomIcuFile)) + throw new ArgumentException("WasmBuildTest is invalid, value for Custom globalization mode is required when GlobalizationMode=Custom."); // predefined ICU name can be identical with the icu files from runtime pack - expected.Add(Path.GetFileName(assertOptions.PredefinedIcudt)); + expected.Add(Path.GetFileName(assertOptions.CustomIcuFile)); break; case GlobalizationMode.Sharded: // icu shard chosen based on the locale @@ -401,12 +401,12 @@ public void AssertIcuAssets(AssertBundleOptionsBase assertOptions, BootJsonData } AssertFileNames(expected, actual); - if (assertOptions.GlobalizationMode is GlobalizationMode.PredefinedIcu) + if (assertOptions.GlobalizationMode is GlobalizationMode.Custom) { - string srcPath = assertOptions.PredefinedIcudt!; + string srcPath = assertOptions.CustomIcuFile!; string runtimePackDir = BuildTestBase.s_buildEnv.GetRuntimeNativeDir(assertOptions.TargetFramework, assertOptions.RuntimeType); if (!Path.IsPathRooted(srcPath)) - srcPath = Path.Combine(runtimePackDir, assertOptions.PredefinedIcudt!); + srcPath = Path.Combine(runtimePackDir, assertOptions.CustomIcuFile!); TestUtils.AssertSameFile(srcPath, actual.Single()); } } diff --git a/src/mono/wasm/Wasm.Build.Tests/Templates/WasmTemplateTests.cs b/src/mono/wasm/Wasm.Build.Tests/Templates/WasmTemplateTests.cs index b32b95debd879..319130a3ec08f 100644 --- a/src/mono/wasm/Wasm.Build.Tests/Templates/WasmTemplateTests.cs +++ b/src/mono/wasm/Wasm.Build.Tests/Templates/WasmTemplateTests.cs @@ -15,98 +15,33 @@ namespace Wasm.Build.Tests { - public class WasmTemplateTests : BlazorWasmTestBase + public class WasmTemplateTests : WasmTemplateTestsBase { public WasmTemplateTests(ITestOutputHelper output, SharedBuildPerTestClassFixture buildContext) : base(output, buildContext) { } - - private string StringReplaceWithAssert(string oldContent, string oldValue, string newValue) - { - string newContent = oldContent.Replace(oldValue, newValue); - if (oldValue != newValue && oldContent == newContent) - throw new XunitException($"Replacing '{oldValue}' with '{newValue}' did not change the content '{oldContent}'"); - - return newContent; - } - - private void UpdateBrowserProgramCs() - { - var path = Path.Combine(_projectDir!, "Program.cs"); - string text = File.ReadAllText(path); - text = StringReplaceWithAssert(text, "while(true)", $"int i = 0;{Environment.NewLine}while(i++ < 10)"); - text = StringReplaceWithAssert(text, "partial class StopwatchSample", $"return 42;{Environment.NewLine}partial class StopwatchSample"); - File.WriteAllText(path, text); - } - - private void UpdateConsoleProgramCs() - { - string programText = """ + + private static string s_consoleProgramUpdateText = """ Console.WriteLine("Hello, Console!"); for (int i = 0; i < args.Length; i ++) Console.WriteLine ($"args[{i}] = {args[i]}"); """; - var path = Path.Combine(_projectDir!, "Program.cs"); - string text = File.ReadAllText(path); - text = StringReplaceWithAssert(text, @"Console.WriteLine(""Hello, Console!"");", programText); - text = StringReplaceWithAssert(text, "return 0;", "return 42;"); - File.WriteAllText(path, text); - } - - private void UpdateBrowserMainJs(string targetFramework, string runtimeAssetsRelativePath = DefaultRuntimeAssetsRelativePath) + private Dictionary consoleProgramReplacements = new Dictionary { - base.UpdateBrowserMainJs( - (mainJsContent) => - { - // .withExitOnUnhandledError() is available only only >net7.0 - mainJsContent = StringReplaceWithAssert( - mainJsContent, - ".create()", - (targetFramework == "net8.0" || targetFramework == "net9.0") - ? ".withConsoleForwarding().withElementOnExit().withExitCodeLogging().withExitOnUnhandledError().create()" - : ".withConsoleForwarding().withElementOnExit().withExitCodeLogging().create()" - ); - - // dotnet.run() is already used in <= net8.0 - if (targetFramework != "net8.0") - mainJsContent = StringReplaceWithAssert(mainJsContent, "runMain()", "dotnet.run()"); - - mainJsContent = StringReplaceWithAssert(mainJsContent, "from './_framework/dotnet.js'", $"from '{runtimeAssetsRelativePath}dotnet.js'"); - - return mainJsContent; - }, - targetFramework, - runtimeAssetsRelativePath - ); - } - - private void UpdateConsoleMainJs() + { "Console.WriteLine(\"Hello, Console!\");", s_consoleProgramUpdateText }, + { "return 0;", "return 42;" } + }; + private Dictionary browserProgramReplacements = new Dictionary { - string mainJsPath = Path.Combine(_projectDir!, "main.mjs"); - string mainJsContent = File.ReadAllText(mainJsPath); - - mainJsContent = StringReplaceWithAssert(mainJsContent, ".create()", ".withConsoleForwarding().create()"); - - File.WriteAllText(mainJsPath, mainJsContent); - } - - private void UpdateMainJsEnvironmentVariables(params (string key, string value)[] variables) + { "while(true)", $"int i = 0;{Environment.NewLine}while(i++ < 10)" }, + { "partial class StopwatchSample", $"return 42;{Environment.NewLine}partial class StopwatchSample" } + }; + private Dictionary consoleMainJSReplacements = new Dictionary { - string mainJsPath = Path.Combine(_projectDir!, "main.mjs"); - string mainJsContent = File.ReadAllText(mainJsPath); - - StringBuilder js = new(); - foreach (var variable in variables) - { - js.Append($".withEnvironmentVariable(\"{variable.key}\", \"{variable.value}\")"); - } - - mainJsContent = StringReplaceWithAssert(mainJsContent, ".create()", js.ToString() + ".create()"); - - File.WriteAllText(mainJsPath, mainJsContent); - } + { ".create()", ".withConsoleForwarding().create()" } + }; [Theory, TestCategory("no-fingerprinting")] [InlineData("Debug")] @@ -117,7 +52,7 @@ public void BrowserBuildThenPublish(string config) string projectFile = CreateWasmTemplateProject(id, "wasmbrowser"); string projectName = Path.GetFileNameWithoutExtension(projectFile); - UpdateBrowserProgramCs(); + UpdateProjectFile("Program.cs", browserProgramReplacements); UpdateBrowserMainJs(DefaultTargetFramework); var buildArgs = new BuildArgs(projectName, config, false, id, null); @@ -176,7 +111,7 @@ public void ConsoleBuildThenPublish(string config) string projectFile = CreateWasmTemplateProject(id, "wasmconsole"); string projectName = Path.GetFileNameWithoutExtension(projectFile); - UpdateConsoleMainJs(); + UpdateProjectFile("main.mjs", consoleMainJSReplacements); var buildArgs = new BuildArgs(projectName, config, false, id, null); buildArgs = ExpandBuildArgs(buildArgs); @@ -242,8 +177,8 @@ private void ConsoleBuildAndRun(string config, bool relinking, string extraNewAr string projectFile = CreateWasmTemplateProject(id, "wasmconsole", extraNewArgs, addFrameworkArg: addFrameworkArg); string projectName = Path.GetFileNameWithoutExtension(projectFile); - UpdateConsoleProgramCs(); - UpdateConsoleMainJs(); + UpdateProjectFile("Program.cs", consoleProgramReplacements); + UpdateProjectFile("main.mjs", consoleMainJSReplacements); if (relinking) AddItemsPropertiesToProject(projectFile, "true"); @@ -307,7 +242,7 @@ private async Task BrowserRunTwiceWithAndThenWithoutBuildAsync(string config, st string id = $"browser_{config}_{GetRandomId()}"; string projectFile = CreateWasmTemplateProject(id, "wasmbrowser"); - UpdateBrowserProgramCs(); + UpdateProjectFile("Program.cs", browserProgramReplacements); UpdateBrowserMainJs(DefaultTargetFramework); if (!string.IsNullOrEmpty(extraProperties)) @@ -341,8 +276,8 @@ private Task ConsoleRunWithAndThenWithoutBuildAsync(string config, string extraP string id = $"console_{config}_{GetRandomId()}"; string projectFile = CreateWasmTemplateProject(id, "wasmconsole"); - UpdateConsoleProgramCs(); - UpdateConsoleMainJs(); + UpdateProjectFile("Program.cs", consoleProgramReplacements); + UpdateProjectFile("main.mjs", consoleMainJSReplacements); if (!string.IsNullOrEmpty(extraProperties)) AddItemsPropertiesToProject(projectFile, extraProperties: extraProperties); @@ -399,8 +334,8 @@ public void ConsolePublishAndRun(string config, bool aot, bool relinking) string projectFile = CreateWasmTemplateProject(id, "wasmconsole"); string projectName = Path.GetFileNameWithoutExtension(projectFile); - UpdateConsoleProgramCs(); - UpdateConsoleMainJs(); + UpdateProjectFile("Program.cs", consoleProgramReplacements); + UpdateProjectFile("main.mjs", consoleMainJSReplacements); if (aot) { @@ -470,7 +405,7 @@ public async Task BrowserBuildAndRun(string extraNewArgs, string targetFramework CreateWasmTemplateProject(id, "wasmbrowser", extraNewArgs, addFrameworkArg: extraNewArgs.Length == 0); if (targetFramework != "net8.0") - UpdateBrowserProgramCs(); + UpdateProjectFile("Program.cs", browserProgramReplacements); UpdateBrowserMainJs(targetFramework, runtimeAssetsRelativePath); @@ -554,8 +489,8 @@ public void Test_WasmStripILAfterAOT(string stripILAfterAOT, bool expectILStripp string projectDirectory = Path.GetDirectoryName(projectFile)!; bool aot = true; - UpdateConsoleProgramCs(); - UpdateConsoleMainJs(); + UpdateProjectFile("Program.cs", consoleProgramReplacements); + UpdateProjectFile("main.mjs", consoleMainJSReplacements); string extraProperties = "true"; if (!string.IsNullOrEmpty(stripILAfterAOT)) diff --git a/src/mono/wasm/Wasm.Build.Tests/Templates/WasmTemplateTestsBase.cs b/src/mono/wasm/Wasm.Build.Tests/Templates/WasmTemplateTestsBase.cs new file mode 100644 index 0000000000000..0354ae1951dab --- /dev/null +++ b/src/mono/wasm/Wasm.Build.Tests/Templates/WasmTemplateTestsBase.cs @@ -0,0 +1,125 @@ +// 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.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; +using Xunit.Sdk; + +#nullable enable + +namespace Wasm.Build.Tests +{ + public class WasmTemplateTestsBase : BlazorWasmTestBase + { + public WasmTemplateTestsBase(ITestOutputHelper output, SharedBuildPerTestClassFixture buildContext) + : base(output, buildContext) + { + } + + private string StringReplaceWithAssert(string oldContent, string oldValue, string newValue) + { + string newContent = oldContent.Replace(oldValue, newValue); + if (oldValue != newValue && oldContent == newContent) + throw new XunitException($"Replacing '{oldValue}' with '{newValue}' did not change the content '{oldContent}'"); + + return newContent; + } + + protected void UpdateProjectFile(string pathRelativeToProjectDir, Dictionary replacements) + { + var path = Path.Combine(_projectDir!, pathRelativeToProjectDir); + string text = File.ReadAllText(path); + foreach (var replacement in replacements) + { + text = StringReplaceWithAssert(text, replacement.Key, replacement.Value); + } + File.WriteAllText(path, text); + } + + protected void RemoveContentsFromProjectFile(string pathRelativeToProjectDir, string afterMarker, string beforeMarker) + { + var path = Path.Combine(_projectDir!, pathRelativeToProjectDir); + string text = File.ReadAllText(path); + int start = text.IndexOf(afterMarker); + int end = text.IndexOf(beforeMarker, start); + if (start == -1 || end == -1) + throw new XunitException($"Start or end marker not found in '{path}'"); + start += afterMarker.Length; + text = text.Remove(start, end - start); + // separate the markers with a new line + text = text.Insert(start, "\n"); + File.WriteAllText(path, text); + } + + protected void UpdateBrowserMainJs(string targetFramework, string runtimeAssetsRelativePath = DefaultRuntimeAssetsRelativePath) + { + base.UpdateBrowserMainJs( + (mainJsContent) => + { + // .withExitOnUnhandledError() is available only only >net7.0 + mainJsContent = StringReplaceWithAssert( + mainJsContent, + ".create()", + (targetFramework == "net8.0" || targetFramework == "net9.0") + ? ".withConsoleForwarding().withElementOnExit().withExitCodeLogging().withExitOnUnhandledError().create()" + : ".withConsoleForwarding().withElementOnExit().withExitCodeLogging().create()" + ); + + // dotnet.run() is already used in <= net8.0 + if (targetFramework != "net8.0") + mainJsContent = StringReplaceWithAssert(mainJsContent, "runMain()", "dotnet.run()"); + + mainJsContent = StringReplaceWithAssert(mainJsContent, "from './_framework/dotnet.js'", $"from '{runtimeAssetsRelativePath}dotnet.js'"); + + return mainJsContent; + }, + targetFramework, + runtimeAssetsRelativePath + ); + } + + protected void UpdateMainJsEnvironmentVariables(params (string key, string value)[] variables) + { + string mainJsPath = Path.Combine(_projectDir!, "main.mjs"); + string mainJsContent = File.ReadAllText(mainJsPath); + + StringBuilder js = new(); + foreach (var variable in variables) + { + js.Append($".withEnvironmentVariable(\"{variable.key}\", \"{variable.value}\")"); + } + + mainJsContent = StringReplaceWithAssert(mainJsContent, ".create()", js.ToString() + ".create()"); + + File.WriteAllText(mainJsPath, mainJsContent); + } + + protected string RunConsole(BuildArgs buildArgs, int expectedExitCode = 42, string language = "en-US") + { + CommandResult res = new RunCommand(s_buildEnv, _testOutput) + .WithWorkingDirectory(_projectDir!) + .WithEnvironmentVariable("LANG", language) + .ExecuteWithCapturedOutput($"run --no-silent --no-build -c {buildArgs.Config}") + .EnsureExitCode(expectedExitCode); + return res.Output; + } + + protected async Task RunBrowser(string config, string projectFile, string language = "en-US") + { + using var runCommand = new RunCommand(s_buildEnv, _testOutput) + .WithWorkingDirectory(_projectDir!); + + await using var runner = new BrowserRunner(_testOutput); + var page = await runner.RunAsync(runCommand, $"run --no-silent -c {config} --no-build --project \"{projectFile}\" --forward-console", language: language); + await runner.WaitForExitMessageAsync(TimeSpan.FromMinutes(2)); + Assert.Contains("WASM EXIT 42", string.Join(Environment.NewLine, runner.OutputLines)); + return string.Join("\n", runner.OutputLines); + } + } +} diff --git a/src/mono/wasm/Wasm.Build.Tests/TestMainJsProjectProvider.cs b/src/mono/wasm/Wasm.Build.Tests/TestMainJsProjectProvider.cs index 4ca29b0f3298f..39b1effac5867 100644 --- a/src/mono/wasm/Wasm.Build.Tests/TestMainJsProjectProvider.cs +++ b/src/mono/wasm/Wasm.Build.Tests/TestMainJsProjectProvider.cs @@ -105,7 +105,7 @@ public void AssertBundle(BuildArgs buildArgs, BuildProjectOptions buildProjectOp MainJS: buildProjectOptions.MainJS ?? "test-main.js", GlobalizationMode: buildProjectOptions.GlobalizationMode, HasV8Script: buildProjectOptions.HasV8Script, - PredefinedIcudt: buildProjectOptions.PredefinedIcudt ?? string.Empty, + CustomIcuFile: buildProjectOptions.CustomIcuFile ?? string.Empty, IsBrowserProject: buildProjectOptions.IsBrowserProject, ExpectedFileType: expectedFileType, ExpectSymbolsFile: !buildArgs.AOT); diff --git a/src/mono/wasm/Wasm.Build.Tests/WasmSdkBasedProjectProvider.cs b/src/mono/wasm/Wasm.Build.Tests/WasmSdkBasedProjectProvider.cs index bd35644b0e8d0..16108e5af24ec 100644 --- a/src/mono/wasm/Wasm.Build.Tests/WasmSdkBasedProjectProvider.cs +++ b/src/mono/wasm/Wasm.Build.Tests/WasmSdkBasedProjectProvider.cs @@ -73,7 +73,7 @@ public void AssertBundle(BuildArgs buildArgs, BuildProjectOptions buildProjectOp IsPublish: buildProjectOptions.Publish, TargetFramework: buildProjectOptions.TargetFramework, BinFrameworkDir: buildProjectOptions.BinFrameworkDir ?? FindBinFrameworkDir(buildArgs.Config, buildProjectOptions.Publish, buildProjectOptions.TargetFramework), - PredefinedIcudt: buildProjectOptions.PredefinedIcudt, + CustomIcuFile: buildProjectOptions.CustomIcuFile, GlobalizationMode: buildProjectOptions.GlobalizationMode, AssertSymbolsFile: false, ExpectedFileType: buildProjectOptions.Publish && buildArgs.Config == "Release" ? NativeFilesType.Relinked : NativeFilesType.FromRuntimePack