[wasm] WBT fixes, and rationalization to allow improved testing (#89360)
authorAnkit Jain <radical@gmail.com>
Tue, 25 Jul 2023 15:32:39 +0000 (11:32 -0400)
committerGitHub <noreply@github.com>
Tue, 25 Jul 2023 15:32:39 +0000 (11:32 -0400)
* Move DotNetFileName.cs, and TestUtils.cs to Common

* Move some types from BuildTestBase.cs to separate files

- FileStat
- BuildPaths
- BlazorBuildOptions
- GlobalizationMode

* Move AssertTestMainJsAppBundleOptions.cs to Common

* Instead of globalizationMode=null use GlobalizationMode.Default

* Merge behaviors for the various project types

- Move parsing bootjson, checking icu assets, symbols to ProviderBase, so it can
  be used by all the project types. These come from WasmAppBuilder which
  is shared by all the projects.

- Instead of multiple separate ways to build the project, use one
  `BuildTestBase.BuildWithoutAssert` method that uses `DotNetCommand` to
  build. And all the project types can use this.
  - This allows having any build customizations or fixes to be in once
    place, and the outputs to be consistent.

- Instead of having `UseWebcil` in various option types, use it directly
  as needed, because this setting is *not* changed per test, rather it
  is fixed per run.

- Rationalize figuring out bin framework directories

Known limitations:
- Wasm template tests use a TestMainJs provider to assert the bundle
  because the templates are not yet based on wasm sdk.
- Blazor has a bug due to which all the icu assets get deployed
  irrespective of settings, so asserting that is disabled.
- Also, blazor does not yet support symbols file.

* Update tests to track api changes
* CI: don't trigger jobs when WasmBuild.sln changes

* Address feedback from @ilonatommy
* Address feedback from @ilonatommy - simplify icu assert code

32 files changed:
eng/pipelines/common/evaluate-default-paths.yml
src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.targets
src/mono/wasi/Wasi.Build.Tests/BuildTestBase.cs
src/mono/wasm/Wasm.Build.Tests/AssertTestMainJsAppBundleOptions.cs [deleted file]
src/mono/wasm/Wasm.Build.Tests/AssertWasmSdkBundleOptions.cs [new file with mode: 0644]
src/mono/wasm/Wasm.Build.Tests/Blazor/BlazorBuildOptions.cs [new file with mode: 0644]
src/mono/wasm/Wasm.Build.Tests/Blazor/BlazorWasmProjectProvider.cs
src/mono/wasm/Wasm.Build.Tests/Blazor/BlazorWasmTestBase.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/BuildProjectOptions.cs
src/mono/wasm/Wasm.Build.Tests/BuildTestBase.cs
src/mono/wasm/Wasm.Build.Tests/Common/AssertBundleOptionsBase.cs [new file with mode: 0644]
src/mono/wasm/Wasm.Build.Tests/Common/AssertTestMainJsAppBundleOptions.cs [new file with mode: 0644]
src/mono/wasm/Wasm.Build.Tests/Common/BuildEnvironment.cs
src/mono/wasm/Wasm.Build.Tests/Common/BuildPaths.cs [new file with mode: 0644]
src/mono/wasm/Wasm.Build.Tests/Common/DotNetFileName.cs [moved from src/mono/wasm/Wasm.Build.Tests/DotNetFileName.cs with 100% similarity]
src/mono/wasm/Wasm.Build.Tests/Common/FileStat.cs [new file with mode: 0644]
src/mono/wasm/Wasm.Build.Tests/Common/GlobalizationMode.cs [new file with mode: 0644]
src/mono/wasm/Wasm.Build.Tests/Common/TestUtils.cs [moved from src/mono/wasm/Wasm.Build.Tests/TestUtils.cs with 100% similarity]
src/mono/wasm/Wasm.Build.Tests/HostRunner/IHostRunner.cs
src/mono/wasm/Wasm.Build.Tests/IcuShardingTests.cs
src/mono/wasm/Wasm.Build.Tests/InvariantGlobalizationTests.cs
src/mono/wasm/Wasm.Build.Tests/NativeRebuildTests/NativeRebuildTestsBase.cs
src/mono/wasm/Wasm.Build.Tests/ProjectProviderBase.cs
src/mono/wasm/Wasm.Build.Tests/RebuildTests.cs
src/mono/wasm/Wasm.Build.Tests/Templates/WasmTemplateTests.cs
src/mono/wasm/Wasm.Build.Tests/TestMainJsProjectProvider.cs
src/mono/wasm/Wasm.Build.Tests/TestMainJsTestBase.cs
src/mono/wasm/Wasm.Build.Tests/WasmSdkBasedProjectProvider.cs
src/mono/wasm/Wasm.Build.Tests/WasmTemplateTestBase.cs
src/mono/wasm/sln/WasmBuild.sln

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