From 12396a392cb3ea546f070b24687dc8b50bc28547 Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Tue, 25 Jul 2023 11:32:39 -0400 Subject: [PATCH] [wasm] WBT fixes, and rationalization to allow improved testing (#89360) * Move DotNetFileName.cs, and TestUtils.cs to Common * Move some types from BuildTestBase.cs to separate files - FileStat - BuildPaths - BlazorBuildOptions - GlobalizationMode * Move AssertTestMainJsAppBundleOptions.cs to Common * Instead of globalizationMode=null use GlobalizationMode.Default * Merge behaviors for the various project types - Move parsing bootjson, checking icu assets, symbols to ProviderBase, so it can be used by all the project types. These come from WasmAppBuilder which is shared by all the projects. - Instead of multiple separate ways to build the project, use one `BuildTestBase.BuildWithoutAssert` method that uses `DotNetCommand` to build. And all the project types can use this. - This allows having any build customizations or fixes to be in once place, and the outputs to be consistent. - Instead of having `UseWebcil` in various option types, use it directly as needed, because this setting is *not* changed per test, rather it is fixed per run. - Rationalize figuring out bin framework directories Known limitations: - Wasm template tests use a TestMainJs provider to assert the bundle because the templates are not yet based on wasm sdk. - Blazor has a bug due to which all the icu assets get deployed irrespective of settings, so asserting that is disabled. - Also, blazor does not yet support symbols file. * Update tests to track api changes * CI: don't trigger jobs when WasmBuild.sln changes * Address feedback from @ilonatommy * Address feedback from @ilonatommy - simplify icu assert code --- eng/pipelines/common/evaluate-default-paths.yml | 1 + .../Microsoft.NET.Sdk.WebAssembly.Browser.targets | 2 - src/mono/wasi/Wasi.Build.Tests/BuildTestBase.cs | 2 - .../AssertTestMainJsAppBundleOptions.cs | 20 -- .../Wasm.Build.Tests/AssertWasmSdkBundleOptions.cs | 37 +++ .../Wasm.Build.Tests/Blazor/BlazorBuildOptions.cs | 19 ++ .../Blazor/BlazorWasmProjectProvider.cs | 57 ++--- .../Wasm.Build.Tests/Blazor/BlazorWasmTestBase.cs | 75 +++--- .../Wasm.Build.Tests/Blazor/BuildPublishTests.cs | 15 +- src/mono/wasm/Wasm.Build.Tests/Blazor/MiscTests.cs | 35 +-- .../wasm/Wasm.Build.Tests/BuildProjectOptions.cs | 4 +- src/mono/wasm/Wasm.Build.Tests/BuildTestBase.cs | 84 +++---- .../Common/AssertBundleOptionsBase.cs | 29 +++ .../Common/AssertTestMainJsAppBundleOptions.cs | 41 ++++ .../Wasm.Build.Tests/Common/BuildEnvironment.cs | 1 - .../wasm/Wasm.Build.Tests/Common/BuildPaths.cs | 7 + .../{ => Common}/DotNetFileName.cs | 0 src/mono/wasm/Wasm.Build.Tests/Common/FileStat.cs | 9 + .../Wasm.Build.Tests/Common/GlobalizationMode.cs | 14 ++ .../Wasm.Build.Tests/{ => Common}/TestUtils.cs | 0 .../Wasm.Build.Tests/HostRunner/IHostRunner.cs | 2 +- src/mono/wasm/Wasm.Build.Tests/IcuShardingTests.cs | 2 +- .../InvariantGlobalizationTests.cs | 2 +- .../NativeRebuildTests/NativeRebuildTestsBase.cs | 9 +- .../wasm/Wasm.Build.Tests/ProjectProviderBase.cs | 273 +++++++++++++++++---- src/mono/wasm/Wasm.Build.Tests/RebuildTests.cs | 6 +- .../Templates/WasmTemplateTests.cs | 21 -- .../Wasm.Build.Tests/TestMainJsProjectProvider.cs | 179 +++++--------- .../wasm/Wasm.Build.Tests/TestMainJsTestBase.cs | 70 +----- .../WasmSdkBasedProjectProvider.cs | 136 +++------- .../wasm/Wasm.Build.Tests/WasmTemplateTestBase.cs | 78 ++---- src/mono/wasm/sln/WasmBuild.sln | 6 + 32 files changed, 643 insertions(+), 593 deletions(-) delete mode 100644 src/mono/wasm/Wasm.Build.Tests/AssertTestMainJsAppBundleOptions.cs create mode 100644 src/mono/wasm/Wasm.Build.Tests/AssertWasmSdkBundleOptions.cs create mode 100644 src/mono/wasm/Wasm.Build.Tests/Blazor/BlazorBuildOptions.cs create mode 100644 src/mono/wasm/Wasm.Build.Tests/Common/AssertBundleOptionsBase.cs create mode 100644 src/mono/wasm/Wasm.Build.Tests/Common/AssertTestMainJsAppBundleOptions.cs create mode 100644 src/mono/wasm/Wasm.Build.Tests/Common/BuildPaths.cs rename src/mono/wasm/Wasm.Build.Tests/{ => Common}/DotNetFileName.cs (100%) create mode 100644 src/mono/wasm/Wasm.Build.Tests/Common/FileStat.cs create mode 100644 src/mono/wasm/Wasm.Build.Tests/Common/GlobalizationMode.cs rename src/mono/wasm/Wasm.Build.Tests/{ => Common}/TestUtils.cs (100%) diff --git a/eng/pipelines/common/evaluate-default-paths.yml b/eng/pipelines/common/evaluate-default-paths.yml index adf4dc8..20d7bedb 100644 --- a/eng/pipelines/common/evaluate-default-paths.yml +++ b/eng/pipelines/common/evaluate-default-paths.yml @@ -56,6 +56,7 @@ parameters: PATENTS.TXT THIRD-PARTY-NOTICES.TXT src/workloads/* + src/mono/wasm/sln/* ] jobs: diff --git a/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.targets b/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.targets index 13d920f..eb16d02 100644 --- a/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.targets +++ b/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.targets @@ -341,8 +341,6 @@ Copyright (c) .NET Foundation. All rights reserved. - - AssertBundle(new AssertWasmSdkBundleOptions( + Config: options.Config, + IsPublish: options.IsPublish, + TargetFramework: options.TargetFramework, + BinFrameworkDir: FindBinFrameworkDir(options.Config, options.IsPublish, options.TargetFramework), + GlobalizationMode: GlobalizationMode.Default, + PredefinedIcudt: null, + ExpectFingerprintOnDotnetJs: options.ExpectFingerprintOnDotnetJs, + ExpectedFileType: options.ExpectedFileType, + RuntimeType: options.RuntimeType, + AssertIcuAssets: false, // FIXME: this is broken right now + AssertSymbolsFile: false // FIXME: not supported yet + )); } diff --git a/src/mono/wasm/Wasm.Build.Tests/Blazor/BlazorWasmTestBase.cs b/src/mono/wasm/Wasm.Build.Tests/Blazor/BlazorWasmTestBase.cs index 48ecb24..8918a74 100644 --- a/src/mono/wasm/Wasm.Build.Tests/Blazor/BlazorWasmTestBase.cs +++ b/src/mono/wasm/Wasm.Build.Tests/Blazor/BlazorWasmTestBase.cs @@ -16,11 +16,12 @@ namespace Wasm.Build.Tests; public abstract class BlazorWasmTestBase : WasmTemplateTestBase { - protected BlazorWasmProjectProvider _provider; + protected readonly BlazorWasmProjectProvider _provider; protected BlazorWasmTestBase(ITestOutputHelper output, SharedBuildPerTestClassFixture buildContext) : base(output, buildContext, new BlazorWasmProjectProvider(output)) { _provider = GetProvider(); + _provider.BundleDirName = "wwwroot"; } public void InitBlazorWasmProjectDir(string id, string targetFramework = DefaultTargetFrameworkForBlazor) @@ -48,10 +49,7 @@ public abstract class BlazorWasmTestBase : WasmTemplateTestBase .ExecuteWithCapturedOutput("new blazorwasm") .EnsureSuccessful(); - string projectFile = Path.Combine(_projectDir!, $"{id}.csproj"); - if (!UseWebcil) - AddItemsPropertiesToProject(projectFile, "false"); - return projectFile; + return Path.Combine(_projectDir!, $"{id}.csproj"); } protected (CommandResult, string) BlazorBuild(BlazorBuildOptions options, params string[] extraArgs) @@ -59,61 +57,66 @@ public abstract class BlazorWasmTestBase : WasmTemplateTestBase if (options.WarnAsError) extraArgs = extraArgs.Append("/warnaserror").ToArray(); - var res = BlazorBuildInternal(options.Id, options.Config, publish: false, setWasmDevel: false, extraArgs); - _provider.AssertBlazorBundle(options, isPublish: false); + (CommandResult res, string logPath) = BlazorBuildInternal(options.Id, options.Config, publish: false, setWasmDevel: false, extraArgs); + + AssertBundle(res.Output, options with { IsPublish = false }); - return res; + return (res, logPath); } protected (CommandResult, string) BlazorPublish(BlazorBuildOptions options, params string[] extraArgs) { - var res = BlazorBuildInternal(options.Id, options.Config, publish: true, setWasmDevel: false, extraArgs); - _provider.AssertBlazorBundle(options, isPublish: true); + if (options.WarnAsError) + extraArgs = extraArgs.Append("/warnaserror").ToArray(); + + (CommandResult res, string logPath) = BlazorBuildInternal(options.Id, options.Config, publish: true, setWasmDevel: false, extraArgs); + AssertBundle(res.Output, options with { IsPublish = true }); if (options.ExpectedFileType == NativeFilesType.AOT) { // check for this too, so we know the format is correct for the negative // test for jsinterop.webassembly.dll - Assert.Contains("Microsoft.JSInterop.dll -> Microsoft.JSInterop.dll.bc", res.Item1.Output); + Assert.Contains("Microsoft.JSInterop.dll -> Microsoft.JSInterop.dll.bc", res.Output); // make sure this assembly gets skipped - Assert.DoesNotContain("Microsoft.JSInterop.WebAssembly.dll -> Microsoft.JSInterop.WebAssembly.dll.bc", res.Item1.Output); + Assert.DoesNotContain("Microsoft.JSInterop.WebAssembly.dll -> Microsoft.JSInterop.WebAssembly.dll.bc", res.Output); } - string objBuildDir = Path.Combine(_projectDir!, "obj", options.Config, options.TargetFramework, "wasm", "for-build"); + string objBuildDir = Path.Combine(_projectDir!, "obj", options.Config, options.TargetFramework!, "wasm", "for-build"); // Check that we linked only for publish if (options.ExpectRelinkDirWhenPublishing) Assert.True(Directory.Exists(objBuildDir), $"Could not find expected {objBuildDir}, which gets created when relinking during Build. This is likely a test authoring error"); else Assert.False(Directory.Exists(objBuildDir), $"Found unexpected {objBuildDir}, which gets created when relinking during Build"); - return res; + return (res, logPath); } - protected (CommandResult, string) BlazorBuildInternal(string id, string config, bool publish = false, bool setWasmDevel = true, params string[] extraArgs) + protected (CommandResult res, string logPath) BlazorBuildInternal( + string id, + string config, + bool publish = false, + bool setWasmDevel = true, + params string[] extraArgs) + => BuildProjectWithoutAssert( + id, + config, + new BuildProjectOptions(CreateProject: false, UseCache: false, Publish: publish), + extraArgs.Concat(new[] + { + "-p:BlazorEnableCompression=false", + setWasmDevel ? "-p:_WasmDevel=true" : string.Empty + }).ToArray()); + + public void AssertBundle(string buildOutput, BlazorBuildOptions blazorBuildOptions) { - string label = publish ? "publish" : "build"; - _testOutput.WriteLine($"{Environment.NewLine}** {label} **{Environment.NewLine}"); - - string logPath = Path.Combine(s_buildEnv.LogRootPath, id, $"{id}-{label}.binlog"); - string[] combinedArgs = new[] + if (IsUsingWorkloads) { - label, // same as the command name - $"-bl:{logPath}", - $"-p:Configuration={config}", - !UseWebcil ? "-p:WasmEnableWebcil=false" : string.Empty, - "-p:BlazorEnableCompression=false", - "-nr:false", - setWasmDevel ? "-p:_WasmDevel=true" : string.Empty - }.Concat(extraArgs).ToArray(); - - CommandResult res = new DotNetCommand(s_buildEnv, _testOutput) - .WithWorkingDirectory(_projectDir!) - .WithEnvironmentVariable("NUGET_PACKAGES", _nugetPackagesDir) - .ExecuteWithCapturedOutput(combinedArgs) - .EnsureSuccessful(); + // In no-workload case, the path would be from a restored nuget + ProjectProviderBase.AssertRuntimePackPath(buildOutput, blazorBuildOptions.TargetFramework ?? DefaultTargetFramework); + } - return (res, logPath); + _provider.AssertBundle(blazorBuildOptions); } protected string CreateProjectWithNativeReference(string id) @@ -177,4 +180,6 @@ public abstract class BlazorWasmTestBase : WasmTemplateTestBase } } + public string FindBlazorBinFrameworkDir(string config, bool forPublish, string framework = DefaultTargetFrameworkForBlazor) + => _provider.FindBinFrameworkDir(config: config, forPublish: forPublish, framework: framework); } diff --git a/src/mono/wasm/Wasm.Build.Tests/Blazor/BuildPublishTests.cs b/src/mono/wasm/Wasm.Build.Tests/Blazor/BuildPublishTests.cs index 1446212..520dc8d 100644 --- a/src/mono/wasm/Wasm.Build.Tests/Blazor/BuildPublishTests.cs +++ b/src/mono/wasm/Wasm.Build.Tests/Blazor/BuildPublishTests.cs @@ -1,11 +1,9 @@ // 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 System.Linq; using System.Text.Json; -using System.Text.RegularExpressions; using System.Threading.Tasks; using Xunit; using Xunit.Abstractions; @@ -27,18 +25,16 @@ public class BuildPublishTests : BlazorWasmTestBase [Theory, TestCategory("no-workload")] [InlineData("Debug")] [InlineData("Release")] - public void DefaultTemplate_WithoutWorkload(string config) + public async Task DefaultTemplate_WithoutWorkload(string config) { string id = $"blz_no_workload_{config}_{Path.GetRandomFileName()}_{s_unicodeChar}"; CreateBlazorWasmTemplateProject(id); - // Build - BlazorBuildInternal(id, config, publish: false); - _provider.AssertBlazorBootJson(config, isPublish: false); + BlazorBuild(new BlazorBuildOptions(id, config)); + await BlazorRunForBuildWithDotnetRun(config); - // Publish - BlazorBuildInternal(id, config, publish: true); - _provider.AssertBlazorBootJson(config, isPublish: true); + BlazorPublish(new BlazorBuildOptions(id, config)); + await BlazorRunForPublishWithWebServer(config); } [Theory] @@ -280,5 +276,4 @@ public class BuildPublishTests : BlazorWasmTestBase string oldContent = File.ReadAllText(counterRazorPath); File.WriteAllText(counterRazorPath, oldContent + additionalCode); } - } diff --git a/src/mono/wasm/Wasm.Build.Tests/Blazor/MiscTests.cs b/src/mono/wasm/Wasm.Build.Tests/Blazor/MiscTests.cs index 5f06927..c1e2eae 100644 --- a/src/mono/wasm/Wasm.Build.Tests/Blazor/MiscTests.cs +++ b/src/mono/wasm/Wasm.Build.Tests/Blazor/MiscTests.cs @@ -35,32 +35,15 @@ public class MiscTests : BlazorWasmTestBase AddItemsPropertiesToProject(projectFile, extraProperties: extraProperties); // build with -p:DeployOnBuild=true, and that will trigger a publish - (CommandResult res, _) = BlazorBuildInternal(id, config, publish: false, setWasmDevel: false, "-p:DeployOnBuild=true"); - - var expectedFileType = nativeRelink ? NativeFilesType.Relinked : NativeFilesType.AOT; - - _provider.AssertBlazorBundle(new BlazorBuildOptions - ( - Id: id, - Config: config, - ExpectedFileType: expectedFileType - ), isPublish: true); - - if (expectedFileType == NativeFilesType.AOT) - { - // check for this too, so we know the format is correct for the negative - // test for jsinterop.webassembly.dll - Assert.Contains("Microsoft.JSInterop.dll -> Microsoft.JSInterop.dll.bc", res.Output); - - // make sure this assembly gets skipped - Assert.DoesNotContain("Microsoft.JSInterop.WebAssembly.dll -> Microsoft.JSInterop.WebAssembly.dll.bc", res.Output); - } - - // Check that we linked only for publish - string objBuildDir = Path.Combine(_projectDir!, "obj", config, DefaultTargetFramework, "wasm", "for-build"); - Assert.False(Directory.Exists(objBuildDir), $"Found unexpected {objBuildDir}, which gets creating when relinking during Build"); - - // double check! + (CommandResult res, _) = BlazorBuild(new BlazorBuildOptions( + Id: id, + Config: config, + ExpectedFileType: nativeRelink ? NativeFilesType.Relinked : NativeFilesType.AOT, + ExpectRelinkDirWhenPublishing: false, + IsPublish: false), + "-p:DeployBuild=true"); + + // double check relinking! int index = res.Output.IndexOf("pinvoke.c -> pinvoke.o"); Assert.NotEqual(-1, index); diff --git a/src/mono/wasm/Wasm.Build.Tests/BuildProjectOptions.cs b/src/mono/wasm/Wasm.Build.Tests/BuildProjectOptions.cs index 77a4b2b..0f666a5 100644 --- a/src/mono/wasm/Wasm.Build.Tests/BuildProjectOptions.cs +++ b/src/mono/wasm/Wasm.Build.Tests/BuildProjectOptions.cs @@ -12,7 +12,7 @@ public record BuildProjectOptions ( Action? InitProject = null, bool? DotnetWasmFromRuntimePack = null, - GlobalizationMode? GlobalizationMode = null, + GlobalizationMode GlobalizationMode = GlobalizationMode.Default, string? PredefinedIcudt = null, bool UseCache = true, bool ExpectSuccess = true, @@ -23,7 +23,7 @@ public record BuildProjectOptions bool HasV8Script = true, string? Verbosity = null, string? Label = null, - string? TargetFramework = null, + string TargetFramework = BuildTestBase.DefaultTargetFramework, string? MainJS = null, bool IsBrowserProject = true, IDictionary? ExtraBuildEnvironmentVariables = null diff --git a/src/mono/wasm/Wasm.Build.Tests/BuildTestBase.cs b/src/mono/wasm/Wasm.Build.Tests/BuildTestBase.cs index 614aacf..77b0ae8 100644 --- a/src/mono/wasm/Wasm.Build.Tests/BuildTestBase.cs +++ b/src/mono/wasm/Wasm.Build.Tests/BuildTestBase.cs @@ -10,14 +10,12 @@ using System.IO; using System.Linq; using System.Runtime.InteropServices; using System.Text; -using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Threading; using System.Xml; using Xunit; using Xunit.Abstractions; using Xunit.Sdk; -using Microsoft.Playwright; #nullable enable @@ -104,10 +102,6 @@ namespace Wasm.Build.Tests _providerOfBaseType = providerBase; } - // Meant for special case where we *want* to set it to null, - // and thus avoid the ArgumentNullException - // protected void ResetProjectDir() => _providerOfBaseType.ProjectDir = null; - public static IEnumerable> ConfigWithAOTData(bool aot, string? config = null, string? extraArgs = null) { if (extraArgs == null) @@ -134,6 +128,45 @@ namespace Wasm.Build.Tests } } + public (CommandResult res, string logPath) BuildProjectWithoutAssert( + string id, + string config, + BuildProjectOptions buildProjectOptions, + params string[] extraArgs) + { + string buildType = buildProjectOptions.Publish ? "publish" : "build"; + string logFileSuffix = buildProjectOptions.Label == null ? string.Empty : buildProjectOptions.Label.Replace(' ', '_') + "-"; + string logFilePath = Path.Combine(s_buildEnv.LogRootPath, id, $"{id}-{logFileSuffix}{buildType}.binlog"); + + _testOutput.WriteLine($"{Environment.NewLine}** -------- {buildType} -------- **{Environment.NewLine}"); + _testOutput.WriteLine($"Binlog path: {logFilePath}"); + + List commandLineArgs = new() + { + buildType, + $"-bl:{logFilePath}", + $"-p:Configuration={config}", + "-nr:false", + !UseWebcil ? "-p:WasmEnableWebcil=false" : string.Empty, + }; + commandLineArgs.AddRange(extraArgs); + + if (buildProjectOptions.Publish && buildProjectOptions.BuildOnlyAfterPublish) + commandLineArgs.Append("-p:WasmBuildOnlyAfterPublish=true"); + + CommandResult res = new DotNetCommand(s_buildEnv, _testOutput) + .WithWorkingDirectory(_projectDir!) + .WithEnvironmentVariable("NUGET_PACKAGES", _nugetPackagesDir) + .WithEnvironmentVariables(buildProjectOptions.ExtraBuildEnvironmentVariables) + .ExecuteWithCapturedOutput(commandLineArgs.ToArray()); + if (buildProjectOptions.ExpectSuccess) + res.EnsureSuccessful(); + else if (res.ExitCode == 0) + throw new XunitException($"Build should have failed, but it didn't. Process exited with exitCode : {res.ExitCode}"); + + return (res, logFilePath); + } + protected string RunAndTestWasmApp(BuildArgs buildArgs, RunHost host, string id, @@ -338,11 +371,6 @@ namespace Wasm.Build.Tests extraProperties += $"\n{s_isWindows}\n"; } - if (!UseWebcil) - { - extraProperties += "false\n"; - } - extraItems += ""; string projectContents = projectTemplate @@ -361,10 +389,6 @@ namespace Wasm.Build.Tests return contents.Replace(s_nugetInsertionTag, $@""); } - public string FindBlazorBinFrameworkDir(string config, bool forPublish, string framework = DefaultTargetFrameworkForBlazor) - => new BlazorWasmProjectProvider(_testOutput, _projectDir) - .FindBlazorBinFrameworkDir(config, forPublish, framework); - protected string GetBinDir(string config, string targetFramework = DefaultTargetFramework, string? baseDir = null) { var dir = baseDir ?? _projectDir; @@ -550,12 +574,6 @@ namespace Wasm.Build.Tests _buildContext.RemoveFromCache(_projectDir, keepDir: s_skipProjectCleanup); } - private static string GetEnvironmentVariableOrDefault(string envVarName, string defaultValue) - { - string? value = Environment.GetEnvironmentVariable(envVarName); - return string.IsNullOrEmpty(value) ? defaultValue : value; - } - internal BuildPaths GetBuildPaths(BuildArgs buildArgs, bool forPublish = true) { string objDir = GetObjDir(buildArgs.Config); @@ -578,7 +596,7 @@ namespace Wasm.Build.Tests } }"; - private IHostRunner GetHostRunnerFromRunHost(RunHost host) => host switch + private static IHostRunner GetHostRunnerFromRunHost(RunHost host) => host switch { RunHost.V8 => new V8HostRunner(), RunHost.NodeJS => new NodeJSHostRunner(), @@ -592,28 +610,6 @@ namespace Wasm.Build.Tests string ProjectFileContents, string? ExtraBuildArgs); public record BuildProduct(string ProjectDir, string LogFile, bool Result, string BuildOutput); - public record FileStat(bool Exists, DateTime LastWriteTimeUtc, long Length, string FullPath); - public record BuildPaths(string ObjWasmDir, string ObjDir, string BinDir, string BundleDir); - - public record BlazorBuildOptions - ( - string Id, - string Config, - NativeFilesType ExpectedFileType, - string TargetFramework = BuildTestBase.DefaultTargetFrameworkForBlazor, - bool WarnAsError = true, - bool ExpectRelinkDirWhenPublishing = false, - bool ExpectFingerprintOnDotnetJs = false, - RuntimeVariant RuntimeType = RuntimeVariant.SingleThreaded - ); - - public enum GlobalizationMode - { - Invariant, // no icu - FullIcu, // full icu data: icudt.dat is loaded - PredefinedIcu, // user set WasmIcuDataFileName value and we are loading that file - Hybrid // reduced icu, missing data is provided by platform-native functions (web api for wasm) - }; 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 new file mode 100644 index 0000000..807659d --- /dev/null +++ b/src/mono/wasm/Wasm.Build.Tests/Common/AssertBundleOptionsBase.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable enable + +using System.IO; + +namespace Wasm.Build.Tests; + +public abstract record AssertBundleOptionsBase( + string Config, + bool IsPublish, + string TargetFramework, + string BinFrameworkDir, + string? PredefinedIcudt, + string BundleDirName = "wwwroot", + GlobalizationMode GlobalizationMode = GlobalizationMode.Default, + string BootJsonFileName = "blazor.boot.json", + NativeFilesType ExpectedFileType = NativeFilesType.FromRuntimePack, + RuntimeVariant RuntimeType = RuntimeVariant.SingleThreaded, + bool ExpectFingerprintOnDotnetJs = false, + bool ExpectSymbolsFile = true, + bool AssertIcuAssets = true, + bool AssertSymbolsFile = true) +{ + public bool DotnetWasmFromRuntimePack => ExpectedFileType == NativeFilesType.FromRuntimePack; + public bool AOT => ExpectedFileType == NativeFilesType.AOT; + public string BundleDir => Path.Combine(BinFrameworkDir, ".."); +} diff --git a/src/mono/wasm/Wasm.Build.Tests/Common/AssertTestMainJsAppBundleOptions.cs b/src/mono/wasm/Wasm.Build.Tests/Common/AssertTestMainJsAppBundleOptions.cs new file mode 100644 index 0000000..aa01674 --- /dev/null +++ b/src/mono/wasm/Wasm.Build.Tests/Common/AssertTestMainJsAppBundleOptions.cs @@ -0,0 +1,41 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable enable + +namespace Wasm.Build.Tests; + +public record AssertTestMainJsAppBundleOptions( + string Config, + bool IsPublish, + string TargetFramework, + string BinFrameworkDir, + string? PredefinedIcudt, + string ProjectName, + string MainJS, + GlobalizationMode GlobalizationMode = GlobalizationMode.Default, + string BootJsonFileName = "blazor.boot.json", + NativeFilesType ExpectedFileType = NativeFilesType.FromRuntimePack, + RuntimeVariant RuntimeType = RuntimeVariant.SingleThreaded, + bool ExpectFingerprintOnDotnetJs = false, + bool ExpectSymbolsFile = true, + bool AssertIcuAssets = true, + bool AssertSymbolsFile = true, + bool HasV8Script = false, + bool IsBrowserProject = true) + : AssertBundleOptionsBase( + Config: Config, + IsPublish: IsPublish, + TargetFramework: TargetFramework, + BinFrameworkDir: BinFrameworkDir, + PredefinedIcudt: PredefinedIcudt, + GlobalizationMode: GlobalizationMode, + ExpectedFileType: ExpectedFileType, + RuntimeType: RuntimeType, + BootJsonFileName: BootJsonFileName, + ExpectFingerprintOnDotnetJs: ExpectFingerprintOnDotnetJs, + ExpectSymbolsFile: ExpectSymbolsFile, + AssertIcuAssets: AssertIcuAssets, + AssertSymbolsFile: AssertSymbolsFile) +{ +} diff --git a/src/mono/wasm/Wasm.Build.Tests/Common/BuildEnvironment.cs b/src/mono/wasm/Wasm.Build.Tests/Common/BuildEnvironment.cs index 257502b..9c062b0 100644 --- a/src/mono/wasm/Wasm.Build.Tests/Common/BuildEnvironment.cs +++ b/src/mono/wasm/Wasm.Build.Tests/Common/BuildEnvironment.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.IO; -using System.Reflection; using System.Runtime.InteropServices; #nullable enable diff --git a/src/mono/wasm/Wasm.Build.Tests/Common/BuildPaths.cs b/src/mono/wasm/Wasm.Build.Tests/Common/BuildPaths.cs new file mode 100644 index 0000000..1affeed --- /dev/null +++ b/src/mono/wasm/Wasm.Build.Tests/Common/BuildPaths.cs @@ -0,0 +1,7 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable enable + +namespace Wasm.Build.Tests; +public record BuildPaths(string ObjWasmDir, string ObjDir, string BinDir, string BundleDir); diff --git a/src/mono/wasm/Wasm.Build.Tests/DotNetFileName.cs b/src/mono/wasm/Wasm.Build.Tests/Common/DotNetFileName.cs similarity index 100% rename from src/mono/wasm/Wasm.Build.Tests/DotNetFileName.cs rename to src/mono/wasm/Wasm.Build.Tests/Common/DotNetFileName.cs diff --git a/src/mono/wasm/Wasm.Build.Tests/Common/FileStat.cs b/src/mono/wasm/Wasm.Build.Tests/Common/FileStat.cs new file mode 100644 index 0000000..780af48 --- /dev/null +++ b/src/mono/wasm/Wasm.Build.Tests/Common/FileStat.cs @@ -0,0 +1,9 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +#nullable enable + +namespace Wasm.Build.Tests; +public record FileStat(bool Exists, DateTime LastWriteTimeUtc, long Length, string FullPath); diff --git a/src/mono/wasm/Wasm.Build.Tests/Common/GlobalizationMode.cs b/src/mono/wasm/Wasm.Build.Tests/Common/GlobalizationMode.cs new file mode 100644 index 0000000..29fee6d --- /dev/null +++ b/src/mono/wasm/Wasm.Build.Tests/Common/GlobalizationMode.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable enable + +namespace Wasm.Build.Tests; +public enum GlobalizationMode +{ + Default, // chosen based on locale + Invariant, // no icu + FullIcu, // full icu data: icudt.dat is loaded + PredefinedIcu, // user set WasmIcuDataFileName 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/TestUtils.cs b/src/mono/wasm/Wasm.Build.Tests/Common/TestUtils.cs similarity index 100% rename from src/mono/wasm/Wasm.Build.Tests/TestUtils.cs rename to src/mono/wasm/Wasm.Build.Tests/Common/TestUtils.cs diff --git a/src/mono/wasm/Wasm.Build.Tests/HostRunner/IHostRunner.cs b/src/mono/wasm/Wasm.Build.Tests/HostRunner/IHostRunner.cs index 0c93a73..9e41c5f 100644 --- a/src/mono/wasm/Wasm.Build.Tests/HostRunner/IHostRunner.cs +++ b/src/mono/wasm/Wasm.Build.Tests/HostRunner/IHostRunner.cs @@ -7,7 +7,7 @@ namespace Wasm.Build.Tests; public record XHarnessArgsOptions(string jsRelativePath, string environmentLocale, RunHost host); -interface IHostRunner +interface IHostRunner { string GetTestCommand(); string GetXharnessArgsWindowsOS(XHarnessArgsOptions options); diff --git a/src/mono/wasm/Wasm.Build.Tests/IcuShardingTests.cs b/src/mono/wasm/Wasm.Build.Tests/IcuShardingTests.cs index a31bc03..d388c22 100644 --- a/src/mono/wasm/Wasm.Build.Tests/IcuShardingTests.cs +++ b/src/mono/wasm/Wasm.Build.Tests/IcuShardingTests.cs @@ -226,7 +226,7 @@ public class IcuShardingTests : TestMainJsTestBase new BuildProjectOptions( InitProject: () => File.WriteAllText(Path.Combine(_projectDir!, "Program.cs"), programText), DotnetWasmFromRuntimePack: dotnetWasmFromRuntimePack, - GlobalizationMode: invariant ? GlobalizationMode.Invariant : fullIcu ? GlobalizationMode.FullIcu : null)); + GlobalizationMode: invariant ? GlobalizationMode.Invariant : fullIcu ? GlobalizationMode.FullIcu : GlobalizationMode.Default)); string runOutput = RunAndTestWasmApp(buildArgs, buildDir: _projectDir, expectedExitCode: 42, host: host, id: id); } diff --git a/src/mono/wasm/Wasm.Build.Tests/InvariantGlobalizationTests.cs b/src/mono/wasm/Wasm.Build.Tests/InvariantGlobalizationTests.cs index 7ff7bf2..43c0f63 100644 --- a/src/mono/wasm/Wasm.Build.Tests/InvariantGlobalizationTests.cs +++ b/src/mono/wasm/Wasm.Build.Tests/InvariantGlobalizationTests.cs @@ -59,7 +59,7 @@ namespace Wasm.Build.Tests new BuildProjectOptions( InitProject: () => File.Copy(Path.Combine(BuildEnvironment.TestAssetsPath, "Wasm.Buid.Tests.Programs", "InvariantGlobalization.cs"), Path.Combine(_projectDir!, "Program.cs")), DotnetWasmFromRuntimePack: dotnetWasmFromRuntimePack, - GlobalizationMode: invariantGlobalization == true ? GlobalizationMode.Invariant : null)); + GlobalizationMode: invariantGlobalization == true ? GlobalizationMode.Invariant : GlobalizationMode.Default)); if (invariantGlobalization == true) { diff --git a/src/mono/wasm/Wasm.Build.Tests/NativeRebuildTests/NativeRebuildTestsBase.cs b/src/mono/wasm/Wasm.Build.Tests/NativeRebuildTests/NativeRebuildTestsBase.cs index cebe396..ceb63b9 100644 --- a/src/mono/wasm/Wasm.Build.Tests/NativeRebuildTests/NativeRebuildTestsBase.cs +++ b/src/mono/wasm/Wasm.Build.Tests/NativeRebuildTests/NativeRebuildTestsBase.cs @@ -10,6 +10,8 @@ using Xunit; using Xunit.Abstractions; using Xunit.Sdk; using System.Text; +using System.Threading.Tasks; +using System.Threading; #nullable enable @@ -52,7 +54,7 @@ namespace Wasm.Build.NativeRebuild.Tests new BuildProjectOptions( InitProject: () => File.WriteAllText(Path.Combine(_projectDir!, "Program.cs"), programText), DotnetWasmFromRuntimePack: false, - GlobalizationMode: invariant ? GlobalizationMode.Invariant : null, + GlobalizationMode: invariant ? GlobalizationMode.Invariant : GlobalizationMode.Default, CreateProject: true)); RunAndTestWasmApp(buildArgs, buildDir: _projectDir, expectedExitCode: 42, host: RunHost.Chrome, id: id); @@ -77,12 +79,15 @@ namespace Wasm.Build.NativeRebuild.Tests File.WriteAllText(Path.Combine(_projectDir!, $"{buildArgs.ProjectName}.csproj"), buildArgs.ProjectFileContents); buildArgs = newBuildArgs; + // artificial delay to have new enough timestamps + Thread.Sleep(5000); + _testOutput.WriteLine($"{Environment.NewLine}Rebuilding with no changes ..{Environment.NewLine}"); (_, string output) = BuildProject(buildArgs, id: id, new BuildProjectOptions( DotnetWasmFromRuntimePack: false, - GlobalizationMode: invariant ? GlobalizationMode.Invariant : null, + GlobalizationMode: invariant ? GlobalizationMode.Invariant : GlobalizationMode.Default, CreateProject: false, UseCache: false, Verbosity: verbosity)); diff --git a/src/mono/wasm/Wasm.Build.Tests/ProjectProviderBase.cs b/src/mono/wasm/Wasm.Build.Tests/ProjectProviderBase.cs index 50afb40..ccdf07d 100644 --- a/src/mono/wasm/Wasm.Build.Tests/ProjectProviderBase.cs +++ b/src/mono/wasm/Wasm.Build.Tests/ProjectProviderBase.cs @@ -8,48 +8,81 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; +using System.Runtime.Serialization.Json; using System.Text; using System.Text.RegularExpressions; +using Microsoft.NET.Sdk.WebAssembly; using Xunit; using Xunit.Abstractions; using Xunit.Sdk; namespace Wasm.Build.Tests; +// For projects using WasmAppBuilder public abstract class ProjectProviderBase(ITestOutputHelper _testOutput, string? _projectDir) { public const string WebcilInWasmExtension = ".wasm"; - protected const string s_dotnetVersionHashRegex = @"\.(?.+)\.(?[a-zA-Z0-9]+)\."; + protected const string s_dotnetVersionHashRegex = @"\.(?[0-9]+\.[0-9]+\.[a-zA-Z0-9\.-]+)\.(?[a-zA-Z0-9]+)\."; + + private const string s_runtimePackPathPattern = "\\*\\* MicrosoftNetCoreAppRuntimePackDir : '([^ ']*)'"; + private static Regex s_runtimePackPathRegex = new Regex(s_runtimePackPathPattern); private static string[] s_dotnetExtensionsToIgnore = new[] { ".gz", - ".br", - ".symbols" + ".br" }; - private const string s_runtimePackPathPattern = "\\*\\* MicrosoftNetCoreAppRuntimePackDir : '([^ ']*)'"; - private static Regex s_runtimePackPathRegex = new Regex(s_runtimePackPathPattern); public string? ProjectDir { get; set; } = _projectDir; protected ITestOutputHelper _testOutput = _testOutput; protected BuildEnvironment _buildEnv = BuildTestBase.s_buildEnv; + public string BundleDirName { get; set; } = "wwwroot"; - public IReadOnlyDictionary FindAndAssertDotnetFiles( - string dir, - bool isPublish, - bool expectFingerprintOnDotnetJs, - RuntimeVariant runtimeType) + // Returns the actual files on disk + public IReadOnlyDictionary AssertBasicBundle(AssertBundleOptionsBase assertOptions) + { + EnsureProjectDirIsSet(); + var dotnetFiles = FindAndAssertDotnetFiles(assertOptions); + + TestUtils.AssertFilesExist(assertOptions.BinFrameworkDir, + new[] { "System.Private.CoreLib.dll" }, + expectToExist: !BuildTestBase.UseWebcil); + TestUtils.AssertFilesExist(assertOptions.BinFrameworkDir, + new[] { "System.Private.CoreLib.wasm" }, + expectToExist: BuildTestBase.UseWebcil); + + AssertBootJson(assertOptions); + + // icu + if (assertOptions.AssertIcuAssets) + { + _testOutput.WriteLine("Skipping asserting icu assets"); + AssertIcuAssets(assertOptions); + } + + // symbols + if (assertOptions.AssertSymbolsFile) + { + _testOutput.WriteLine("Skipping asserting symbols file"); + AssertDotNetJsSymbols(assertOptions); + } + + return dotnetFiles; + } + + public IReadOnlyDictionary FindAndAssertDotnetFiles(AssertBundleOptionsBase assertOptions) { - return FindAndAssertDotnetFiles(dir: dir, - expectFingerprintOnDotnetJs: expectFingerprintOnDotnetJs, - superSet: GetAllKnownDotnetFilesToFingerprintMap(runtimeType), - expected: GetDotNetFilesExpectedSet(runtimeType, isPublish)); + EnsureProjectDirIsSet(); + return FindAndAssertDotnetFiles(binFrameworkDir: assertOptions.BinFrameworkDir, + expectFingerprintOnDotnetJs: assertOptions.ExpectFingerprintOnDotnetJs, + superSet: GetAllKnownDotnetFilesToFingerprintMap(assertOptions), + expected: GetDotNetFilesExpectedSet(assertOptions)); } - protected abstract IReadOnlyDictionary GetAllKnownDotnetFilesToFingerprintMap(RuntimeVariant runtimeType); - protected abstract IReadOnlySet GetDotNetFilesExpectedSet(RuntimeVariant runtimeType, bool isPublish); + protected abstract IReadOnlyDictionary GetAllKnownDotnetFilesToFingerprintMap(AssertBundleOptionsBase assertOptions); + protected abstract IReadOnlySet GetDotNetFilesExpectedSet(AssertBundleOptionsBase assertOptions); public IReadOnlyDictionary FindAndAssertDotnetFiles( - string dir, + string binFrameworkDir, bool expectFingerprintOnDotnetJs, IReadOnlyDictionary superSet, IReadOnlySet? expected) @@ -57,7 +90,10 @@ public abstract class ProjectProviderBase(ITestOutputHelper _testOutput, string? EnsureProjectDirIsSet(); var actual = new SortedDictionary(); - IList dotnetFiles = Directory.EnumerateFiles(dir, + if (!Directory.Exists(binFrameworkDir)) + throw new XunitException($"Could not find bundle directory {binFrameworkDir}"); + + IList dotnetFiles = Directory.EnumerateFiles(binFrameworkDir, "dotnet.*", SearchOption.TopDirectoryOnly) .Order() @@ -74,7 +110,7 @@ public abstract class ProjectProviderBase(ITestOutputHelper _testOutput, string? return false; string actualFilename = Path.GetFileName(actualFile); - _testOutput.WriteLine($"Comparing {expectedFilename} with {actualFile}, expectFingerprintOnDotnetJs: {expectFingerprintOnDotnetJs}, expectFingerprint: {expectFingerprint}"); + // _testOutput.WriteLine($"Comparing {expectedFilename} with {actualFile}, expectFingerprintOnDotnetJs: {expectFingerprintOnDotnetJs}, expectFingerprint: {expectFingerprint}"); if (ShouldCheckFingerprint(expectedFilename: expectedFilename, expectFingerprintOnDotnetJs: expectFingerprintOnDotnetJs, expectFingerprintForThisFile: expectFingerprint)) @@ -104,32 +140,37 @@ public abstract class ProjectProviderBase(ITestOutputHelper _testOutput, string? }).ToList(); } - _testOutput.WriteLine($"Accepted count: {actual.Count}"); - foreach (var kvp in actual) - _testOutput.WriteLine($"Accepted: \t[{kvp.Key}] = {kvp.Value}"); + // _testOutput.WriteLine($"Accepted count: {actual.Count}"); + // foreach (var kvp in actual) + // _testOutput.WriteLine($"Accepted: \t[{kvp.Key}] = {kvp.Value}"); if (dotnetFiles.Any()) { - throw new XunitException($"Found unknown files in {dir}:{Environment.NewLine} {string.Join($"{Environment.NewLine} ", dotnetFiles)}"); + throw new XunitException($"Found unknown files in {binFrameworkDir}:{Environment.NewLine} " + + $"{string.Join($"{Environment.NewLine} ", dotnetFiles.Select(f => Path.GetRelativePath(binFrameworkDir, f)))}{Environment.NewLine}" + + $"Add these to {nameof(GetAllKnownDotnetFilesToFingerprintMap)} method"); } if (expected is not null) - AssertDotNetFilesSet(expected, superSet, actual, expectFingerprintOnDotnetJs); + AssertDotNetFilesSet(expected, superSet, actual, expectFingerprintOnDotnetJs, binFrameworkDir); return actual; } - public void AssertDotNetFilesSet( + private void AssertDotNetFilesSet( IReadOnlySet expected, IReadOnlyDictionary superSet, - IDictionary actual, - bool expectFingerprintOnDotnetJs) + IReadOnlyDictionary actualReadOnly, + bool expectFingerprintOnDotnetJs, + string bundleDir) { EnsureProjectDirIsSet(); + + var actual = new Dictionary(actualReadOnly); foreach (string expectedFilename in expected) { bool expectFingerprint = superSet[expectedFilename]; - Assert.True(actual.ContainsKey(expectedFilename), $"Could not find {expectedFilename} in {string.Join(", ", actual.Keys)}"); + Assert.True(actual.ContainsKey(expectedFilename), $"Could not find {expectedFilename} in the list of actual files on disk - {string.Join(", ", actual.Keys)} in bundle directory: {bundleDir}"); // Check that the version and hash are present or not present as expected if (ShouldCheckFingerprint(expectedFilename: expectedFilename, @@ -148,15 +189,13 @@ public abstract class ProjectProviderBase(ITestOutputHelper _testOutput, string? if (!string.IsNullOrEmpty(actual[expectedFilename].Hash)) throw new XunitException($"Expected no hash in filename: {actual[expectedFilename].ActualPath}"); } + actual.Remove(expectedFilename); } - if (expected.Count < actual.Count) + if (actual.Any()) { - StringBuilder sb = new(); - sb.AppendLine($"Expected: {string.Join(", ", expected)}"); - // FIXME: show the difference in a better way - sb.AppendLine($"Actual: {string.Join(", ", actual.Values.Select(a => a.ActualPath).Order())}"); - throw new XunitException($"Expected and actual file sets do not match.{Environment.NewLine}{sb}"); + var actualFileNames = actual.Values.Select(x => x.ActualPath).Order(); + throw new XunitException($"Found unexpected files: {string.Join(", ", actualFileNames)}"); } } @@ -210,6 +249,19 @@ public abstract class ProjectProviderBase(ITestOutputHelper _testOutput, string? return table; } + public static string FindSubDirIgnoringCase(string parentDir, string dirName) + { + IEnumerable matchingDirs = Directory.EnumerateDirectories(parentDir, + dirName, + new EnumerationOptions { MatchCasing = MatchCasing.CaseInsensitive }); + + string? first = matchingDirs.FirstOrDefault(); + if (matchingDirs.Count() > 1) + throw new Exception($"Found multiple directories with names that differ only in case. {string.Join(", ", matchingDirs.ToArray())}"); + + return first ?? Path.Combine(parentDir, dirName); + } + public IDictionary GetFilesTable(bool unchanged, params string[] baseDirs) { var dict = new Dictionary(); @@ -263,22 +315,11 @@ public abstract class ProjectProviderBase(ITestOutputHelper _testOutput, string? return dict; } - public static string FindSubDirIgnoringCase(string parentDir, string dirName) - { - IEnumerable matchingDirs = Directory.EnumerateDirectories(parentDir, - dirName, - new EnumerationOptions { MatchCasing = MatchCasing.CaseInsensitive }); - - string? first = matchingDirs.FirstOrDefault(); - if (matchingDirs.Count() > 1) - throw new Exception($"Found multiple directories with names that differ only in case. {string.Join(", ", matchingDirs.ToArray())}"); - - return first ?? Path.Combine(parentDir, dirName); - } public static bool ShouldCheckFingerprint(string expectedFilename, bool expectFingerprintOnDotnetJs, bool expectFingerprintForThisFile) => (expectedFilename == "dotnet.js" && expectFingerprintOnDotnetJs) || expectFingerprintForThisFile; + public static void AssertRuntimePackPath(string buildOutput, string targetFramework) { var match = s_runtimePackPathRegex.Match(buildOutput); @@ -291,10 +332,142 @@ public abstract class ProjectProviderBase(ITestOutputHelper _testOutput, string? throw new XunitException($"Runtime pack path doesn't match.{Environment.NewLine}Expected: '{expectedRuntimePackDir}'{Environment.NewLine}Actual: '{actualPath}'"); } - public static void AssertDotNetJsSymbols(string bundleDir, bool fromRuntimePack, string targetFramework) - => TestUtils.AssertFile(Path.Combine(BuildTestBase.s_buildEnv.GetRuntimeNativeDir(targetFramework), "dotnet.native.js.symbols"), - Path.Combine(bundleDir, "_framework/dotnet.native.js.symbols"), - same: fromRuntimePack); + public static void AssertDotNetJsSymbols(AssertBundleOptionsBase assertOptions) + { + TestUtils.AssertFilesExist(assertOptions.BinFrameworkDir, new[] { "dotnet.native.js.symbols" }, expectToExist: assertOptions.ExpectSymbolsFile); + + if (assertOptions.ExpectedFileType == NativeFilesType.FromRuntimePack) + { + TestUtils.AssertFile( + Path.Combine(BuildTestBase.s_buildEnv.GetRuntimeNativeDir(assertOptions.TargetFramework, assertOptions.RuntimeType), "dotnet.native.js.symbols"), + Path.Combine(assertOptions.BinFrameworkDir, "dotnet.native.js.symbols"), + same: true); + } + } + + public void AssertIcuAssets(AssertBundleOptionsBase assertOptions) + { + List expected = new(); + switch (assertOptions.GlobalizationMode) + { + case GlobalizationMode.Invariant: + break; + case GlobalizationMode.FullIcu: + expected.Add("icudt.dat"); + break; + case GlobalizationMode.Hybrid: + expected.Add("icudt_hybrid.dat"); + break; + case GlobalizationMode.PredefinedIcu: + if (string.IsNullOrEmpty(assertOptions.PredefinedIcudt)) + throw new ArgumentException("WasmBuildTest is invalid, value for predefinedIcudt is required when GlobalizationMode=PredefinedIcu."); + + // predefined ICU name can be identical with the icu files from runtime pack + expected.Add(assertOptions.PredefinedIcudt); + break; + case GlobalizationMode.Default: + // icu shard chosen based on the locale + expected.Add("icudt_CJK.dat"); + expected.Add("icudt_EFIGS.dat"); + expected.Add("icudt_no_CJK.dat"); + break; + default: + throw new NotImplementedException($"Unknown {nameof(assertOptions.GlobalizationMode)} = {assertOptions.GlobalizationMode}"); + } + + IEnumerable actual = Directory.EnumerateFiles(assertOptions.BinFrameworkDir, "icudt*dat"); + AssertFilesOnDisk(expected, actual); + if (assertOptions.GlobalizationMode is GlobalizationMode.PredefinedIcu) + TestUtils.AssertSameFile(assertOptions.PredefinedIcudt!, actual.Single()); + } + + public void AssertBootJson(AssertBundleOptionsBase options) + { + EnsureProjectDirIsSet(); + // string binFrameworkDir = FindBinFrameworkDir(options.Config, options.IsPublish, options.TargetFramework); + string binFrameworkDir = options.BinFrameworkDir; + string bootJsonPath = Path.Combine(binFrameworkDir, options.BootJsonFileName); + Assert.True(File.Exists(bootJsonPath), $"Expected to find {bootJsonPath}"); + + BootJsonData bootJson = ParseBootData(bootJsonPath); + var bootJsonEntries = bootJson.resources.runtime.Keys.Where(k => k.StartsWith("dotnet.", StringComparison.Ordinal)).ToArray(); + + var expectedEntries = new SortedDictionary>(); + IReadOnlySet expected = GetDotNetFilesExpectedSet(options); + + var knownSet = GetAllKnownDotnetFilesToFingerprintMap(options); + foreach (string expectedFilename in expected) + { + if (Path.GetExtension(expectedFilename) == ".map") + continue; + + bool expectFingerprint = knownSet[expectedFilename]; + expectedEntries[expectedFilename] = item => + { + string prefix = Path.GetFileNameWithoutExtension(expectedFilename); + string extension = Path.GetExtension(expectedFilename).Substring(1); + + if (ShouldCheckFingerprint(expectedFilename: expectedFilename, + expectFingerprintOnDotnetJs: options.ExpectFingerprintOnDotnetJs, + expectFingerprintForThisFile: expectFingerprint)) + { + Assert.Matches($"{prefix}{s_dotnetVersionHashRegex}{extension}", item); + } + else + { + Assert.Equal(expectedFilename, item); + } + + string absolutePath = Path.Combine(binFrameworkDir, item); + Assert.True(File.Exists(absolutePath), $"Expected to find '{absolutePath}'"); + }; + } + // FIXME: maybe use custom code so the details can show up in the log + bootJsonEntries = bootJsonEntries.Order().ToArray(); + if (bootJsonEntries.Length != expectedEntries.Count) + { + throw new XunitException($"In {bootJsonPath}{Environment.NewLine}" + + $" Expected: {string.Join(", ", expectedEntries.Keys.ToArray())}{Environment.NewLine}" + + $" Actual : {string.Join(", ", bootJsonEntries)}"); + + + } + Assert.Collection(bootJsonEntries.Order(), expectedEntries.Values.ToArray()); + } + + public static BootJsonData ParseBootData(string bootJsonPath) + { + using FileStream stream = File.OpenRead(bootJsonPath); + stream.Position = 0; + var serializer = new DataContractJsonSerializer( + typeof(BootJsonData), + new DataContractJsonSerializerSettings { UseSimpleDictionaryFormat = true }); + + var config = (BootJsonData?)serializer.ReadObject(stream); + Assert.NotNull(config); + return config; + } + + private void AssertFilesOnDisk(IEnumerable expected, IEnumerable actual) + { + expected = expected.Order().Select(f => Path.GetFileName(f)).Distinct(); + var actualFileNames = actual.Order().Select(f => Path.GetFileName(f)); + Assert.True(expected.Count() == actualFileNames.Count(), + $"Expected: {string.Join(", ", expected)}{Environment.NewLine}" + + $"Actual: {string.Join(", ", actualFileNames)}"); + + Assert.Equal(expected, actualFileNames); + } + + public virtual string FindBinFrameworkDir(string config, bool forPublish, string framework, string? bundleDirName = null) + { + EnsureProjectDirIsSet(); + string basePath = Path.Combine(ProjectDir!, "bin", config, framework); + if (forPublish) + basePath = FindSubDirIgnoringCase(basePath, "publish"); + + return Path.Combine(basePath, bundleDirName ?? this.BundleDirName, "_framework"); + } [MemberNotNull(nameof(ProjectDir))] protected void EnsureProjectDirIsSet() diff --git a/src/mono/wasm/Wasm.Build.Tests/RebuildTests.cs b/src/mono/wasm/Wasm.Build.Tests/RebuildTests.cs index 52599a6..0a316a4 100644 --- a/src/mono/wasm/Wasm.Build.Tests/RebuildTests.cs +++ b/src/mono/wasm/Wasm.Build.Tests/RebuildTests.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Threading.Tasks; using Xunit; using Xunit.Abstractions; using Xunit.Sdk; @@ -27,7 +28,7 @@ namespace Wasm.Build.Tests [Theory] [MemberData(nameof(NonNativeDebugRebuildData))] - public void NoOpRebuild(BuildArgs buildArgs, RunHost host, string id) + public async Task NoOpRebuild(BuildArgs buildArgs, RunHost host, string id) { string projectName = $"rebuild_{buildArgs.Config}_{buildArgs.AOT}"; @@ -48,6 +49,9 @@ namespace Wasm.Build.Tests File.Move(product!.LogFile, Path.ChangeExtension(product.LogFile!, ".first.binlog")); + // artificial delay to have new enough timestamps + await Task.Delay(5000); + _testOutput.WriteLine($"{Environment.NewLine}Rebuilding with no changes ..{Environment.NewLine}"); // no-op Rebuild diff --git a/src/mono/wasm/Wasm.Build.Tests/Templates/WasmTemplateTests.cs b/src/mono/wasm/Wasm.Build.Tests/Templates/WasmTemplateTests.cs index 2452b9a..2e698c3 100644 --- a/src/mono/wasm/Wasm.Build.Tests/Templates/WasmTemplateTests.cs +++ b/src/mono/wasm/Wasm.Build.Tests/Templates/WasmTemplateTests.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -using System.Collections.Generic; using System.IO; using System.Text; using System.Threading.Tasks; @@ -107,8 +106,6 @@ namespace Wasm.Build.Tests TargetFramework: BuildTestBase.DefaultTargetFramework )); - ProjectProviderBase.AssertDotNetJsSymbols(Path.Combine(GetBinDir(config), "AppBundle"), fromRuntimePack: true, targetFramework: DefaultTargetFramework); - if (!_buildContext.TryGetBuildFor(buildArgs, out BuildProduct? product)) throw new XunitException($"Test bug: could not get the build product in the cache"); @@ -127,8 +124,6 @@ namespace Wasm.Build.Tests Publish: true, TargetFramework: BuildTestBase.DefaultTargetFramework, UseCache: false)); - - ProjectProviderBase.AssertDotNetJsSymbols(Path.Combine(GetBinDir(config), "AppBundle"), fromRuntimePack: !expectRelinking, targetFramework: DefaultTargetFramework); } [Theory] @@ -157,8 +152,6 @@ namespace Wasm.Build.Tests IsBrowserProject: false )); - ProjectProviderBase.AssertDotNetJsSymbols(Path.Combine(GetBinDir(config), "AppBundle"), fromRuntimePack: true, targetFramework: DefaultTargetFramework); - CommandResult res = new RunCommand(s_buildEnv, _testOutput) .WithWorkingDirectory(_projectDir!) .ExecuteWithCapturedOutput($"run --no-silent --no-build -c {config}") @@ -184,8 +177,6 @@ namespace Wasm.Build.Tests TargetFramework: BuildTestBase.DefaultTargetFramework, UseCache: false, IsBrowserProject: false)); - - ProjectProviderBase.AssertDotNetJsSymbols(Path.Combine(GetBinDir(config), "AppBundle"), fromRuntimePack: !expectRelinking, targetFramework: DefaultTargetFramework); } [ConditionalTheory(typeof(BuildTestBase), nameof(IsUsingWorkloads))] @@ -229,8 +220,6 @@ namespace Wasm.Build.Tests IsBrowserProject: false )); - ProjectProviderBase.AssertDotNetJsSymbols(Path.Combine(GetBinDir(config, expectedTFM), "AppBundle"), fromRuntimePack: !relinking, targetFramework: expectedTFM); - CommandResult res = new RunCommand(s_buildEnv, _testOutput) .WithWorkingDirectory(_projectDir!) .ExecuteWithCapturedOutput($"run --no-silent --no-build -c {config} x y z") @@ -403,16 +392,6 @@ namespace Wasm.Build.Tests UseCache: false, IsBrowserProject: false)); - if (!aot) - { - // These are disabled for AOT explicitly - ProjectProviderBase.AssertDotNetJsSymbols(Path.Combine(GetBinDir(config), "AppBundle"), fromRuntimePack: !expectRelinking, targetFramework: DefaultTargetFramework); - } - else - { - TestUtils.AssertFilesDontExist(Path.Combine(GetBinDir(config), "AppBundle"), new[] { "dotnet.native.js.symbols" }); - } - string runArgs = $"run --no-silent --no-build -c {config}"; runArgs += " x y z"; var res = new RunCommand(s_buildEnv, _testOutput, label: id) diff --git a/src/mono/wasm/Wasm.Build.Tests/TestMainJsProjectProvider.cs b/src/mono/wasm/Wasm.Build.Tests/TestMainJsProjectProvider.cs index 5d5887c..bc1fa0a 100644 --- a/src/mono/wasm/Wasm.Build.Tests/TestMainJsProjectProvider.cs +++ b/src/mono/wasm/Wasm.Build.Tests/TestMainJsProjectProvider.cs @@ -14,49 +14,46 @@ public class TestMainJsProjectProvider : ProjectProviderBase { public TestMainJsProjectProvider(ITestOutputHelper _testOutput, string? _projectDir = null) : base(_testOutput, _projectDir) - {} + { + BundleDirName = "AppBundle"; + } // no fingerprinting - protected override IReadOnlyDictionary GetAllKnownDotnetFilesToFingerprintMap(RuntimeVariant runtimeType) + protected override IReadOnlyDictionary GetAllKnownDotnetFilesToFingerprintMap(AssertBundleOptionsBase assertOptions) => new SortedDictionary() { { "dotnet.js", false }, { "dotnet.js.map", false }, { "dotnet.native.js", false }, + { "dotnet.native.js.symbols", false }, { "dotnet.native.wasm", false }, { "dotnet.native.worker.js", false }, { "dotnet.runtime.js", false }, { "dotnet.runtime.js.map", false } }; - protected override IReadOnlySet GetDotNetFilesExpectedSet(RuntimeVariant runtimeType, bool isPublish) + protected override IReadOnlySet GetDotNetFilesExpectedSet(AssertBundleOptionsBase assertOptions) { - SortedSet? res = null; - if (runtimeType is RuntimeVariant.SingleThreaded) + SortedSet? res = new(); + if (assertOptions.RuntimeType is RuntimeVariant.SingleThreaded) { - res = new SortedSet() - { - "dotnet.js", - "dotnet.native.wasm", - "dotnet.native.js", - "dotnet.runtime.js", - }; - + res.Add("dotnet.js"); + res.Add("dotnet.native.wasm"); + res.Add("dotnet.native.js"); + res.Add("dotnet.runtime.js"); res.Add("dotnet.js.map"); res.Add("dotnet.runtime.js.map"); } - if (runtimeType is RuntimeVariant.MultiThreaded) + if (assertOptions.RuntimeType is RuntimeVariant.MultiThreaded) { - res = new SortedSet() - { - "dotnet.js", - "dotnet.native.js", - "dotnet.native.wasm", - "dotnet.native.worker.js", - "dotnet.runtime.js", - }; - if (!isPublish) + res.Add("dotnet.js"); + res.Add("dotnet.native.wasm"); + res.Add("dotnet.native.js"); + res.Add("dotnet.runtime.js"); + res.Add("dotnet.native.worker.js"); + + if (!assertOptions.IsPublish) { res.Add("dotnet.js.map"); res.Add("dotnet.runtime.js.map"); @@ -64,106 +61,56 @@ public class TestMainJsProjectProvider : ProjectProviderBase } } - return res ?? throw new ArgumentException($"Unknown runtime type: {runtimeType}"); + if (assertOptions.AssertSymbolsFile && assertOptions.ExpectSymbolsFile) + res.Add("dotnet.native.js.symbols"); + + return res ?? throw new ArgumentException($"Unknown runtime type: {assertOptions.RuntimeType}"); } - public void AssertBasicAppBundle(AssertTestMainJsAppBundleOptions options) + public void AssertBundle(AssertTestMainJsAppBundleOptions assertOptions) { - EnsureProjectDirIsSet(); - new TestMainJsProjectProvider(_testOutput, ProjectDir) - .FindAndAssertDotnetFiles( - Path.Combine(options.BundleDir, "_framework"), - isPublish: options.IsPublish, - expectFingerprintOnDotnetJs: false, - runtimeType: RuntimeVariant.SingleThreaded); - - var filesToExist = new List() - { - options.MainJS, - "_framework/blazor.boot.json", - "_framework/dotnet.js.map", - "_framework/dotnet.runtime.js.map", - }; - - if (options.IsBrowserProject) - filesToExist.Add("index.html"); - - TestUtils.AssertFilesExist(options.BundleDir, filesToExist); + AssertBasicBundle(assertOptions); - TestUtils.AssertFilesExist(options.BundleDir, new[] { "run-v8.sh" }, expectToExist: options.HasV8Script); - AssertIcuAssets(); + TestUtils.AssertFilesExist(assertOptions.BundleDir, new[] { assertOptions.MainJS }); + if (assertOptions.IsBrowserProject) + TestUtils.AssertFilesExist(assertOptions.BundleDir, new[] { "index.html" }); + TestUtils.AssertFilesExist(assertOptions.BundleDir, new[] { "run-v8.sh" }, expectToExist: assertOptions.HasV8Script); - string managedDir = Path.Combine(options.BundleDir, "_framework"); string bundledMainAppAssembly = - options.UseWebcil ? $"{options.ProjectName}{WebcilInWasmExtension}" : $"{options.ProjectName}.dll"; - TestUtils.AssertFilesExist(managedDir, new[] { bundledMainAppAssembly }); - - bool is_debug = options.Config == "Debug"; - if (is_debug) - { - // Use cecil to check embedded pdb? - // AssertFilesExist(managedDir, new[] { $"{projectName}.pdb" }); - - //FIXME: um.. what about these? embedded? why is linker omitting them? - //foreach (string file in Directory.EnumerateFiles(managedDir, "*.dll")) - //{ - //string pdb = Path.ChangeExtension(file, ".pdb"); - //Assert.True(File.Exists(pdb), $"Could not find {pdb} for {file}"); - //} - } + BuildTestBase.UseWebcil ? $"{assertOptions.ProjectName}{WebcilInWasmExtension}" : $"{assertOptions.ProjectName}.dll"; + TestUtils.AssertFilesExist(assertOptions.BinFrameworkDir, new[] { bundledMainAppAssembly }); + } - void AssertIcuAssets() - { - bool expectEFIGS = false; - bool expectCJK = false; - bool expectNOCJK = false; - bool expectFULL = false; - bool expectHYBRID = false; - switch (options.GlobalizationMode) - { - case GlobalizationMode.Invariant: - break; - case GlobalizationMode.FullIcu: - expectFULL = true; - break; - case GlobalizationMode.Hybrid: - expectHYBRID = true; - break; - case GlobalizationMode.PredefinedIcu: - if (string.IsNullOrEmpty(options.PredefinedIcudt)) - throw new ArgumentException("WasmBuildTest is invalid, value for predefinedIcudt is required when GlobalizationMode=PredefinedIcu."); - TestUtils.AssertFilesExist(options.BundleDir, new[] { Path.Combine("_framework", options.PredefinedIcudt) }, expectToExist: true); - // predefined ICU name can be identical with the icu files from runtime pack - switch (options.PredefinedIcudt) - { - case "icudt.dat": - expectFULL = true; - break; - case "icudt_EFIGS.dat": - expectEFIGS = true; - break; - case "icudt_CJK.dat": - expectCJK = true; - break; - case "icudt_no_CJK.dat": - expectNOCJK = true; - break; - } - break; - default: - // icu shard chosen based on the locale - expectCJK = true; - expectEFIGS = true; - expectNOCJK = true; - break; - } + public void AssertBundle(BuildArgs buildArgs, BuildProjectOptions buildProjectOptions) + { + string binFrameworkDir = FindBinFrameworkDir(buildArgs.Config, + buildProjectOptions.Publish, + buildProjectOptions.TargetFramework); + NativeFilesType expectedFileType = buildArgs.AOT + ? NativeFilesType.AOT + : buildProjectOptions.DotnetWasmFromRuntimePack == false + ? NativeFilesType.Relinked + : NativeFilesType.FromRuntimePack; + + var assertOptions = new AssertTestMainJsAppBundleOptions( + Config: buildArgs.Config, + IsPublish: buildProjectOptions.Publish, + TargetFramework: buildProjectOptions.TargetFramework!, + BinFrameworkDir: binFrameworkDir, + ProjectName: buildArgs.ProjectName, + MainJS: buildProjectOptions.MainJS ?? "test-main.js", + GlobalizationMode: buildProjectOptions.GlobalizationMode, + HasV8Script: buildProjectOptions.HasV8Script, + PredefinedIcudt: buildProjectOptions.PredefinedIcudt ?? string.Empty, + IsBrowserProject: buildProjectOptions.IsBrowserProject, + ExpectedFileType: expectedFileType, + ExpectSymbolsFile: !buildArgs.AOT); + AssertBundle(assertOptions); + } - var frameworkDir = Path.Combine(options.BundleDir, "_framework"); - TestUtils.AssertFilesExist(frameworkDir, new[] { "icudt.dat" }, expectToExist: expectFULL); - TestUtils.AssertFilesExist(frameworkDir, new[] { "icudt_EFIGS.dat" }, expectToExist: expectEFIGS); - TestUtils.AssertFilesExist(frameworkDir, new[] { "icudt_CJK.dat" }, expectToExist: expectCJK); - TestUtils.AssertFilesExist(frameworkDir, new[] { "icudt_no_CJK.dat" }, expectToExist: expectNOCJK); - TestUtils.AssertFilesExist(frameworkDir, new[] { "icudt_hybrid.dat" }, expectToExist: expectHYBRID); - } + public override string FindBinFrameworkDir(string config, bool forPublish, string framework, string? bundleDirName = null) + { + EnsureProjectDirIsSet(); + return Path.Combine(ProjectDir!, "bin", config, framework, "browser-wasm", bundleDirName ?? this.BundleDirName, "_framework"); } } diff --git a/src/mono/wasm/Wasm.Build.Tests/TestMainJsTestBase.cs b/src/mono/wasm/Wasm.Build.Tests/TestMainJsTestBase.cs index f7eeb4b..ae36d1c 100644 --- a/src/mono/wasm/Wasm.Build.Tests/TestMainJsTestBase.cs +++ b/src/mono/wasm/Wasm.Build.Tests/TestMainJsTestBase.cs @@ -4,9 +4,7 @@ #nullable enable using System; -using System.Collections.Generic; using System.IO; -using System.Text; using Xunit.Abstractions; using Xunit.Sdk; @@ -63,79 +61,29 @@ public abstract class TestMainJsTestBase : BuildTestBase throw new Exception("_projectDir should be set, to use options.createProject=false"); } - StringBuilder sb = new(); - sb.Append(options.Publish ? "publish" : "build"); - if (options.Publish && options.BuildOnlyAfterPublish) - sb.Append(" -p:WasmBuildOnlyAfterPublish=true"); - sb.Append($" {s_buildEnv.DefaultBuildArgs}"); - - sb.Append($" /p:Configuration={buildArgs.Config}"); - - string logFileSuffix = options.Label == null ? string.Empty : options.Label.Replace(' ', '_'); - string logFilePath = Path.Combine(_logPath, $"{buildArgs.ProjectName}{logFileSuffix}.binlog"); - _testOutput.WriteLine($"-------- Building ---------"); - _testOutput.WriteLine($"Binlog path: {logFilePath}"); - sb.Append($" /bl:\"{logFilePath}\" /nologo"); - sb.Append($" /v:{options.Verbosity ?? "minimal"}"); - if (buildArgs.ExtraBuildArgs != null) - sb.Append($" {buildArgs.ExtraBuildArgs} "); - - _testOutput.WriteLine($"Building {buildArgs.ProjectName} in {_projectDir}"); - - (int exitCode, string buildOutput) result; try { - var envVars = s_buildEnv.EnvVars; - if (options.ExtraBuildEnvironmentVariables is not null) - { - envVars = new Dictionary(s_buildEnv.EnvVars); - foreach (var kvp in options.ExtraBuildEnvironmentVariables!) - envVars[kvp.Key] = kvp.Value; - } - envVars["NUGET_PACKAGES"] = _nugetPackagesDir; - result = AssertBuild(sb.ToString(), id, expectSuccess: options.ExpectSuccess, envVars: envVars); - - // check that we are using the correct runtime pack! + (CommandResult res, string logFilePath) = BuildProjectWithoutAssert(id, + buildArgs.Config, + options, + string.Join(" ", buildArgs.ExtraBuildArgs)); if (options.ExpectSuccess && options.AssertAppBundle) { - ProjectProviderBase.AssertRuntimePackPath(result.buildOutput, options.TargetFramework ?? DefaultTargetFramework); - - string bundleDir = Path.Combine(GetBinDir(config: buildArgs.Config, targetFramework: options.TargetFramework ?? DefaultTargetFramework), "AppBundle"); - _provider.AssertBasicAppBundle(new AssertTestMainJsAppBundleOptions( - BundleDir: bundleDir, - ProjectName: buildArgs.ProjectName, - Config: buildArgs.Config, - MainJS: options.MainJS ?? "test-main.js", - HasV8Script: options.HasV8Script, - GlobalizationMode: options.GlobalizationMode, - PredefinedIcudt: options.PredefinedIcudt ?? "", - UseWebcil: UseWebcil, - IsBrowserProject: options.IsBrowserProject, - IsPublish: options.Publish)); + ProjectProviderBase.AssertRuntimePackPath(res.Output, options.TargetFramework ?? DefaultTargetFramework); + _provider.AssertBundle(buildArgs, options); } if (options.UseCache) - _buildContext.CacheBuild(buildArgs, new BuildProduct(_projectDir, logFilePath, true, result.buildOutput)); + _buildContext.CacheBuild(buildArgs, new BuildProduct(_projectDir, logFilePath, true, res.Output)); - return (_projectDir, result.buildOutput); + return (_projectDir, res.Output); } catch (Exception ex) { if (options.UseCache) - _buildContext.CacheBuild(buildArgs, new BuildProduct(_projectDir, logFilePath, false, $"The build attempt resulted in exception: {ex}.")); + _buildContext.CacheBuild(buildArgs, new BuildProduct(_projectDir, /*logFilePath*/"unset-log-path", false, $"The build attempt resulted in exception: {ex}.")); throw; } } - - protected (int exitCode, string buildOutput) AssertBuild(string args, string label = "build", bool expectSuccess = true, IDictionary? envVars = null, int? timeoutMs = null) - { - var result = RunProcess(s_buildEnv.DotNet, _testOutput, args, workingDir: _projectDir, label: label, envVars: envVars, timeoutMs: timeoutMs ?? s_defaultPerTestTimeoutMs); - if (expectSuccess && result.exitCode != 0) - throw new XunitException($"Build process exited with non-zero exit code: {result.exitCode}"); - if (!expectSuccess && result.exitCode == 0) - throw new XunitException($"Build should have failed, but it didn't. Process exited with exitCode : {result.exitCode}"); - - return result; - } } diff --git a/src/mono/wasm/Wasm.Build.Tests/WasmSdkBasedProjectProvider.cs b/src/mono/wasm/Wasm.Build.Tests/WasmSdkBasedProjectProvider.cs index 5dbc2ba..1f70aa4 100644 --- a/src/mono/wasm/Wasm.Build.Tests/WasmSdkBasedProjectProvider.cs +++ b/src/mono/wasm/Wasm.Build.Tests/WasmSdkBasedProjectProvider.cs @@ -2,13 +2,11 @@ // 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.Runtime.Serialization.Json; -using Microsoft.NET.Sdk.WebAssembly; -using Xunit; +using System.Collections.Generic; using Xunit.Abstractions; +using Xunit.Sdk; +using System.Linq; #nullable enable @@ -20,19 +18,20 @@ public class WasmSdkBasedProjectProvider : ProjectProviderBase : base(_testOutput, _projectDir) {} - protected override IReadOnlyDictionary GetAllKnownDotnetFilesToFingerprintMap(RuntimeVariant runtimeType) + protected override IReadOnlyDictionary GetAllKnownDotnetFilesToFingerprintMap(AssertBundleOptionsBase assertOptions) => new SortedDictionary() { { "dotnet.js", false }, { "dotnet.js.map", false }, { "dotnet.native.js", true }, + { "dotnet.native.js.symbols", false }, { "dotnet.native.wasm", false }, { "dotnet.native.worker.js", true }, { "dotnet.runtime.js", true }, - { "dotnet.runtime.js.map", false } + { "dotnet.runtime.js.map", false }, }; - protected override IReadOnlySet GetDotNetFilesExpectedSet(RuntimeVariant runtimeType, bool isPublish) + protected override IReadOnlySet GetDotNetFilesExpectedSet(AssertBundleOptionsBase assertOptions) { SortedSet res = new() { @@ -41,131 +40,62 @@ public class WasmSdkBasedProjectProvider : ProjectProviderBase "dotnet.native.js", "dotnet.runtime.js", }; - if (runtimeType is RuntimeVariant.MultiThreaded) + if (assertOptions.RuntimeType is RuntimeVariant.MultiThreaded) { res.Add("dotnet.native.worker.js"); } - if (!isPublish) + if (!assertOptions.IsPublish) { res.Add("dotnet.js.map"); res.Add("dotnet.runtime.js.map"); } + if (assertOptions.AssertSymbolsFile && assertOptions.ExpectSymbolsFile) + res.Add("dotnet.native.js.symbols"); + return res; } - public void AssertDotNetNativeFiles( - NativeFilesType type, - string config, - bool forPublish, - string targetFramework, - bool expectFingerprintOnDotnetJs, - RuntimeVariant runtimeType = RuntimeVariant.SingleThreaded) + public void AssertBundle(AssertWasmSdkBundleOptions assertOptions) { - EnsureProjectDirIsSet(); - string label = forPublish ? "publish" : "build"; - string objBuildDir = Path.Combine(ProjectDir, "obj", config, targetFramework, "wasm", forPublish ? "for-publish" : "for-build"); - string binFrameworkDir = FindBlazorBinFrameworkDir(config, forPublish, framework: targetFramework); + IReadOnlyDictionary actualDotnetFiles = AssertBasicBundle(assertOptions); + + if (!BuildTestBase.IsUsingWorkloads) + return; - var dotnetFiles = FindAndAssertDotnetFiles( - dir: binFrameworkDir, - isPublish: forPublish, - expectFingerprintOnDotnetJs: expectFingerprintOnDotnetJs, - runtimeType: runtimeType); + // Compare files with the runtime pack + string objBuildDir = Path.Combine(ProjectDir!, "obj", assertOptions.Config, assertOptions.TargetFramework, "wasm", assertOptions.IsPublish ? "for-publish" : "for-build"); - string runtimeNativeDir = _buildEnv.GetRuntimeNativeDir(targetFramework, runtimeType); + string runtimeNativeDir = BuildTestBase.s_buildEnv.GetRuntimeNativeDir(assertOptions.TargetFramework, assertOptions.RuntimeType); - string srcDirForNativeFileToCompareAgainst = type switch + string srcDirForNativeFileToCompareAgainst = assertOptions.ExpectedFileType switch { NativeFilesType.FromRuntimePack => runtimeNativeDir, NativeFilesType.Relinked => objBuildDir, NativeFilesType.AOT => objBuildDir, - _ => throw new ArgumentOutOfRangeException(nameof(type)) + _ => throw new ArgumentOutOfRangeException(nameof(assertOptions.ExpectedFileType)) }; + string buildType = assertOptions.IsPublish ? "publish" : "build"; foreach (string nativeFilename in new[] { "dotnet.native.wasm", "dotnet.native.js" }) { + if (!actualDotnetFiles.TryGetValue(nativeFilename, out DotNetFileName? dotnetFile)) + { + throw new XunitException($"Could not find {nativeFilename}. Actual files on disk: {string.Join($"{Environment.NewLine} ", actualDotnetFiles.Values.Select(a => a.ActualPath).Order())}"); + + } // For any *type*, check against the expected path TestUtils.AssertSameFile(Path.Combine(srcDirForNativeFileToCompareAgainst, nativeFilename), - dotnetFiles[nativeFilename].ActualPath, - label); + actualDotnetFiles[nativeFilename].ActualPath, + buildType); - if (type != NativeFilesType.FromRuntimePack) + if (assertOptions.ExpectedFileType != NativeFilesType.FromRuntimePack) { // Confirm that it doesn't match the file from the runtime pack TestUtils.AssertNotSameFile(Path.Combine(runtimeNativeDir, nativeFilename), - dotnetFiles[nativeFilename].ActualPath, - label); + actualDotnetFiles[nativeFilename].ActualPath, + buildType); } } } - public void AssertBootJson( - string binFrameworkDir, - bool expectFingerprintOnDotnetJs = false, - bool isPublish = false, - RuntimeVariant runtimeType = RuntimeVariant.SingleThreaded) - { - EnsureProjectDirIsSet(); - string bootJsonPath = Path.Combine(binFrameworkDir, "blazor.boot.json"); - Assert.True(File.Exists(bootJsonPath), $"Expected to find {bootJsonPath}"); - - BootJsonData bootJson = ParseBootData(bootJsonPath); - var bootJsonEntries = bootJson.resources.runtime.Keys.Where(k => k.StartsWith("dotnet.", StringComparison.Ordinal)).ToArray(); - - var expectedEntries = new SortedDictionary>(); - IReadOnlySet expected = GetDotNetFilesExpectedSet(runtimeType, isPublish); - - var knownSet = GetAllKnownDotnetFilesToFingerprintMap(runtimeType); - foreach (string expectedFilename in expected) - { - if (Path.GetExtension(expectedFilename) == ".map") - continue; - - bool expectFingerprint = knownSet[expectedFilename]; - expectedEntries[expectedFilename] = item => - { - string prefix = Path.GetFileNameWithoutExtension(expectedFilename); - string extension = Path.GetExtension(expectedFilename).Substring(1); - - if (ShouldCheckFingerprint(expectedFilename: expectedFilename, - expectFingerprintOnDotnetJs: expectFingerprintOnDotnetJs, - expectFingerprintForThisFile: expectFingerprint)) - { - Assert.Matches($"{prefix}{s_dotnetVersionHashRegex}{extension}", item); - } - else - { - Assert.Equal(expectedFilename, item); - } - - string absolutePath = Path.Combine(binFrameworkDir, item); - Assert.True(File.Exists(absolutePath), $"Expected to find '{absolutePath}'"); - }; - } - // FIXME: maybe use custom code so the details can show up in the log - Assert.Collection(bootJsonEntries.Order(), expectedEntries.Values.ToArray()); - } - - public static BootJsonData ParseBootData(string bootJsonPath) - { - using FileStream stream = File.OpenRead(bootJsonPath); - stream.Position = 0; - var serializer = new DataContractJsonSerializer( - typeof(BootJsonData), - new DataContractJsonSerializerSettings { UseSimpleDictionaryFormat = true }); - - var config = (BootJsonData?)serializer.ReadObject(stream); - Assert.NotNull(config); - return config; - } - - public string FindBlazorBinFrameworkDir(string config, bool forPublish, string framework) - { - EnsureProjectDirIsSet(); - string basePath = Path.Combine(ProjectDir, "bin", config, framework); - if (forPublish) - basePath = FindSubDirIgnoringCase(basePath, "publish"); - - return Path.Combine(basePath, "wwwroot", "_framework"); - } } diff --git a/src/mono/wasm/Wasm.Build.Tests/WasmTemplateTestBase.cs b/src/mono/wasm/Wasm.Build.Tests/WasmTemplateTestBase.cs index c82be4a..aee3d0e 100644 --- a/src/mono/wasm/Wasm.Build.Tests/WasmTemplateTestBase.cs +++ b/src/mono/wasm/Wasm.Build.Tests/WasmTemplateTestBase.cs @@ -1,31 +1,28 @@ // 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; +#nullable enable + using System.IO; -using System.Text; -using System.Threading.Tasks; -using Xunit; using Xunit.Abstractions; -using Xunit.Sdk; - -#nullable enable namespace Wasm.Build.Tests; -public class WasmTemplateTestBase : BuildTestBase +public abstract class WasmTemplateTestBase : BuildTestBase { private readonly WasmSdkBasedProjectProvider _provider; protected WasmTemplateTestBase(ITestOutputHelper output, SharedBuildPerTestClassFixture buildContext, WasmSdkBasedProjectProvider? projectProvider = null) : base(projectProvider ?? new WasmSdkBasedProjectProvider(output), output, buildContext) { _provider = GetProvider(); + // Wasm templates are not using wasm sdk yet + _provider.BundleDirName = "AppBundle"; } public string CreateWasmTemplateProject(string id, string template = "wasmbrowser", string extraArgs = "", bool runAnalyzers = true) { InitPaths(id); + _testOutput.WriteLine($"after initpaths: {_provider.ProjectDir}"); InitProjectDir(_projectDir, addNuGetSourceForLocalPackages: true); File.WriteAllText(Path.Combine(_projectDir, "Directory.Build.props"), ""); @@ -48,8 +45,6 @@ public class WasmTemplateTestBase : BuildTestBase extraProperties += "true"; if (runAnalyzers) extraProperties += "true"; - if (!UseWebcil) - extraProperties += "false"; // TODO: Can be removed after updated templates propagate in. string extraItems = string.Empty; @@ -64,51 +59,32 @@ public class WasmTemplateTestBase : BuildTestBase } public (string projectDir, string buildOutput) BuildTemplateProject(BuildArgs buildArgs, - string id, - BuildProjectOptions buildProjectOptions, - AssertTestMainJsAppBundleOptions? assertAppBundleOptions = null) + string id, + BuildProjectOptions buildProjectOptions, + AssertTestMainJsAppBundleOptions? assertAppBundleOptions = null) { - StringBuilder buildCmdLine = new(); - buildCmdLine.Append(buildProjectOptions.Publish ? "publish" : "build"); - - string logFilePath = Path.Combine(s_buildEnv.LogRootPath, $"{id}.binlog"); - _testOutput.WriteLine($"-------- Building ---------"); - _testOutput.WriteLine($"Binlog path: {logFilePath}"); - buildCmdLine.Append($" -c {buildArgs.Config} -bl:{logFilePath} {buildArgs.ExtraBuildArgs}"); - - if (buildProjectOptions.Publish && buildProjectOptions.BuildOnlyAfterPublish) - buildCmdLine.Append(" -p:WasmBuildOnlyAfterPublish=true"); - - CommandResult res = new DotNetCommand(s_buildEnv, _testOutput) - .WithWorkingDirectory(_projectDir!) - .WithEnvironmentVariables(buildProjectOptions.ExtraBuildEnvironmentVariables) - .ExecuteWithCapturedOutput(buildCmdLine.ToString()); - if (buildProjectOptions.ExpectSuccess) - res.EnsureSuccessful(); - else - Assert.NotEqual(0, res.ExitCode); - + (CommandResult res, string logFilePath) = BuildProjectWithoutAssert(id, buildArgs.Config, buildProjectOptions); if (buildProjectOptions.UseCache) _buildContext.CacheBuild(buildArgs, new BuildProduct(_projectDir!, logFilePath, true, res.Output)); - ProjectProviderBase.AssertRuntimePackPath(res.Output, buildProjectOptions.TargetFramework ?? DefaultTargetFramework); - string bundleDir = Path.Combine(GetBinDir(config: buildArgs.Config, targetFramework: buildProjectOptions.TargetFramework ?? DefaultTargetFramework), "AppBundle"); - - assertAppBundleOptions ??= new AssertTestMainJsAppBundleOptions( - BundleDir: bundleDir, - ProjectName: buildArgs.ProjectName, - Config: buildArgs.Config, - MainJS: buildProjectOptions.MainJS ?? "test-main.js", - HasV8Script: buildProjectOptions.HasV8Script, - GlobalizationMode: buildProjectOptions.GlobalizationMode, - PredefinedIcudt: buildProjectOptions.PredefinedIcudt ?? "", - UseWebcil: UseWebcil, - IsBrowserProject: buildProjectOptions.IsBrowserProject, - IsPublish: buildProjectOptions.Publish); - new TestMainJsProjectProvider(_testOutput, _projectDir) - .AssertBasicAppBundle(assertAppBundleOptions); - + if (buildProjectOptions.AssertAppBundle) + AssertBundle(buildArgs, buildProjectOptions, res.Output, assertAppBundleOptions); return (_projectDir!, res.Output); } + public void AssertBundle(BuildArgs buildArgs, + BuildProjectOptions buildProjectOptions, + string? buildOutput = null, + AssertTestMainJsAppBundleOptions? assertAppBundleOptions = null) + { + if (buildOutput is not null) + ProjectProviderBase.AssertRuntimePackPath(buildOutput, buildProjectOptions.TargetFramework ?? DefaultTargetFramework); + + // TODO: templates don't use wasm sdk yet + var testMainJsProvider = new TestMainJsProjectProvider(_testOutput, _projectDir!); + if (assertAppBundleOptions is not null) + testMainJsProvider.AssertBundle(assertAppBundleOptions); + else + testMainJsProvider.AssertBundle(buildArgs, buildProjectOptions); + } } diff --git a/src/mono/wasm/sln/WasmBuild.sln b/src/mono/wasm/sln/WasmBuild.sln index b2ce61d..265cbcd 100755 --- a/src/mono/wasm/sln/WasmBuild.sln +++ b/src/mono/wasm/sln/WasmBuild.sln @@ -25,6 +25,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ApplyUpdateReferencedAssemb EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.NET.Sdk.WebAssembly.Pack.Tasks", "..\..\..\tasks\Microsoft.NET.Sdk.WebAssembly.Pack.Tasks\Microsoft.NET.Sdk.WebAssembly.Pack.Tasks.csproj", "{5EEC2925-2021-4830-B7E9-72BB8B2C283D}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wasi.Build.Tests", "..\..\wasi\Wasi.Build.Tests\Wasi.Build.Tests.csproj", "{3A3AEAE5-0110-45D3-89B0-B82AC430535C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -75,6 +77,10 @@ Global {5EEC2925-2021-4830-B7E9-72BB8B2C283D}.Debug|Any CPU.Build.0 = Debug|Any CPU {5EEC2925-2021-4830-B7E9-72BB8B2C283D}.Release|Any CPU.ActiveCfg = Release|Any CPU {5EEC2925-2021-4830-B7E9-72BB8B2C283D}.Release|Any CPU.Build.0 = Release|Any CPU + {3A3AEAE5-0110-45D3-89B0-B82AC430535C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3A3AEAE5-0110-45D3-89B0-B82AC430535C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3A3AEAE5-0110-45D3-89B0-B82AC430535C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3A3AEAE5-0110-45D3-89B0-B82AC430535C}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE -- 2.7.4