From d3d910ea88b00c7af2f69df07cbfbbe86da56596 Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Wed, 12 Jul 2023 02:09:40 -0400 Subject: [PATCH] [wasm] Wasm.Build.Tests - some refactoring, and rationalizing (#88357) --- .../AssertTestMainJsAppBundleOptions.cs | 20 + .../Blazor/AppsettingsTests.cs | 2 +- .../Blazor/BuildPublishTests.cs | 4 +- .../wasm/Wasm.Build.Tests/Blazor/MiscTests.cs | 8 +- .../Wasm.Build.Tests/Blazor/MiscTests2.cs | 66 --- .../BlazorWasmProjectProvider.cs | 88 ++++ .../Wasm.Build.Tests/BuildProjectOptions.cs | 30 ++ .../wasm/Wasm.Build.Tests/BuildTestBase.cs | 388 +++++++----------- .../Common/BuildEnvironment.cs | 12 +- .../Wasm.Build.Tests/Common/RuntimeVariant.cs | 8 + .../Common/TestOutputWrapper.cs | 26 ++ .../Wasm.Build.Tests/Common/ToolCommand.cs | 4 - .../wasm/Wasm.Build.Tests/DotNetFileName.cs | 14 + .../Wasm.Build.Tests/ProjectProviderBase.cs | 170 ++++++++ .../TestMainJsProjectProvider.cs | 65 +++ .../Wasm.Build.Tests/Wasm.Build.Tests.csproj | 15 +- .../WasmSdkBasedProjectProvider.cs | 48 +++ .../Wasm.Build.Tests/WasmTemplateTests.cs | 12 +- src/mono/wasm/wasm.code-workspace | 3 +- 19 files changed, 661 insertions(+), 322 deletions(-) create mode 100644 src/mono/wasm/Wasm.Build.Tests/AssertTestMainJsAppBundleOptions.cs create mode 100644 src/mono/wasm/Wasm.Build.Tests/BlazorWasmProjectProvider.cs create mode 100644 src/mono/wasm/Wasm.Build.Tests/BuildProjectOptions.cs create mode 100644 src/mono/wasm/Wasm.Build.Tests/Common/RuntimeVariant.cs create mode 100644 src/mono/wasm/Wasm.Build.Tests/Common/TestOutputWrapper.cs create mode 100644 src/mono/wasm/Wasm.Build.Tests/DotNetFileName.cs create mode 100644 src/mono/wasm/Wasm.Build.Tests/ProjectProviderBase.cs create mode 100644 src/mono/wasm/Wasm.Build.Tests/TestMainJsProjectProvider.cs create mode 100644 src/mono/wasm/Wasm.Build.Tests/WasmSdkBasedProjectProvider.cs diff --git a/src/mono/wasm/Wasm.Build.Tests/AssertTestMainJsAppBundleOptions.cs b/src/mono/wasm/Wasm.Build.Tests/AssertTestMainJsAppBundleOptions.cs new file mode 100644 index 0000000000000..125c5c6a709bd --- /dev/null +++ b/src/mono/wasm/Wasm.Build.Tests/AssertTestMainJsAppBundleOptions.cs @@ -0,0 +1,20 @@ +// 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 BundleDir, + string ProjectName, + string Config, + string MainJS, + bool HasV8Script, + GlobalizationMode? GlobalizationMode, + string PredefinedIcudt = "", + bool UseWebcil = true, + bool IsBrowserProject = true, + bool IsPublish = false +); diff --git a/src/mono/wasm/Wasm.Build.Tests/Blazor/AppsettingsTests.cs b/src/mono/wasm/Wasm.Build.Tests/Blazor/AppsettingsTests.cs index 89ecb5b3c6a32..767099b21a2cb 100644 --- a/src/mono/wasm/Wasm.Build.Tests/Blazor/AppsettingsTests.cs +++ b/src/mono/wasm/Wasm.Build.Tests/Blazor/AppsettingsTests.cs @@ -54,4 +54,4 @@ await BlazorRunForBuildWithDotnetRun("debug", onConsoleMessage: msg => Assert.True(existsChecked, "File '/appsettings.json' wasn't found"); Assert.True(contentChecked, "Content of '/appsettings.json' is not matched"); } -} \ No newline at end of file +} diff --git a/src/mono/wasm/Wasm.Build.Tests/Blazor/BuildPublishTests.cs b/src/mono/wasm/Wasm.Build.Tests/Blazor/BuildPublishTests.cs index c94d1dda9fdf9..7ccb136d1dc29 100644 --- a/src/mono/wasm/Wasm.Build.Tests/Blazor/BuildPublishTests.cs +++ b/src/mono/wasm/Wasm.Build.Tests/Blazor/BuildPublishTests.cs @@ -34,11 +34,11 @@ public void DefaultTemplate_WithoutWorkload(string config) // Build BlazorBuildInternal(id, config, publish: false); - AssertBlazorBootJson(config, isPublish: false, isNet7AndBelow: false); + AssertBlazorBootJson(config, isPublish: false); // Publish BlazorBuildInternal(id, config, publish: true); - AssertBlazorBootJson(config, isPublish: true, isNet7AndBelow: false); + AssertBlazorBootJson(config, isPublish: true); } [Theory] diff --git a/src/mono/wasm/Wasm.Build.Tests/Blazor/MiscTests.cs b/src/mono/wasm/Wasm.Build.Tests/Blazor/MiscTests.cs index 9a094d9d10265..fd7a96b1e8cd3 100644 --- a/src/mono/wasm/Wasm.Build.Tests/Blazor/MiscTests.cs +++ b/src/mono/wasm/Wasm.Build.Tests/Blazor/MiscTests.cs @@ -39,8 +39,12 @@ public void NativeBuild_WithDeployOnBuild_UsedByVS(string config, bool nativeRel var expectedFileType = nativeRelink ? NativeFilesType.Relinked : NativeFilesType.AOT; - AssertDotNetNativeFiles(expectedFileType, config, forPublish: true, targetFramework: DefaultTargetFrameworkForBlazor); - AssertBlazorBundle(config, isPublish: true, dotnetWasmFromRuntimePack: false); + AssertBlazorBundle(new BlazorBuildOptions + ( + Id: id, + Config: config, + ExpectedFileType: expectedFileType + ), isPublish: true); if (expectedFileType == NativeFilesType.AOT) { diff --git a/src/mono/wasm/Wasm.Build.Tests/Blazor/MiscTests2.cs b/src/mono/wasm/Wasm.Build.Tests/Blazor/MiscTests2.cs index 467a23597f777..d23fa9e0d7447 100644 --- a/src/mono/wasm/Wasm.Build.Tests/Blazor/MiscTests2.cs +++ b/src/mono/wasm/Wasm.Build.Tests/Blazor/MiscTests2.cs @@ -66,70 +66,4 @@ private CommandResult PublishForRequiresWorkloadTest(string config, string extra $"-bl:{publishLogPath}", $"-p:Configuration={config}"); } - - [Theory] - [InlineData("Debug")] - [InlineData("Release")] - public void Net50Projects_NativeReference(string config) - => BuildNet50Project(config, aot: false, expectError: true, @""); - - public static TheoryData Net50TestData = new() - { - { "Debug", /*aot*/ true, /*expectError*/ true }, - { "Debug", /*aot*/ false, /*expectError*/ false }, - { "Release", /*aot*/ true, /*expectError*/ true }, - { "Release", /*aot*/ false, /*expectError*/ false } - }; - - // FIXME: test for WasmBuildNative=true? - [Theory] - [MemberData(nameof(Net50TestData))] - public void Net50Projects_AOT(string config, bool aot, bool expectError) - => BuildNet50Project(config, aot: aot, expectError: expectError); - - private void BuildNet50Project(string config, bool aot, bool expectError, string? extraItems=null) - { - string id = $"Blazor_net50_{config}_{aot}_{Path.GetRandomFileName()}"; - InitBlazorWasmProjectDir(id); - - string directoryBuildTargets = @" - - - - "; - - File.WriteAllText(Path.Combine(_projectDir!, "Directory.Build.props"), ""); - File.WriteAllText(Path.Combine(_projectDir!, "Directory.Build.targets"), directoryBuildTargets); - - string logPath = Path.Combine(s_buildEnv.LogRootPath, id); - Utils.DirectoryCopy(Path.Combine(BuildEnvironment.TestAssetsPath, "Blazor_net50"), Path.Combine(_projectDir!)); - - string projectFile = Path.Combine(_projectDir!, "Blazor_net50.csproj"); - AddItemsPropertiesToProject(projectFile, extraItems: extraItems); - - string publishLogPath = Path.Combine(logPath, $"{id}.binlog"); - CommandResult result = new DotNetCommand(s_buildEnv, _testOutput) - .WithWorkingDirectory(_projectDir!) - .WithEnvironmentVariable("NUGET_PACKAGES", _nugetPackagesDir) - .ExecuteWithCapturedOutput("publish", - $"-bl:{publishLogPath}", - (aot ? "-p:RunAOTCompilation=true" : ""), - $"-p:Configuration={config}"); - - if (expectError) - { - result.EnsureExitCode(1); - Assert.Contains("are only supported for projects targeting net6.0+", result.Output); - } - else - { - result.EnsureSuccessful(); - Assert.Contains("** UsingBrowserRuntimeWorkload: 'false'", result.Output); - - string binFrameworkDir = FindBlazorBinFrameworkDir(config, forPublish: true, framework: "net5.0"); - AssertBlazorBootJson(config, isPublish: true, isNet7AndBelow: true, binFrameworkDir: binFrameworkDir); - // dotnet.wasm here would be from 5.0 nuget like: - // /Users/radical/.nuget/packages/microsoft.netcore.app.runtime.browser-wasm/5.0.9/runtimes/browser-wasm/native/dotnet.wasm - } - } } diff --git a/src/mono/wasm/Wasm.Build.Tests/BlazorWasmProjectProvider.cs b/src/mono/wasm/Wasm.Build.Tests/BlazorWasmProjectProvider.cs new file mode 100644 index 0000000000000..7432a78eed603 --- /dev/null +++ b/src/mono/wasm/Wasm.Build.Tests/BlazorWasmProjectProvider.cs @@ -0,0 +1,88 @@ +// 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.Data; +using System.IO; +using System.Linq; +using Xunit; +using Xunit.Abstractions; +using System.Runtime.Serialization.Json; +using Microsoft.NET.Sdk.WebAssembly; + +#nullable enable + +namespace Wasm.Build.Tests; + +public class BlazorWasmProjectProvider(string projectDir, ITestOutputHelper testOutput) + : WasmSdkBasedProjectProvider(projectDir, testOutput) +{ + public void AssertBlazorBootJson( + string binFrameworkDir, + bool expectFingerprintOnDotnetJs = false, + bool isPublish = false, + RuntimeVariant runtimeType = RuntimeVariant.SingleThreaded) + { + 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) + { + 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/BuildProjectOptions.cs b/src/mono/wasm/Wasm.Build.Tests/BuildProjectOptions.cs new file mode 100644 index 0000000000000..77a4b2bee8ae6 --- /dev/null +++ b/src/mono/wasm/Wasm.Build.Tests/BuildProjectOptions.cs @@ -0,0 +1,30 @@ +// 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 + +namespace Wasm.Build.Tests; + +public record BuildProjectOptions +( + Action? InitProject = null, + bool? DotnetWasmFromRuntimePack = null, + GlobalizationMode? GlobalizationMode = null, + string? PredefinedIcudt = null, + bool UseCache = true, + bool ExpectSuccess = true, + bool AssertAppBundle = true, + bool CreateProject = true, + bool Publish = true, + bool BuildOnlyAfterPublish = true, + bool HasV8Script = true, + string? Verbosity = null, + string? Label = null, + string? TargetFramework = null, + 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 433af91ec0f16..e9d15fe279483 100644 --- a/src/mono/wasm/Wasm.Build.Tests/BuildTestBase.cs +++ b/src/mono/wasm/Wasm.Build.Tests/BuildTestBase.cs @@ -10,7 +10,6 @@ using System.Linq; using System.Runtime.InteropServices; using System.Text; -using System.Text.Json.Nodes; using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Threading; @@ -19,8 +18,6 @@ using Xunit.Abstractions; using Xunit.Sdk; using Microsoft.Playwright; -using System.Runtime.Serialization.Json; -using Microsoft.NET.Sdk.WebAssembly; #nullable enable @@ -101,21 +98,10 @@ public BuildTestBase(ITestOutputHelper output, SharedBuildPerTestClassFixture bu { _testIdx = Interlocked.Increment(ref s_testCounter); _buildContext = buildContext; - _testOutput = output; + _testOutput = new TestOutputWrapper(output); _logPath = s_buildEnv.LogRootPath; // FIXME: } - /* - * TODO: - - AOT modes - - llvmonly - - aotinterp - - skipped assemblies should get have their pinvoke/icall stuff scanned - - - only buildNative - - aot but no wrapper - check that AppBundle wasn't generated - */ - public static IEnumerable> ConfigWithAOTData(bool aot, string? config = null, string? extraArgs = null) { if (extraArgs == null) @@ -142,7 +128,6 @@ public BuildTestBase(ITestOutputHelper output, SharedBuildPerTestClassFixture bu } } - protected string RunAndTestWasmApp(BuildArgs buildArgs, RunHost host, string id, @@ -361,6 +346,53 @@ protected static BuildArgs ExpandBuildArgs(BuildArgs buildArgs, string extraProp return buildArgs with { ProjectFileContents = projectContents }; } + public (string projectDir, string buildOutput) BuildTemplateProject(BuildArgs buildArgs, + 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); + + if (buildProjectOptions.UseCache) + _buildContext.CacheBuild(buildArgs, new BuildProduct(_projectDir!, logFilePath, true, res.Output)); + + 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); + AssertBasicAppBundle(assertAppBundleOptions); + + return (_projectDir!, res.Output); + } + public (string projectDir, string buildOutput) BuildProject(BuildArgs buildArgs, string id, BuildProjectOptions options) @@ -442,17 +474,17 @@ protected static BuildArgs ExpandBuildArgs(BuildArgs buildArgs, string extraProp AssertRuntimePackPath(result.buildOutput, options.TargetFramework ?? DefaultTargetFramework); string bundleDir = Path.Combine(GetBinDir(config: buildArgs.Config, targetFramework: options.TargetFramework ?? DefaultTargetFramework), "AppBundle"); - AssertBasicAppBundle(bundleDir, - buildArgs.ProjectName, - buildArgs.Config, - options.MainJS ?? "test-main.js", - options.HasV8Script, - options.TargetFramework ?? DefaultTargetFramework, - options.GlobalizationMode, - options.PredefinedIcudt ?? "", - options.DotnetWasmFromRuntimePack ?? !buildArgs.AOT, - UseWebcil, - options.IsBrowserProject); + 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)); } if (options.UseCache) @@ -554,13 +586,7 @@ public string CreateBlazorWasmTemplateProject(string id) extraArgs = extraArgs.Append("/warnaserror").ToArray(); var res = BlazorBuildInternal(options.Id, options.Config, publish: false, setWasmDevel: false, extraArgs); - _testOutput.WriteLine($"BlazorBuild, options.tfm: {options.TargetFramework}"); - AssertDotNetNativeFiles(options.ExpectedFileType, options.Config, forPublish: false, targetFramework: options.TargetFramework); - AssertBlazorBundle(options.Config, - isPublish: false, - dotnetWasmFromRuntimePack: options.ExpectedFileType == NativeFilesType.FromRuntimePack, - targetFramework: options.TargetFramework, - expectFingerprintOnDotnetJs: options.ExpectFingerprintOnDotnetJs); + AssertBlazorBundle(options, isPublish: false); return res; } @@ -568,12 +594,7 @@ public string CreateBlazorWasmTemplateProject(string id) protected (CommandResult, string) BlazorPublish(BlazorBuildOptions options, params string[] extraArgs) { var res = BlazorBuildInternal(options.Id, options.Config, publish: true, setWasmDevel: false, extraArgs); - AssertDotNetNativeFiles(options.ExpectedFileType, options.Config, forPublish: true, targetFramework: options.TargetFramework); - AssertBlazorBundle(options.Config, - isPublish: true, - dotnetWasmFromRuntimePack: options.ExpectedFileType == NativeFilesType.FromRuntimePack, - targetFramework: options.TargetFramework, - expectFingerprintOnDotnetJs: options.ExpectFingerprintOnDotnetJs); + AssertBlazorBundle(options, isPublish: true); if (options.ExpectedFileType == NativeFilesType.AOT) { @@ -583,7 +604,6 @@ public string CreateBlazorWasmTemplateProject(string id) // make sure this assembly gets skipped Assert.DoesNotContain("Microsoft.JSInterop.WebAssembly.dll -> Microsoft.JSInterop.WebAssembly.dll.bc", res.Item1.Output); - } string objBuildDir = Path.Combine(_projectDir!, "obj", options.Config, options.TargetFramework, "wasm", "for-build"); @@ -622,36 +642,48 @@ public string CreateBlazorWasmTemplateProject(string id) return (res, logPath); } - protected void AssertDotNetNativeFiles(NativeFilesType type, string config, bool forPublish, string targetFramework) + private void AssertBlazorDotNetNativeFiles( + NativeFilesType type, + string config, + bool forPublish, + string targetFramework, + bool expectFingerprintOnDotnetJs, + RuntimeVariant runtimeType = RuntimeVariant.SingleThreaded) { 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); - string srcDir = type switch + var dotnetFiles = new WasmSdkBasedProjectProvider(_projectDir!, _testOutput) + .FindAndAssertDotnetFiles( + dir: binFrameworkDir, + isPublish: forPublish, + expectFingerprintOnDotnetJs: expectFingerprintOnDotnetJs, + runtimeType: runtimeType); + + string runtimeNativeDir = s_buildEnv.GetRuntimeNativeDir(targetFramework, runtimeType); + + string srcDirForNativeFileToCompareAgainst = type switch { - NativeFilesType.FromRuntimePack => s_buildEnv.GetRuntimeNativeDir(targetFramework), + NativeFilesType.FromRuntimePack => runtimeNativeDir, NativeFilesType.Relinked => objBuildDir, NativeFilesType.AOT => objBuildDir, _ => throw new ArgumentOutOfRangeException(nameof(type)) }; - - AssertSameFile(Path.Combine(srcDir, "dotnet.native.wasm"), Path.Combine(binFrameworkDir, "dotnet.native.wasm"), label); - - // find dotnet*js - string? dotnetJsPath = Directory.EnumerateFiles(binFrameworkDir) - .Where(p => Path.GetFileName(p).StartsWith("dotnet.native", StringComparison.OrdinalIgnoreCase) && - Path.GetFileName(p).EndsWith(".js", StringComparison.OrdinalIgnoreCase)) - .SingleOrDefault(); - - Assert.True(!string.IsNullOrEmpty(dotnetJsPath), $"[{label}] Expected to find dotnet.native*js in {binFrameworkDir}"); - AssertSameFile(Path.Combine(srcDir, "dotnet.native.js"), dotnetJsPath!, label); - - if (type != NativeFilesType.FromRuntimePack) + foreach (string nativeFilename in new[] { "dotnet.native.wasm", "dotnet.native.js" }) { - // check that the files are *not* from runtime pack - AssertNotSameFile(Path.Combine(s_buildEnv.GetRuntimeNativeDir(targetFramework), "dotnet.native.wasm"), Path.Combine(binFrameworkDir, "dotnet.native.wasm"), label); - AssertNotSameFile(Path.Combine(s_buildEnv.GetRuntimeNativeDir(targetFramework), "dotnet.native.js"), dotnetJsPath!, label); + // For any *type*, check against the expected path + AssertSameFile(Path.Combine(srcDirForNativeFileToCompareAgainst, nativeFilename), + dotnetFiles[nativeFilename].ActualPath, + label); + + if (type != NativeFilesType.FromRuntimePack) + { + // Confirm that it doesn't match the file from the runtime pack + AssertNotSameFile(Path.Combine(runtimeNativeDir, nativeFilename), + dotnetFiles[nativeFilename].ActualPath, + label); + } } } @@ -667,44 +699,37 @@ static void AssertRuntimePackPath(string buildOutput, string targetFramework) throw new XunitException($"Runtime pack path doesn't match.{Environment.NewLine}Expected: '{expectedRuntimePackDir}'{Environment.NewLine}Actual: '{actualPath}'"); } - protected static void AssertBasicAppBundle(string bundleDir, - string projectName, - string config, - string mainJS, - bool hasV8Script, - string targetFramework, - GlobalizationMode? globalizationMode, - string predefinedIcudt = "", - bool dotnetWasmFromRuntimePack = true, - bool useWebcil = true, - bool isBrowserProject = true) + private void AssertBasicAppBundle(AssertTestMainJsAppBundleOptions options) { + new TestMainJsProjectProvider(_projectDir!, _testOutput) + .FindAndAssertDotnetFiles( + Path.Combine(options.BundleDir, "_framework"), + isPublish: options.IsPublish, + expectFingerprintOnDotnetJs: false, + runtimeType: RuntimeVariant.SingleThreaded); + var filesToExist = new List() { - mainJS, - "_framework/dotnet.native.wasm", + options.MainJS, "_framework/blazor.boot.json", - "_framework/dotnet.js", "_framework/dotnet.js.map", - "_framework/dotnet.native.js", - "_framework/dotnet.runtime.js", "_framework/dotnet.runtime.js.map", }; - if (isBrowserProject) + if (options.IsBrowserProject) filesToExist.Add("index.html"); - AssertFilesExist(bundleDir, filesToExist); + AssertFilesExist(options.BundleDir, filesToExist); - AssertFilesExist(bundleDir, new[] { "run-v8.sh" }, expectToExist: hasV8Script); + AssertFilesExist(options.BundleDir, new[] { "run-v8.sh" }, expectToExist: options.HasV8Script); AssertIcuAssets(); - string managedDir = Path.Combine(bundleDir, "_framework"); + string managedDir = Path.Combine(options.BundleDir, "_framework"); string bundledMainAppAssembly = - useWebcil ? $"{projectName}{WebcilInWasmExtension}" : $"{projectName}.dll"; + options.UseWebcil ? $"{options.ProjectName}{WebcilInWasmExtension}" : $"{options.ProjectName}.dll"; AssertFilesExist(managedDir, new[] { bundledMainAppAssembly }); - bool is_debug = config == "Debug"; + bool is_debug = options.Config == "Debug"; if (is_debug) { // Use cecil to check embedded pdb? @@ -718,8 +743,6 @@ protected static void AssertBasicAppBundle(string bundleDir, //} } - AssertDotNetWasmJs(bundleDir, fromRuntimePack: dotnetWasmFromRuntimePack, targetFramework); - void AssertIcuAssets() { bool expectEFIGS = false; @@ -727,7 +750,7 @@ void AssertIcuAssets() bool expectNOCJK = false; bool expectFULL = false; bool expectHYBRID = false; - switch (globalizationMode) + switch (options.GlobalizationMode) { case GlobalizationMode.Invariant: break; @@ -738,11 +761,11 @@ void AssertIcuAssets() expectHYBRID = true; break; case GlobalizationMode.PredefinedIcu: - if (string.IsNullOrEmpty(predefinedIcudt)) + if (string.IsNullOrEmpty(options.PredefinedIcudt)) throw new ArgumentException("WasmBuildTest is invalid, value for predefinedIcudt is required when GlobalizationMode=PredefinedIcu."); - AssertFilesExist(bundleDir, new[] { Path.Combine("_framework", predefinedIcudt) }, expectToExist: true); + 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 (predefinedIcudt) + switch (options.PredefinedIcudt) { case "icudt.dat": expectFULL = true; @@ -766,7 +789,7 @@ void AssertIcuAssets() break; } - var frameworkDir = Path.Combine(bundleDir, "_framework"); + var frameworkDir = Path.Combine(options.BundleDir, "_framework"); AssertFilesExist(frameworkDir, new[] { "icudt.dat" }, expectToExist: expectFULL); AssertFilesExist(frameworkDir, new[] { "icudt_EFIGS.dat" }, expectToExist: expectEFIGS); AssertFilesExist(frameworkDir, new[] { "icudt_CJK.dat" }, expectToExist: expectCJK); @@ -775,19 +798,6 @@ void AssertIcuAssets() } } - protected static void AssertDotNetWasmJs(string bundleDir, bool fromRuntimePack, string targetFramework) - { - AssertFile(Path.Combine(s_buildEnv.GetRuntimeNativeDir(targetFramework), "dotnet.native.wasm"), - Path.Combine(bundleDir, "_framework/dotnet.native.wasm"), - "Expected dotnet.native.wasm to be same as the runtime pack", - same: fromRuntimePack); - - AssertFile(Path.Combine(s_buildEnv.GetRuntimeNativeDir(targetFramework), "dotnet.native.js"), - Path.Combine(bundleDir, "_framework/dotnet.native.js"), - "Expected dotnet.native.js to be same as the runtime pack", - same: fromRuntimePack); - } - protected static void AssertDotNetJsSymbols(string bundleDir, bool fromRuntimePack, string targetFramework) => AssertFile(Path.Combine(s_buildEnv.GetRuntimeNativeDir(targetFramework), "dotnet.native.js.symbols"), Path.Combine(bundleDir, "_framework/dotnet.native.js.symbols"), @@ -841,109 +851,45 @@ protected static void AssertFile(string file0, string file1, string? label = nul return result; } - protected void AssertBlazorBundle(string config, bool isPublish, bool dotnetWasmFromRuntimePack, string targetFramework = DefaultTargetFrameworkForBlazor, string? binFrameworkDir = null, bool expectFingerprintOnDotnetJs = false) - { - binFrameworkDir ??= FindBlazorBinFrameworkDir(config, isPublish, targetFramework); - - AssertBlazorBootJson(config, isPublish, targetFramework != DefaultTargetFrameworkForBlazor, targetFramework, binFrameworkDir: binFrameworkDir); - AssertFile(Path.Combine(s_buildEnv.GetRuntimeNativeDir(targetFramework), "dotnet.native.wasm"), - Path.Combine(binFrameworkDir, "dotnet.native.wasm"), - "Expected dotnet.native.wasm to be same as the runtime pack", - same: dotnetWasmFromRuntimePack); - - string? dotnetJsPath = Directory.EnumerateFiles(binFrameworkDir, "dotnet.native.*.js").FirstOrDefault(); - Assert.True(dotnetJsPath != null, $"Could not find blazor's dotnet*js in {binFrameworkDir}"); - - AssertFile(Path.Combine(s_buildEnv.GetRuntimeNativeDir(targetFramework), "dotnet.native.js"), - dotnetJsPath!, - "Expected dotnet.native.js to be same as the runtime pack", - same: dotnetWasmFromRuntimePack); - - string bootConfigPath = Path.Combine(binFrameworkDir, "blazor.boot.json"); - Assert.True(File.Exists(bootConfigPath), $"Expected to find '{bootConfigPath}'"); - - using (var bootConfigContent = File.OpenRead(bootConfigPath)) - { - var bootConfig = ParseBootData(bootConfigContent); - var dotnetJsEntries = bootConfig.resources.runtime.Keys.Where(k => k.StartsWith("dotnet.") && k.EndsWith(".js")).ToArray(); - - void AssertFileExists(string fileName) - { - string absolutePath = Path.Combine(binFrameworkDir, fileName); - Assert.True(File.Exists(absolutePath), $"Expected to find '{absolutePath}'"); - } - - string versionHashRegex = @"\.(?.+)\.(?[a-zA-Z0-9]+)\."; - - Assert.Collection( - dotnetJsEntries.OrderBy(f => f), - item => - { - if (expectFingerprintOnDotnetJs) - Assert.Matches($"dotnet{versionHashRegex}js", item); - else - Assert.Equal("dotnet.js", item); - - AssertFileExists(item); - }, - item => { Assert.Matches($"dotnet\\.native{versionHashRegex}js", item); AssertFileExists(item); }, - item => { Assert.Matches($"dotnet\\.runtime{versionHashRegex}js", item); AssertFileExists(item); } - ); - } - } - - private static BootJsonData ParseBootData(Stream stream) - { - stream.Position = 0; - var serializer = new DataContractJsonSerializer( - typeof(BootJsonData), - new DataContractJsonSerializerSettings { UseSimpleDictionaryFormat = true }); - - var config = (BootJsonData?)serializer.ReadObject(stream); - Assert.NotNull(config); - return config; - } - - protected void AssertBlazorBootJson(string config, bool isPublish, bool isNet7AndBelow, string targetFramework = DefaultTargetFrameworkForBlazor, string? binFrameworkDir = null) + protected void AssertBlazorBundle( + BlazorBuildOptions options, + bool isPublish, + string? binFrameworkDir = null) { - binFrameworkDir ??= FindBlazorBinFrameworkDir(config, isPublish, targetFramework); - - string bootJsonPath = Path.Combine(binFrameworkDir, "blazor.boot.json"); - Assert.True(File.Exists(bootJsonPath), $"Expected to find {bootJsonPath}"); - - string bootJson = File.ReadAllText(bootJsonPath); - var bootJsonNode = JsonNode.Parse(bootJson); - var runtimeObj = bootJsonNode?["resources"]?["runtime"]?.AsObject(); - Assert.NotNull(runtimeObj); - - string msgPrefix = $"[{(isPublish ? "publish" : "build")}]"; - Assert.True(runtimeObj!.Where(kvp => kvp.Key == (isNet7AndBelow ? "dotnet.wasm" : "dotnet.native.wasm")).Any(), $"{msgPrefix} Could not find dotnet.native.wasm entry in blazor.boot.json"); - Assert.True(runtimeObj!.Where(kvp => kvp.Key.StartsWith("dotnet.", StringComparison.OrdinalIgnoreCase) && - kvp.Key.EndsWith(".js", StringComparison.OrdinalIgnoreCase)).Any(), - $"{msgPrefix} Could not find dotnet.*js in {bootJson}"); + if (options.TargetFramework is null) + options = options with { TargetFramework = DefaultTargetFrameworkForBlazor }; + + AssertBlazorDotNetNativeFiles(options.ExpectedFileType, + options.Config, + forPublish: isPublish, + targetFramework: options.TargetFramework, + expectFingerprintOnDotnetJs: options.ExpectFingerprintOnDotnetJs, + runtimeType: options.RuntimeType); + + AssertBlazorBootJson(config: options.Config, + isPublish: isPublish, + targetFramework: options.TargetFramework, + expectFingerprintOnDotnetJs: options.ExpectFingerprintOnDotnetJs, + runtimeType: options.RuntimeType); } - protected string FindBlazorBinFrameworkDir(string config, bool forPublish, string framework = DefaultTargetFrameworkForBlazor) + protected void AssertBlazorBootJson( + string config, + bool isPublish, + string targetFramework = DefaultTargetFrameworkForBlazor, + bool expectFingerprintOnDotnetJs = false, + RuntimeVariant runtimeType = RuntimeVariant.SingleThreaded) { - string basePath = Path.Combine(_projectDir!, "bin", config, framework); - if (forPublish) - basePath = FindSubDirIgnoringCase(basePath, "publish"); - - return Path.Combine(basePath, "wwwroot", "_framework"); + new BlazorWasmProjectProvider(_projectDir!, _testOutput) + .AssertBlazorBootJson(binFrameworkDir: FindBlazorBinFrameworkDir(config, isPublish, targetFramework), + isPublish: isPublish, + expectFingerprintOnDotnetJs: expectFingerprintOnDotnetJs, + runtimeType: runtimeType); } - private 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 string FindBlazorBinFrameworkDir(string config, bool forPublish, string framework = DefaultTargetFrameworkForBlazor) + => new BlazorWasmProjectProvider(_projectDir!, _testOutput) + .FindBlazorBinFrameworkDir(config, forPublish, framework); protected string GetBinDir(string config, string targetFramework = DefaultTargetFramework, string? baseDir = null) { @@ -1032,8 +978,6 @@ public async Task BlazorRunTest(string runArgs, void OnConsoleMessage(IConsoleMessage msg) { - if (EnvironmentVariables.ShowBuildOutput) - Console.WriteLine($"[{msg.Type}] {msg.Text}"); _testOutput.WriteLine($"[{msg.Type}] {msg.Text}"); onConsoleMessage?.Invoke(msg); @@ -1052,10 +996,9 @@ public static (int exitCode, string buildOutput) RunProcess(string path, IDictionary? envVars = null, string? workingDir = null, string? label = null, - bool logToXUnit = true, int? timeoutMs = null) { - var t = RunProcessAsync(path, _testOutput, args, envVars, workingDir, label, logToXUnit, timeoutMs); + var t = RunProcessAsync(path, _testOutput, args, envVars, workingDir, label, timeoutMs); t.Wait(); return t.Result; } @@ -1066,7 +1009,6 @@ public static (int exitCode, string buildOutput) RunProcess(string path, IDictionary? envVars = null, string? workingDir = null, string? label = null, - bool logToXUnit = true, int? timeoutMs = null) { _testOutput.WriteLine($"Running {path} {args}"); @@ -1167,14 +1109,12 @@ void LogData(string label, string? message) { lock (syncObj) { - if (logToXUnit && message != null) + if (message != null) { _testOutput.WriteLine($"{label} {message}"); } outputBuilder.AppendLine($"{label} {message}"); } - if (EnvironmentVariables.ShowBuildOutput) - Console.WriteLine($"{label} {message}"); } } @@ -1277,6 +1217,16 @@ protected void AssertSubstring(string substring, string full, bool contains) else Assert.DoesNotContain(substring, full); } + + public static void AssertEqual(object expected, object actual, string label) + { + if (expected?.Equals(actual) == true) + return; + + throw new AssertActualExpectedException( + expected, actual, + $"[{label}]\n"); + } } public record BuildArgs(string ProjectName, @@ -1288,27 +1238,6 @@ public record BuildProduct(string ProjectDir, string LogFile, bool Result, strin internal record FileStat(bool Exists, DateTime LastWriteTimeUtc, long Length, string FullPath); internal record BuildPaths(string ObjWasmDir, string ObjDir, string BinDir, string BundleDir); - public record BuildProjectOptions - ( - Action? InitProject = null, - bool? DotnetWasmFromRuntimePack = null, - GlobalizationMode? GlobalizationMode = null, - string? PredefinedIcudt = null, - bool UseCache = true, - bool ExpectSuccess = true, - bool AssertAppBundle = true, - bool CreateProject = true, - bool Publish = true, - bool BuildOnlyAfterPublish = true, - bool HasV8Script = true, - string? Verbosity = null, - string? Label = null, - string? TargetFramework = null, - string? MainJS = null, - bool IsBrowserProject = true, - IDictionary? ExtraBuildEnvironmentVariables = null - ); - public record BlazorBuildOptions ( string Id, @@ -1317,7 +1246,8 @@ public record BlazorBuildOptions string TargetFramework = BuildTestBase.DefaultTargetFrameworkForBlazor, bool WarnAsError = true, bool ExpectRelinkDirWhenPublishing = false, - bool ExpectFingerprintOnDotnetJs = false + bool ExpectFingerprintOnDotnetJs = false, + RuntimeVariant RuntimeType = RuntimeVariant.SingleThreaded ); public enum GlobalizationMode diff --git a/src/mono/wasm/Wasm.Build.Tests/Common/BuildEnvironment.cs b/src/mono/wasm/Wasm.Build.Tests/Common/BuildEnvironment.cs index cb24f659c96c9..4dddc0c2ed2d4 100644 --- a/src/mono/wasm/Wasm.Build.Tests/Common/BuildEnvironment.cs +++ b/src/mono/wasm/Wasm.Build.Tests/Common/BuildEnvironment.cs @@ -139,10 +139,14 @@ public BuildEnvironment() // FIXME: error checks public string GetRuntimePackVersion(string tfm = BuildTestBase.DefaultTargetFramework) => s_runtimePackVersions[tfm]; - public string GetRuntimePackDir(string tfm = BuildTestBase.DefaultTargetFramework) - => Path.Combine(WorkloadPacksDir, $"Microsoft.NETCore.App.Runtime.Mono.{DefaultRuntimeIdentifier}", GetRuntimePackVersion(tfm)); - public string GetRuntimeNativeDir(string tfm = BuildTestBase.DefaultTargetFramework) - => Path.Combine(GetRuntimePackDir(tfm), "runtimes", DefaultRuntimeIdentifier, "native"); + public string GetRuntimePackDir(string tfm = BuildTestBase.DefaultTargetFramework, RuntimeVariant runtimeType = RuntimeVariant.SingleThreaded) + => Path.Combine(WorkloadPacksDir, + runtimeType is RuntimeVariant.SingleThreaded + ? $"Microsoft.NETCore.App.Runtime.Mono.{DefaultRuntimeIdentifier}" + : $"Microsoft.NETCore.App.Runtime.Mono.multithread.{DefaultRuntimeIdentifier}", + GetRuntimePackVersion(tfm)); + public string GetRuntimeNativeDir(string tfm = BuildTestBase.DefaultTargetFramework, RuntimeVariant runtimeType = RuntimeVariant.SingleThreaded) + => Path.Combine(GetRuntimePackDir(tfm, runtimeType), "runtimes", DefaultRuntimeIdentifier, "native"); protected static string s_directoryBuildPropsForWorkloads = File.ReadAllText(Path.Combine(TestDataPath, "Workloads.Directory.Build.props")); protected static string s_directoryBuildTargetsForWorkloads = File.ReadAllText(Path.Combine(TestDataPath, "Workloads.Directory.Build.targets")); diff --git a/src/mono/wasm/Wasm.Build.Tests/Common/RuntimeVariant.cs b/src/mono/wasm/Wasm.Build.Tests/Common/RuntimeVariant.cs new file mode 100644 index 0000000000000..c060f42520588 --- /dev/null +++ b/src/mono/wasm/Wasm.Build.Tests/Common/RuntimeVariant.cs @@ -0,0 +1,8 @@ +// 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 RuntimeVariant { SingleThreaded, MultiThreaded }; diff --git a/src/mono/wasm/Wasm.Build.Tests/Common/TestOutputWrapper.cs b/src/mono/wasm/Wasm.Build.Tests/Common/TestOutputWrapper.cs new file mode 100644 index 0000000000000..a28657fa7bf0e --- /dev/null +++ b/src/mono/wasm/Wasm.Build.Tests/Common/TestOutputWrapper.cs @@ -0,0 +1,26 @@ +// 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 Xunit.Abstractions; + +#nullable enable + +namespace Wasm.Build.Tests; + +public class TestOutputWrapper(ITestOutputHelper baseOutput) : ITestOutputHelper +{ + public void WriteLine(string message) + { + baseOutput.WriteLine(message); + if (EnvironmentVariables.ShowBuildOutput) + Console.WriteLine(message); + } + + public void WriteLine(string format, params object[] args) + { + baseOutput.WriteLine(format, args); + if (EnvironmentVariables.ShowBuildOutput) + Console.WriteLine(format, args); + } +} diff --git a/src/mono/wasm/Wasm.Build.Tests/Common/ToolCommand.cs b/src/mono/wasm/Wasm.Build.Tests/Common/ToolCommand.cs index 2fae80aa4bdd3..a308c7ba668cb 100644 --- a/src/mono/wasm/Wasm.Build.Tests/Common/ToolCommand.cs +++ b/src/mono/wasm/Wasm.Build.Tests/Common/ToolCommand.cs @@ -115,8 +115,6 @@ private async Task ExecuteAsyncInternal(string executable, string string msg = $"[{_label}] {e.Data}"; output.Add(msg); _testOutput.WriteLine(msg); - if (EnvironmentVariables.ShowBuildOutput) - Console.WriteLine(msg); ErrorDataReceived?.Invoke(s, e); }; @@ -128,8 +126,6 @@ private async Task ExecuteAsyncInternal(string executable, string string msg = $"[{_label}] {e.Data}"; output.Add(msg); _testOutput.WriteLine(msg); - if (EnvironmentVariables.ShowBuildOutput) - Console.WriteLine(msg); OutputDataReceived?.Invoke(s, e); }; diff --git a/src/mono/wasm/Wasm.Build.Tests/DotNetFileName.cs b/src/mono/wasm/Wasm.Build.Tests/DotNetFileName.cs new file mode 100644 index 0000000000000..42fd8873e78f5 --- /dev/null +++ b/src/mono/wasm/Wasm.Build.Tests/DotNetFileName.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 sealed record DotNetFileName +( + string ExpectedFilename, + string? Version, + string? Hash, + string ActualPath +); diff --git a/src/mono/wasm/Wasm.Build.Tests/ProjectProviderBase.cs b/src/mono/wasm/Wasm.Build.Tests/ProjectProviderBase.cs new file mode 100644 index 0000000000000..5dd4240c00435 --- /dev/null +++ b/src/mono/wasm/Wasm.Build.Tests/ProjectProviderBase.cs @@ -0,0 +1,170 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable enable + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using Xunit; +using Xunit.Abstractions; +using Xunit.Sdk; + +namespace Wasm.Build.Tests; + +public abstract class ProjectProviderBase(string projectDir, ITestOutputHelper _testOutput) +{ + protected const string s_dotnetVersionHashRegex = @"\.(?.+)\.(?[a-zA-Z0-9]+)\."; + private static string[] s_dotnetExtensionsToIgnore = new[] + { + ".gz", + ".br", + ".symbols" + }; + + public string ProjectDir { get; } = projectDir; + + public IReadOnlyDictionary FindAndAssertDotnetFiles( + string dir, + bool isPublish, + bool expectFingerprintOnDotnetJs, + RuntimeVariant runtimeType) + { + return FindAndAssertDotnetFiles(dir: dir, + expectFingerprintOnDotnetJs: expectFingerprintOnDotnetJs, + superSet: GetAllKnownDotnetFilesToFingerprintMap(runtimeType), + expected: GetDotNetFilesExpectedSet(runtimeType, isPublish)); + } + + protected abstract IReadOnlyDictionary GetAllKnownDotnetFilesToFingerprintMap(RuntimeVariant runtimeType); + protected abstract IReadOnlySet GetDotNetFilesExpectedSet(RuntimeVariant runtimeType, bool isPublish); + + public IReadOnlyDictionary FindAndAssertDotnetFiles( + string dir, + bool expectFingerprintOnDotnetJs, + IReadOnlyDictionary superSet, + IReadOnlySet? expected) + { + var actual = new SortedDictionary(); + + IList dotnetFiles = Directory.EnumerateFiles(dir, + "dotnet.*", + SearchOption.TopDirectoryOnly) + .Order() + .ToList(); + foreach ((string expectedFilename, bool expectFingerprint) in superSet.OrderByDescending(kvp => kvp.Key)) + { + string prefix = Path.GetFileNameWithoutExtension(expectedFilename); + string extension = Path.GetExtension(expectedFilename).Substring(1); + + dotnetFiles = dotnetFiles + .Where(actualFile => + { + if (s_dotnetExtensionsToIgnore.Contains(Path.GetExtension(actualFile))) + return false; + + string actualFilename = Path.GetFileName(actualFile); + _testOutput.WriteLine($"Comparing {expectedFilename} with {actualFile}, expectFingerprintOnDotnetJs: {expectFingerprintOnDotnetJs}, expectFingerprint: {expectFingerprint}"); + if (ShouldCheckFingerprint(expectedFilename: expectedFilename, + expectFingerprintOnDotnetJs: expectFingerprintOnDotnetJs, + expectFingerprintForThisFile: expectFingerprint)) + { + string pattern = $"^{prefix}{s_dotnetVersionHashRegex}{extension}$"; + var match = Regex.Match(actualFilename, pattern); + if (!match.Success) + return true; + + actual[expectedFilename] = new(ExpectedFilename: expectedFilename, + Version: match.Groups[1].Value, + Hash: match.Groups[2].Value, + ActualPath: actualFile); + } + else + { + if (actualFilename != expectedFilename) + return true; + + actual[expectedFilename] = new(ExpectedFilename: expectedFilename, + Version: null, + Hash: null, + ActualPath: actualFile); + } + + return false; + }).ToList(); + } + + _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)}"); + } + + if (expected is not null) + AssertDotNetFilesSet(expected, superSet, actual, expectFingerprintOnDotnetJs); + return actual; + } + + public void AssertDotNetFilesSet( + IReadOnlySet expected, + IReadOnlyDictionary superSet, + IDictionary actual, + bool expectFingerprintOnDotnetJs) + { + foreach (string expectedFilename in expected) + { + bool expectFingerprint = superSet[expectedFilename]; + + Assert.True(actual.ContainsKey(expectedFilename), $"Could not find {expectedFilename} in {string.Join(", ", actual.Keys)}"); + + // Check that the version and hash are present or not present as expected + if (ShouldCheckFingerprint(expectedFilename: expectedFilename, + expectFingerprintOnDotnetJs: expectFingerprintOnDotnetJs, + expectFingerprintForThisFile: expectFingerprint)) + { + if (string.IsNullOrEmpty(actual[expectedFilename].Version)) + throw new XunitException($"Expected version in filename: {actual[expectedFilename].ActualPath}"); + if (string.IsNullOrEmpty(actual[expectedFilename].Hash)) + throw new XunitException($"Expected hash in filename: {actual[expectedFilename].ActualPath}"); + } + else + { + if (!string.IsNullOrEmpty(actual[expectedFilename].Version)) + throw new XunitException($"Expected no version in filename: {actual[expectedFilename].ActualPath}"); + if (!string.IsNullOrEmpty(actual[expectedFilename].Hash)) + throw new XunitException($"Expected no hash in filename: {actual[expectedFilename].ActualPath}"); + } + } + + if (expected.Count < actual.Count) + { + 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}"); + } + } + + 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; +} diff --git a/src/mono/wasm/Wasm.Build.Tests/TestMainJsProjectProvider.cs b/src/mono/wasm/Wasm.Build.Tests/TestMainJsProjectProvider.cs new file mode 100644 index 0000000000000..ac24e587f2662 --- /dev/null +++ b/src/mono/wasm/Wasm.Build.Tests/TestMainJsProjectProvider.cs @@ -0,0 +1,65 @@ +// 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; +using System.Collections.Generic; +using Xunit.Abstractions; + +namespace Wasm.Build.Tests; + +public class TestMainJsProjectProvider(string projectDir, ITestOutputHelper testOutput) + : ProjectProviderBase(projectDir, testOutput) +{ + // no fingerprinting + protected override IReadOnlyDictionary GetAllKnownDotnetFilesToFingerprintMap(RuntimeVariant runtimeType) + => new SortedDictionary() + { + { "dotnet.js", false }, + { "dotnet.js.map", false }, + { "dotnet.native.js", 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) + { + SortedSet? res = null; + if (runtimeType is RuntimeVariant.SingleThreaded) + { + res = new SortedSet() + { + "dotnet.js", + "dotnet.native.wasm", + "dotnet.native.js", + "dotnet.runtime.js", + }; + + res.Add("dotnet.js.map"); + res.Add("dotnet.runtime.js.map"); + } + + if (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.map"); + res.Add("dotnet.runtime.js.map"); + res.Add("dotnet.native.worker.js.map"); + } + } + + return res ?? throw new ArgumentException($"Unknown runtime type: {runtimeType}"); + } +} diff --git a/src/mono/wasm/Wasm.Build.Tests/Wasm.Build.Tests.csproj b/src/mono/wasm/Wasm.Build.Tests/Wasm.Build.Tests.csproj index 0ff15466c29ef..fb09aa484eea2 100644 --- a/src/mono/wasm/Wasm.Build.Tests/Wasm.Build.Tests.csproj +++ b/src/mono/wasm/Wasm.Build.Tests/Wasm.Build.Tests.csproj @@ -45,11 +45,6 @@ - - <_SdkWithWorkloadForTestingDirName>$([System.IO.Path]::GetDirectoryName($(SdkWithWorkloadForTestingPath))) - <_SdkWithWorkloadForTestingDirName>$([System.IO.Path]::GetFilename($(_SdkWithWorkloadForTestingDirName))) - - @@ -72,12 +67,18 @@ <_XUnitTraitArg Condition="'$(TestUsingWorkloads)' != 'true'">-trait category=no-workload + + <_SdkPathForLocalTesting Condition="'$(TestUsingWorkloads)' == 'true'">$([System.IO.Path]::GetDirectoryName($(SdkWithWorkloadForTestingPath))) + <_SdkPathForLocalTesting Condition="'$(TestUsingWorkloads)' != 'true'">$([System.IO.Path]::GetDirectoryName($(SdkWithNoWorkloadForTestingPath))) + + <_SdkPathForLocalTesting>$([System.IO.Path]::GetFilename($(_SdkPathForLocalTesting))) + - - + + diff --git a/src/mono/wasm/Wasm.Build.Tests/WasmSdkBasedProjectProvider.cs b/src/mono/wasm/Wasm.Build.Tests/WasmSdkBasedProjectProvider.cs new file mode 100644 index 0000000000000..6ee5a6ca253f5 --- /dev/null +++ b/src/mono/wasm/Wasm.Build.Tests/WasmSdkBasedProjectProvider.cs @@ -0,0 +1,48 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using Xunit.Abstractions; + +#nullable enable + +namespace Wasm.Build.Tests; + +public class WasmSdkBasedProjectProvider(string projectDir, ITestOutputHelper _testOutput) + : ProjectProviderBase(projectDir, _testOutput) +{ + protected override IReadOnlyDictionary GetAllKnownDotnetFilesToFingerprintMap(RuntimeVariant runtimeType) + => new SortedDictionary() + { + { "dotnet.js", false }, + { "dotnet.js.map", false }, + { "dotnet.native.js", true }, + { "dotnet.native.wasm", false }, + { "dotnet.native.worker.js", true }, + { "dotnet.runtime.js", true }, + { "dotnet.runtime.js.map", false } + }; + + protected override IReadOnlySet GetDotNetFilesExpectedSet(RuntimeVariant runtimeType, bool isPublish) + { + SortedSet res = new() + { + "dotnet.js", + "dotnet.native.wasm", + "dotnet.native.js", + "dotnet.runtime.js", + }; + if (runtimeType is RuntimeVariant.MultiThreaded) + { + res.Add("dotnet.native.worker.js"); + } + + if (!isPublish) + { + res.Add("dotnet.js.map"); + res.Add("dotnet.runtime.js.map"); + } + + return res; + } +} diff --git a/src/mono/wasm/Wasm.Build.Tests/WasmTemplateTests.cs b/src/mono/wasm/Wasm.Build.Tests/WasmTemplateTests.cs index 88af535d0895d..088b1134a8540 100644 --- a/src/mono/wasm/Wasm.Build.Tests/WasmTemplateTests.cs +++ b/src/mono/wasm/Wasm.Build.Tests/WasmTemplateTests.cs @@ -96,7 +96,7 @@ public void BrowserBuildThenPublish(string config) var buildArgs = new BuildArgs(projectName, config, false, id, null); buildArgs = ExpandBuildArgs(buildArgs); - BuildProject(buildArgs, + BuildTemplateProject(buildArgs, id: id, new BuildProjectOptions( DotnetWasmFromRuntimePack: true, @@ -117,7 +117,7 @@ public void BrowserBuildThenPublish(string config) _testOutput.WriteLine($"{Environment.NewLine}Publishing with no changes ..{Environment.NewLine}"); bool expectRelinking = config == "Release"; - BuildProject(buildArgs, + BuildTemplateProject(buildArgs, id: id, new BuildProjectOptions( DotnetWasmFromRuntimePack: !expectRelinking, @@ -145,7 +145,7 @@ public void ConsoleBuildThenPublish(string config) var buildArgs = new BuildArgs(projectName, config, false, id, null); buildArgs = ExpandBuildArgs(buildArgs); - BuildProject(buildArgs, + BuildTemplateProject(buildArgs, id: id, new BuildProjectOptions( DotnetWasmFromRuntimePack: true, @@ -173,7 +173,7 @@ public void ConsoleBuildThenPublish(string config) _testOutput.WriteLine($"{Environment.NewLine}Publishing with no changes ..{Environment.NewLine}"); bool expectRelinking = config == "Release"; - BuildProject(buildArgs, + BuildTemplateProject(buildArgs, id: id, new BuildProjectOptions( DotnetWasmFromRuntimePack: !expectRelinking, @@ -217,7 +217,7 @@ private void ConsoleBuildAndRun(string config, bool relinking, string extraNewAr var buildArgs = new BuildArgs(projectName, config, false, id, null); buildArgs = ExpandBuildArgs(buildArgs); - BuildProject(buildArgs, + BuildTemplateProject(buildArgs, id: id, new BuildProjectOptions( DotnetWasmFromRuntimePack: !relinking, @@ -391,7 +391,7 @@ public void ConsolePublishAndRun(string config, bool aot, bool relinking) buildArgs = ExpandBuildArgs(buildArgs); bool expectRelinking = config == "Release" || aot || relinking; - BuildProject(buildArgs, + BuildTemplateProject(buildArgs, id: id, new BuildProjectOptions( DotnetWasmFromRuntimePack: !expectRelinking, diff --git a/src/mono/wasm/wasm.code-workspace b/src/mono/wasm/wasm.code-workspace index 62834e391e2fb..80a990415ad60 100644 --- a/src/mono/wasm/wasm.code-workspace +++ b/src/mono/wasm/wasm.code-workspace @@ -13,6 +13,7 @@ "settings": { "omnisharp.enableMsBuildLoadProjectsOnDemand": true, "omnisharp.defaultLaunchSolution": "${workspaceFolder}sln/WasmBuild.sln", - "omnisharp.enableRoslynAnalyzers": true + "omnisharp.enableRoslynAnalyzers": true, + "cSpell.enabled": false } }