[wasm] Wasm.Build.Tests - some refactoring, and rationalizing (#88357)
authorAnkit Jain <radical@gmail.com>
Wed, 12 Jul 2023 06:09:40 +0000 (02:09 -0400)
committerGitHub <noreply@github.com>
Wed, 12 Jul 2023 06:09:40 +0000 (02:09 -0400)
19 files changed:
src/mono/wasm/Wasm.Build.Tests/AssertTestMainJsAppBundleOptions.cs [new file with mode: 0644]
src/mono/wasm/Wasm.Build.Tests/Blazor/AppsettingsTests.cs
src/mono/wasm/Wasm.Build.Tests/Blazor/BuildPublishTests.cs
src/mono/wasm/Wasm.Build.Tests/Blazor/MiscTests.cs
src/mono/wasm/Wasm.Build.Tests/Blazor/MiscTests2.cs
src/mono/wasm/Wasm.Build.Tests/BlazorWasmProjectProvider.cs [new file with mode: 0644]
src/mono/wasm/Wasm.Build.Tests/BuildProjectOptions.cs [new file with mode: 0644]
src/mono/wasm/Wasm.Build.Tests/BuildTestBase.cs
src/mono/wasm/Wasm.Build.Tests/Common/BuildEnvironment.cs
src/mono/wasm/Wasm.Build.Tests/Common/RuntimeVariant.cs [new file with mode: 0644]
src/mono/wasm/Wasm.Build.Tests/Common/TestOutputWrapper.cs [new file with mode: 0644]
src/mono/wasm/Wasm.Build.Tests/Common/ToolCommand.cs
src/mono/wasm/Wasm.Build.Tests/DotNetFileName.cs [new file with mode: 0644]
src/mono/wasm/Wasm.Build.Tests/ProjectProviderBase.cs [new file with mode: 0644]
src/mono/wasm/Wasm.Build.Tests/TestMainJsProjectProvider.cs [new file with mode: 0644]
src/mono/wasm/Wasm.Build.Tests/Wasm.Build.Tests.csproj
src/mono/wasm/Wasm.Build.Tests/WasmSdkBasedProjectProvider.cs [new file with mode: 0644]
src/mono/wasm/Wasm.Build.Tests/WasmTemplateTests.cs
src/mono/wasm/wasm.code-workspace

diff --git a/src/mono/wasm/Wasm.Build.Tests/AssertTestMainJsAppBundleOptions.cs b/src/mono/wasm/Wasm.Build.Tests/AssertTestMainJsAppBundleOptions.cs
new file mode 100644 (file)
index 0000000..125c5c6
--- /dev/null
@@ -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
+);
index 89ecb5b..767099b 100644 (file)
@@ -54,4 +54,4 @@ public class AppsettingsTests : BuildTestBase
         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
+}
index c94d1dd..7ccb136 100644 (file)
@@ -34,11 +34,11 @@ public class BuildPublishTests : BuildTestBase
 
         // 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]
index 9a094d9..fd7a96b 100644 (file)
@@ -39,8 +39,12 @@ public class MiscTests : BuildTestBase
 
         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)
         {
index 467a235..d23fa9e 100644 (file)
@@ -66,70 +66,4 @@ public class MiscTests2 : BuildTestBase
                                                     $"-bl:{publishLogPath}",
                                                     $"-p:Configuration={config}");
     }
-
-    [Theory]
-    [InlineData("Debug")]
-    [InlineData("Release")]
-    public void Net50Projects_NativeReference(string config)
-        => BuildNet50Project(config, aot: false, expectError: true, @"<NativeFileReference Include=""native-lib.o"" />");
-
-    public static TheoryData<string, bool, bool> 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 = @"<Project>
-        <Target Name=""PrintAllProjects"" BeforeTargets=""Build"">
-                <Message Text=""** UsingBrowserRuntimeWorkload: '$(UsingBrowserRuntimeWorkload)'"" Importance=""High"" />
-            </Target>
-        </Project>";
-
-        File.WriteAllText(Path.Combine(_projectDir!, "Directory.Build.props"), "<Project />");
-        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 (file)
index 0000000..7432a78
--- /dev/null
@@ -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<string, Action<string>>();
+        IReadOnlySet<string> 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 (file)
index 0000000..77a4b2b
--- /dev/null
@@ -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<string, string>? ExtraBuildEnvironmentVariables = null
+);
index 433af91..e9d15fe 100644 (file)
@@ -10,7 +10,6 @@ using System.IO;
 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;
 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 @@ namespace Wasm.Build.Tests
         {
             _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<IEnumerable<object?>> ConfigWithAOTData(bool aot, string? config = null, string? extraArgs = null)
         {
             if (extraArgs == null)
@@ -142,7 +128,6 @@ namespace Wasm.Build.Tests
             }
         }
 
-
         protected string RunAndTestWasmApp(BuildArgs buildArgs,
                                            RunHost host,
                                            string id,
@@ -361,6 +346,53 @@ namespace Wasm.Build.Tests
             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 @@ namespace Wasm.Build.Tests
                     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 @@ namespace Wasm.Build.Tests
                 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 @@ namespace Wasm.Build.Tests
         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 @@ namespace Wasm.Build.Tests
 
                 // 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 @@ namespace Wasm.Build.Tests
             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 @@ namespace Wasm.Build.Tests
                 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<string>()
             {
-                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 @@ namespace Wasm.Build.Tests
                 //}
             }
 
-            AssertDotNetWasmJs(bundleDir, fromRuntimePack: dotnetWasmFromRuntimePack, targetFramework);
-
             void AssertIcuAssets()
             {
                 bool expectEFIGS = false;
@@ -727,7 +750,7 @@ namespace Wasm.Build.Tests
                 bool expectNOCJK = false;
                 bool expectFULL = false;
                 bool expectHYBRID = false;
-                switch (globalizationMode)
+                switch (options.GlobalizationMode)
                 {
                     case GlobalizationMode.Invariant:
                         break;
@@ -738,11 +761,11 @@ namespace Wasm.Build.Tests
                         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 @@ namespace Wasm.Build.Tests
                         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 @@ namespace Wasm.Build.Tests
             }
         }
 
-        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 @@ namespace Wasm.Build.Tests
             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 = @"\.(?<version>.+)\.(?<hash>[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<string> 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 @@ namespace Wasm.Build.Tests
 
             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 @@ namespace Wasm.Build.Tests
                                          IDictionary<string, string>? 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 @@ namespace Wasm.Build.Tests
                                          IDictionary<string, string>? envVars = null,
                                          string? workingDir = null,
                                          string? label = null,
-                                         bool logToXUnit = true,
                                          int? timeoutMs = null)
         {
             _testOutput.WriteLine($"Running {path} {args}");
@@ -1167,14 +1109,12 @@ namespace Wasm.Build.Tests
             {
                 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 @@ namespace Wasm.Build.Tests
             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 @@ namespace Wasm.Build.Tests
     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<string, string>? ExtraBuildEnvironmentVariables = null
-    );
-
     public record BlazorBuildOptions
     (
         string Id,
@@ -1317,7 +1246,8 @@ namespace Wasm.Build.Tests
         string TargetFramework = BuildTestBase.DefaultTargetFrameworkForBlazor,
         bool WarnAsError = true,
         bool ExpectRelinkDirWhenPublishing = false,
-        bool ExpectFingerprintOnDotnetJs = false
+        bool ExpectFingerprintOnDotnetJs = false,
+        RuntimeVariant RuntimeType = RuntimeVariant.SingleThreaded
     );
 
     public enum GlobalizationMode
index cb24f65..4dddc0c 100644 (file)
@@ -139,10 +139,14 @@ namespace Wasm.Build.Tests
 
         // 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 (file)
index 0000000..c060f42
--- /dev/null
@@ -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 (file)
index 0000000..a28657f
--- /dev/null
@@ -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);
+    }
+}
index 2fae80a..a308c7b 100644 (file)
@@ -115,8 +115,6 @@ namespace Wasm.Build.Tests
                 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 @@ namespace Wasm.Build.Tests
                 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 (file)
index 0000000..42fd887
--- /dev/null
@@ -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 (file)
index 0000000..5dd4240
--- /dev/null
@@ -0,0 +1,170 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+#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 = @"\.(?<version>.+)\.(?<hash>[a-zA-Z0-9]+)\.";
+    private static string[] s_dotnetExtensionsToIgnore = new[]
+    {
+        ".gz",
+        ".br",
+        ".symbols"
+    };
+
+    public string ProjectDir { get; } = projectDir;
+
+    public IReadOnlyDictionary<string, DotNetFileName> 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<string, bool> GetAllKnownDotnetFilesToFingerprintMap(RuntimeVariant runtimeType);
+    protected abstract IReadOnlySet<string> GetDotNetFilesExpectedSet(RuntimeVariant runtimeType, bool isPublish);
+
+    public IReadOnlyDictionary<string, DotNetFileName> FindAndAssertDotnetFiles(
+        string dir,
+        bool expectFingerprintOnDotnetJs,
+        IReadOnlyDictionary<string, bool> superSet,
+        IReadOnlySet<string>? expected)
+    {
+        var actual = new SortedDictionary<string, DotNetFileName>();
+
+        IList<string> 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<string> expected,
+        IReadOnlyDictionary<string, bool> superSet,
+        IDictionary<string, DotNetFileName> 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<string> 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 (file)
index 0000000..ac24e58
--- /dev/null
@@ -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<string, bool> GetAllKnownDotnetFilesToFingerprintMap(RuntimeVariant runtimeType)
+        => new SortedDictionary<string, bool>()
+            {
+               { "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<string> GetDotNetFilesExpectedSet(RuntimeVariant runtimeType, bool isPublish)
+    {
+        SortedSet<string>? res = null;
+        if (runtimeType is RuntimeVariant.SingleThreaded)
+        {
+            res = new SortedSet<string>()
+            {
+               "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<string>()
+            {
+               "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}");
+    }
+}
index 0ff1546..fb09aa4 100644 (file)
     <Error Condition="'$(TestUsingWorkloads)' == 'true' and '$(PackageVersionForWorkloadManifests)' == ''"
            Text="%24(PackageVersionForWorkloadManifests) is not set. PackageVersion=$(PackageVersion)." />
 
-    <PropertyGroup>
-      <_SdkWithWorkloadForTestingDirName>$([System.IO.Path]::GetDirectoryName($(SdkWithWorkloadForTestingPath)))</_SdkWithWorkloadForTestingDirName>
-      <_SdkWithWorkloadForTestingDirName>$([System.IO.Path]::GetFilename($(_SdkWithWorkloadForTestingDirName)))</_SdkWithWorkloadForTestingDirName>
-    </PropertyGroup>
-
     <ItemGroup Condition="'$(TestUsingWorkloads)' == 'true'">
       <RunScriptCommands Condition="'$(OS)' != 'Windows_NT'" Include="export WORKLOAD_PACKS_VER=$(PackageVersionForWorkloadManifests)" />
       <RunScriptCommands Condition="'$(OS)' == 'Windows_NT'" Include="set WORKLOAD_PACKS_VER=$(PackageVersionForWorkloadManifests)" />
       <_XUnitTraitArg Condition="'$(TestUsingWorkloads)' != 'true'">-trait category=no-workload</_XUnitTraitArg>
     </PropertyGroup>
 
+    <PropertyGroup Condition="'$(ContinuousIntegrationBuild)' != 'true'">
+      <_SdkPathForLocalTesting Condition="'$(TestUsingWorkloads)' == 'true'">$([System.IO.Path]::GetDirectoryName($(SdkWithWorkloadForTestingPath)))</_SdkPathForLocalTesting>
+      <_SdkPathForLocalTesting Condition="'$(TestUsingWorkloads)' != 'true'">$([System.IO.Path]::GetDirectoryName($(SdkWithNoWorkloadForTestingPath)))</_SdkPathForLocalTesting>
+
+      <_SdkPathForLocalTesting>$([System.IO.Path]::GetFilename($(_SdkPathForLocalTesting)))</_SdkPathForLocalTesting>
+    </PropertyGroup>
     <ItemGroup Condition="'$(ContinuousIntegrationBuild)' != 'true'">
       <RunScriptCommands Condition="'$(OS)' != 'Windows_NT'" Include="export TEST_USING_WORKLOADS=$(TestUsingWorkloads)" />
       <RunScriptCommands Condition="'$(OS)' == 'Windows_NT'" Include="set TEST_USING_WORKLOADS=$(TestUsingWorkloads)" />
 
-      <RunScriptCommands Condition="'$(OS)' != 'Windows_NT'" Include="export SDK_DIR_NAME=$(_SdkWithWorkloadForTestingDirName)" />
-      <RunScriptCommands Condition="'$(OS)' == 'Windows_NT'" Include="set SDK_DIR_NAME=$(_SdkWithWorkloadForTestingDirName)" />
+      <RunScriptCommands Condition="'$(OS)' != 'Windows_NT'" Include="export SDK_DIR_NAME=$(_SdkPathForLocalTesting)" />
+      <RunScriptCommands Condition="'$(OS)' == 'Windows_NT'" Include="set SDK_DIR_NAME=$(_SdkPathForLocalTesting)" />
 
       <RunScriptCommands Condition="'$(OS)' != 'Windows_NT'" Include="export TEST_USING_WEBCIL=$(TestUsingWebcil)" />
       <RunScriptCommands Condition="'$(OS)' == 'Windows_NT'" Include="set TEST_USING_WEBCIL=$(TestUsingWebcil)" />
diff --git a/src/mono/wasm/Wasm.Build.Tests/WasmSdkBasedProjectProvider.cs b/src/mono/wasm/Wasm.Build.Tests/WasmSdkBasedProjectProvider.cs
new file mode 100644 (file)
index 0000000..6ee5a6c
--- /dev/null
@@ -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<string, bool> GetAllKnownDotnetFilesToFingerprintMap(RuntimeVariant runtimeType)
+        => new SortedDictionary<string, bool>()
+            {
+               { "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<string> GetDotNetFilesExpectedSet(RuntimeVariant runtimeType, bool isPublish)
+    {
+        SortedSet<string> 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;
+    }
+}
index 88af535..088b113 100644 (file)
@@ -96,7 +96,7 @@ namespace Wasm.Build.Tests
             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 @@ namespace Wasm.Build.Tests
             _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 @@ namespace Wasm.Build.Tests
             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 @@ namespace Wasm.Build.Tests
             _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 @@ namespace Wasm.Build.Tests
             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 @@ namespace Wasm.Build.Tests
             buildArgs = ExpandBuildArgs(buildArgs);
 
             bool expectRelinking = config == "Release" || aot || relinking;
-            BuildProject(buildArgs,
+            BuildTemplateProject(buildArgs,
                         id: id,
                         new BuildProjectOptions(
                             DotnetWasmFromRuntimePack: !expectRelinking,
index 62834e3..80a9904 100644 (file)
@@ -13,6 +13,7 @@
        "settings": {
                "omnisharp.enableMsBuildLoadProjectsOnDemand": true,
                "omnisharp.defaultLaunchSolution": "${workspaceFolder}sln/WasmBuild.sln",
-               "omnisharp.enableRoslynAnalyzers": true
+               "omnisharp.enableRoslynAnalyzers": true,
+               "cSpell.enabled": false
        }
 }