From 62197f070a1bcf40396d21d21690e7dd2e024d34 Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Tue, 9 Mar 2021 18:24:37 -0500 Subject: [PATCH 01/18] [wasm] Wasm.Build.Tests: add support for shared builds - Essentially, we want to share builds wherever possible. Example cases: - Same build, but run with different hosts like v8/chrome/safari, as separate test runs - Same build but run with different command line arguments - Sharing builds especially helps when we are AOT'ing, which is slow! - This is done by caching the builds with the key: `public record BuildArgs(string ProjectName, string Config, bool AOT, string ProjectFileContents, string? ExtraBuildArgs);` - Also, ` SharedBuildClassFixture` is added, so that the builds can be cleaned up after all the tests in a particular class have finished running. - Each test run gets a randomly generated test id. This is used for creating: 1. build paths, like `artifacts/bin/Wasm.Build.Tests/net6.0-Release/browser-wasm/xharness-output/logs/n1xwbqxi.ict` 2. and the log for running with xharness, eg. for Chrome, are in `artifacts/bin/Wasm.Build.Tests/net6.0-Release/browser-wasm/xharness-output/logs/n1xwbqxi.ict/Chrome/` - split `WasmBuildAppTest.cs` into : `BuildTestBase.cs`, and `MainWithArgsTests.cs`. --- .../Wasm.Build.Tests/BuildTestBase.cs | 562 +++++++++++++++ .../Wasm.Build.Tests/HelperExtensions.cs | 72 ++ .../InvariantGlobalizationTests.cs | 57 ++ .../Wasm.Build.Tests/MainWithArgsTests.cs | 93 +++ .../BuildWasmApps/Wasm.Build.Tests/RunHost.cs | 18 + .../SharedBuildPerTestClassFixture.cs | 57 ++ .../Wasm.Build.Tests/Wasm.Build.Tests.csproj | 3 +- .../Wasm.Build.Tests/WasmBuildAppTest.cs | 639 +----------------- 8 files changed, 887 insertions(+), 614 deletions(-) create mode 100644 src/tests/BuildWasmApps/Wasm.Build.Tests/BuildTestBase.cs create mode 100644 src/tests/BuildWasmApps/Wasm.Build.Tests/HelperExtensions.cs create mode 100644 src/tests/BuildWasmApps/Wasm.Build.Tests/InvariantGlobalizationTests.cs create mode 100644 src/tests/BuildWasmApps/Wasm.Build.Tests/MainWithArgsTests.cs create mode 100644 src/tests/BuildWasmApps/Wasm.Build.Tests/RunHost.cs create mode 100644 src/tests/BuildWasmApps/Wasm.Build.Tests/SharedBuildPerTestClassFixture.cs diff --git a/src/tests/BuildWasmApps/Wasm.Build.Tests/BuildTestBase.cs b/src/tests/BuildWasmApps/Wasm.Build.Tests/BuildTestBase.cs new file mode 100644 index 00000000000000..02221889fde9ca --- /dev/null +++ b/src/tests/BuildWasmApps/Wasm.Build.Tests/BuildTestBase.cs @@ -0,0 +1,562 @@ +// 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.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Text; +using Xunit; +using Xunit.Abstractions; + +#nullable enable + +namespace Wasm.Build.Tests +{ + public abstract class BuildTestBase : IClassFixture, IDisposable + { + protected const string TestLogPathEnvVar = "TEST_LOG_PATH"; + protected const string SkipProjectCleanupEnvVar = "SKIP_PROJECT_CLEANUP"; + protected const string XHarnessRunnerCommandEnvVar = "XHARNESS_CLI_PATH"; + protected const string s_targetFramework = "net5.0"; + protected static string s_runtimeConfig = "Release"; + protected static string s_runtimePackDir; + protected static string s_defaultBuildArgs; + protected static readonly string s_logRoot; + protected static readonly string s_emsdkPath; + protected static readonly bool s_skipProjectCleanup; + protected static readonly string s_xharnessRunnerCommand; + + protected string? _projectDir; + protected readonly ITestOutputHelper _testOutput; + protected string _logPath; + protected bool _enablePerTestCleanup = false; + protected SharedBuildPerTestClassFixture _buildContext; + + static BuildTestBase() + { + DirectoryInfo? solutionRoot = new (AppContext.BaseDirectory); + while (solutionRoot != null) + { + if (File.Exists(Path.Combine(solutionRoot.FullName, "NuGet.config"))) + { + break; + } + + solutionRoot = solutionRoot.Parent; + } + + if (solutionRoot == null) + { + string? buildDir = Environment.GetEnvironmentVariable("WasmBuildSupportDir"); + + if (buildDir == null || !Directory.Exists(buildDir)) + throw new Exception($"Could not find the solution root, or a build dir: {buildDir}"); + + s_emsdkPath = Path.Combine(buildDir, "emsdk"); + s_runtimePackDir = Path.Combine(buildDir, "microsoft.netcore.app.runtime.browser-wasm"); + s_defaultBuildArgs = $" /p:WasmBuildSupportDir={buildDir} /p:EMSDK_PATH={s_emsdkPath} "; + } + else + { + string artifactsBinDir = Path.Combine(solutionRoot.FullName, "artifacts", "bin"); + s_runtimePackDir = Path.Combine(artifactsBinDir, "microsoft.netcore.app.runtime.browser-wasm", s_runtimeConfig); + + string? emsdk = Environment.GetEnvironmentVariable("EMSDK_PATH"); + if (string.IsNullOrEmpty(emsdk)) + emsdk = Path.Combine(solutionRoot.FullName, "src", "mono", "wasm", "emsdk"); + s_emsdkPath = emsdk; + + s_defaultBuildArgs = $" /p:RuntimeSrcDir={solutionRoot.FullName} /p:RuntimeConfig={s_runtimeConfig} /p:EMSDK_PATH={s_emsdkPath} "; + } + + string? logPathEnvVar = Environment.GetEnvironmentVariable(TestLogPathEnvVar); + if (!string.IsNullOrEmpty(logPathEnvVar)) + { + s_logRoot = logPathEnvVar; + if (!Directory.Exists(s_logRoot)) + { + Directory.CreateDirectory(s_logRoot); + } + } + else + { + s_logRoot = Environment.CurrentDirectory; + } + + string? cleanupVar = Environment.GetEnvironmentVariable(SkipProjectCleanupEnvVar); + s_skipProjectCleanup = !string.IsNullOrEmpty(cleanupVar) && cleanupVar == "1"; + + string? harnessVar = Environment.GetEnvironmentVariable(XHarnessRunnerCommandEnvVar); + if (string.IsNullOrEmpty(harnessVar)) + { + throw new Exception($"{XHarnessRunnerCommandEnvVar} not set"); + } + + s_xharnessRunnerCommand = harnessVar; + } + + public BuildTestBase(ITestOutputHelper output, SharedBuildPerTestClassFixture buildContext) + { + _buildContext = buildContext; + _testOutput = output; + _logPath = s_logRoot; // FIXME: + } + + /* + * TODO: + - AOT modes + - llvmonly + - aotinterp + - skipped assemblies should get have their pinvoke/icall stuff scanned + + - only buildNative + - aot but no wrapper - check that AppBundle wasn't generated + */ + + public static IEnumerable> ConfigWithAOTData(bool aot) + => new IEnumerable[] + { + // list of each member data - for Debug+@aot + new object?[] { new BuildArgs("placeholder", "Debug", aot, "placeholder", string.Empty) }.AsEnumerable(), + + // list of each member data - for Release+@aot + new object?[] { new BuildArgs("placeholder", "Release", aot, "placeholder", string.Empty) }.AsEnumerable() + }.AsEnumerable(); + + public static IEnumerable> ConfigWithAOTData(bool aot, RunHost host) + => ConfigWithAOTData(aot).WithRunHosts(host); + + protected void RunAndTestWasmApp(BuildArgs buildArgs, RunHost host, string id, Action test, string? buildDir=null, int expectedExitCode=0, string? args=null) + { + buildDir ??= _projectDir; + Dictionary? envVars = new(); + envVars["XHARNESS_DISABLE_COLORED_OUTPUT"] = "true"; + if (buildArgs.AOT) + { + envVars["EMSDK_PATH"] = s_emsdkPath; + envVars["MONO_LOG_LEVEL"] = "debug"; + envVars["MONO_LOG_MASK"] = "aot"; + } + + string bundleDir = Path.Combine(GetBinDir(baseDir: buildDir, config: buildArgs.Config), "AppBundle"); + (string testCommand, string extraXHarnessArgs) = host switch + { + RunHost.V8 => ("wasm test", "--js-file=runtime.js --engine=V8 -v trace"), + _ => ("wasm test-browser", $"-v trace -b {host}") + }; + + string testLogPath = Path.Combine(_logPath, host.ToString()); + string output = RunWithXHarness( + testCommand, + testLogPath, + buildArgs.ProjectName, + bundleDir, + _testOutput, + envVars: envVars, + expectedAppExitCode: expectedExitCode, + extraXHarnessArgs: extraXHarnessArgs, + appArgs: args); + + if (buildArgs.AOT) + { + Assert.Contains("AOT: image 'System.Private.CoreLib' found.", output); + Assert.Contains($"AOT: image '{buildArgs.ProjectName}' found.", output); + } + else + { + Assert.DoesNotContain("AOT: image 'System.Private.CoreLib' found.", output); + Assert.DoesNotContain($"AOT: image '{buildArgs.ProjectName}' found.", output); + } + } + + protected static string RunWithXHarness(string testCommand, string testLogPath, string projectName, string bundleDir, + ITestOutputHelper _testOutput, IDictionary? envVars=null, + int expectedAppExitCode=0, int xharnessExitCode=0, string? extraXHarnessArgs=null, string? appArgs=null) + { + Console.WriteLine($"============== {testCommand} ============="); + Directory.CreateDirectory(testLogPath); + + StringBuilder args = new(); + args.Append($"exec {s_xharnessRunnerCommand}"); + args.Append($" {testCommand}"); + args.Append($" --app=."); + args.Append($" --output-directory={testLogPath}"); + args.Append($" --expected-exit-code={expectedAppExitCode}"); + args.Append($" {extraXHarnessArgs ?? string.Empty}"); + + args.Append(" -- "); + + // App arguments + if (envVars != null) + { + var setenv = string.Join(' ', envVars.Select(kvp => $"--setenv={kvp.Key}={kvp.Value}").ToArray()); + args.Append($" {setenv}"); + } + + args.Append($" --run {projectName}.dll"); + args.Append($" {appArgs ?? string.Empty}"); + + var (exitCode, output) = RunProcess("dotnet", _testOutput, + args: args.ToString(), + workingDir: bundleDir, + envVars: envVars, + label: testCommand); + + File.WriteAllText(Path.Combine(testLogPath, $"xharness.log"), output); + + if (exitCode != xharnessExitCode) + { + _testOutput.WriteLine($"Exit code: {exitCode}"); + Assert.True(exitCode == expectedAppExitCode, $"[{testCommand}] Exit code, expected {expectedAppExitCode} but got {exitCode}"); + } + + return output; + } + + [MemberNotNull(nameof(_projectDir), nameof(_logPath))] + protected void InitPaths(string id) + { + _projectDir = Path.Combine(AppContext.BaseDirectory, id); + _logPath = Path.Combine(s_logRoot, id); + + Directory.CreateDirectory(_logPath); + } + + protected static void InitProjectDir(string dir) + { + Directory.CreateDirectory(dir); + File.WriteAllText(Path.Combine(dir, "Directory.Build.props"), s_directoryBuildProps); + File.WriteAllText(Path.Combine(dir, "Directory.Build.targets"), s_directoryBuildTargets); + } + + protected const string SimpleProjectTemplate = + @$" + + {s_targetFramework} + Exe + true + runtime-test.js + ##EXTRA_PROPERTIES## + + "; + + protected BuildArgs GetBuildArgsWith(BuildArgs buildArgs, string? extraProperties=null, string projectTemplate=SimpleProjectTemplate) + { + if (buildArgs.AOT) + extraProperties = $"{extraProperties}\ntrue\n"; + + string projectContents = projectTemplate.Replace("##EXTRA_PROPERTIES##", extraProperties ?? string.Empty); + return buildArgs with { ProjectFileContents = projectContents }; + } + + public string BuildProject(BuildArgs buildArgs, + Action initProject, + string id, + bool? dotnetWasmFromRuntimePack = null, + bool hasIcudt = true, + bool useCache = true) + { + if (useCache && _buildContext.TryGetBuildFor(buildArgs, out BuildProduct? product)) + { + Console.WriteLine ($"Using existing build found at {product.BuildPath}, with build log at {product.LogFile}"); + + Assert.True(product.Result, $"Found existing build at {product.BuildPath}, but it had failed. Check build log at {product.LogFile}"); + _projectDir = product.BuildPath; + + // use this test's id for the run logs + _logPath = Path.Combine(s_logRoot, id); + return _projectDir; + } + + InitPaths(id); + InitProjectDir(_projectDir); + initProject?.Invoke(); + + File.WriteAllText(Path.Combine(_projectDir, $"{buildArgs.ProjectName}.csproj"), buildArgs.ProjectFileContents); + File.Copy(Path.Combine(AppContext.BaseDirectory, "runtime-test.js"), Path.Combine(_projectDir, "runtime-test.js")); + + StringBuilder sb = new(); + sb.Append("publish"); + sb.Append(s_defaultBuildArgs); + + sb.Append($" /p:Configuration={buildArgs.Config}"); + + string logFilePath = Path.Combine(_logPath, $"{buildArgs.ProjectName}.binlog"); + _testOutput.WriteLine($"Binlog path: {logFilePath}"); + sb.Append($" /bl:\"{logFilePath}\" /v:minimal /nologo"); + if (buildArgs.ExtraBuildArgs != null) + sb.Append($" {buildArgs.ExtraBuildArgs} "); + + Console.WriteLine($"Building {buildArgs.ProjectName} in {_projectDir}"); + + try + { + AssertBuild(sb.ToString()); + if (useCache) + { + _buildContext.CacheBuild(buildArgs, new BuildProduct(_projectDir, logFilePath, true)); + Console.WriteLine($"caching build for {buildArgs}"); + } + } + catch + { + if (useCache) + _buildContext.CacheBuild(buildArgs, new BuildProduct(_projectDir, logFilePath, false)); + throw; + } + + string bundleDir = Path.Combine(GetBinDir(config: buildArgs.Config), "AppBundle"); + dotnetWasmFromRuntimePack ??= !buildArgs.AOT; + AssertBasicAppBundle(bundleDir, buildArgs.ProjectName, buildArgs.Config, hasIcudt, dotnetWasmFromRuntimePack.Value); + + return _projectDir; + } + + protected static void AssertBasicAppBundle(string bundleDir, string projectName, string config, bool hasIcudt=true, bool dotnetWasmFromRuntimePack=true) + { + AssertFilesExist(bundleDir, new [] + { + "index.html", + "runtime.js", + "dotnet.timezones.blat", + "dotnet.wasm", + "mono-config.js", + "dotnet.js", + "run-v8.sh" + }); + + AssertFilesExist(bundleDir, new[] { "icudt.dat" }, expectToExist: hasIcudt); + + string managedDir = Path.Combine(bundleDir, "managed"); + AssertFilesExist(managedDir, new[] { $"{projectName}.dll" }); + + bool is_debug = 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}"); + //} + } + + AssertDotNetWasmJs(bundleDir, fromRuntimePack: dotnetWasmFromRuntimePack); + } + + protected static void AssertDotNetWasmJs(string bundleDir, bool fromRuntimePack) + { + string nativeDir = GetRuntimeNativeDir(); + + AssertFile(Path.Combine(nativeDir, "dotnet.wasm"), Path.Combine(bundleDir, "dotnet.wasm"), "Expected dotnet.wasm to be same as the runtime pack", same: fromRuntimePack); + AssertFile(Path.Combine(nativeDir, "dotnet.js"), Path.Combine(bundleDir, "dotnet.js"), "Expected dotnet.js to be same as the runtime pack", same: fromRuntimePack); + } + + protected static void AssertFilesDontExist(string dir, string[] filenames, string? label = null) + => AssertFilesExist(dir, filenames, label, expectToExist: false); + + protected static void AssertFilesExist(string dir, string[] filenames, string? label = null, bool expectToExist=true) + { + Assert.True(Directory.Exists(dir), $"[{label}] {dir} not found"); + foreach (string filename in filenames) + { + string path = Path.Combine(dir, filename); + + if (expectToExist) + { + Assert.True(File.Exists(path), + label != null + ? $"{label}: {path} doesn't exist" + : $"{path} doesn't exist"); + } + else + { + Assert.False(File.Exists(path), + label != null + ? $"{label}: {path} should not exist" + : $"{path} should not exist"); + } + } + } + + protected static void AssertSameFile(string file0, string file1, string? label=null) => AssertFile(file0, file1, label, same: true); + protected static void AssertNotSameFile(string file0, string file1, string? label=null) => AssertFile(file0, file1, label, same: false); + + protected static void AssertFile(string file0, string file1, string? label=null, bool same=true) + { + Assert.True(File.Exists(file0), $"{label}: Expected to find {file0}"); + Assert.True(File.Exists(file1), $"{label}: Expected to find {file1}"); + + FileInfo finfo0 = new(file0); + FileInfo finfo1 = new(file1); + + if (same) + Assert.True(finfo0.Length == finfo1.Length, $"{label}: File sizes don't match for {file0} ({finfo0.Length}), and {file1} ({finfo1.Length})"); + else + Assert.True(finfo0.Length != finfo1.Length, $"{label}: File sizes should not match for {file0} ({finfo0.Length}), and {file1} ({finfo1.Length})"); + } + + protected void AssertBuild(string args) + { + (int exitCode, _) = RunProcess("dotnet", _testOutput, args, workingDir: _projectDir, label: "build"); + Assert.True(0 == exitCode, $"Build process exited with non-zero exit code: {exitCode}"); + } + + // protected string GetObjDir(string targetFramework=s_targetFramework, string? baseDir=null, string config="Debug") + // => Path.Combine(baseDir ?? _projectDir, "obj", config, targetFramework, "browser-wasm", "wasm"); + + protected string GetBinDir(string targetFramework=s_targetFramework, string? baseDir=null, string config="Debug") + { + var dir = baseDir ?? _projectDir; + Assert.NotNull(dir); + return Path.Combine(dir!, "bin", config, targetFramework, "browser-wasm"); + } + + protected static string GetRuntimePackDir() => s_runtimePackDir; + + protected static string GetRuntimeNativeDir() + => Path.Combine(GetRuntimePackDir(), "runtimes", "browser-wasm", "native"); + + + public static (int, string) RunProcess(string path, + ITestOutputHelper _testOutput, + string args = "", + IDictionary? envVars = null, + string? workingDir = null, + string? label = null, + bool logToXUnit = true) + { + _testOutput.WriteLine($"Running {path} {args}"); + Console.WriteLine($"Running: {path}: {args}"); + Console.WriteLine($"WorkingDirectory: {workingDir}"); + StringBuilder outputBuilder = new (); + var processStartInfo = new ProcessStartInfo + { + FileName = path, + UseShellExecute = false, + CreateNoWindow = true, + RedirectStandardError = true, + RedirectStandardOutput = true, + Arguments = args, + }; + + if (workingDir == null || !Directory.Exists(workingDir)) + throw new Exception($"Working directory {workingDir} not found"); + + if (workingDir != null) + processStartInfo.WorkingDirectory = workingDir; + + if (envVars != null) + { + if (envVars.Count > 0) + _testOutput.WriteLine("Setting environment variables for execution:"); + + foreach (KeyValuePair envVar in envVars) + { + processStartInfo.EnvironmentVariables[envVar.Key] = envVar.Value; + _testOutput.WriteLine($"\t{envVar.Key} = {envVar.Value}"); + } + } + + Process process = new (); + process.StartInfo = processStartInfo; + process.EnableRaisingEvents = true; + + process.ErrorDataReceived += (sender, e) => LogData("[stderr]", e.Data); + process.OutputDataReceived += (sender, e) => LogData("[stdout]", e.Data); + // AutoResetEvent resetEvent = new (false); + // process.Exited += (_, _) => { Console.WriteLine ($"- exited called"); resetEvent.Set(); }; + + if (!process.Start()) + throw new ArgumentException("No process was started: process.Start() return false."); + + try + { + process.BeginOutputReadLine(); + process.BeginErrorReadLine(); + + // process.WaitForExit doesn't work if the process exits too quickly? + // resetEvent.WaitOne(); + process.WaitForExit(); + return (process.ExitCode, outputBuilder.ToString().Trim('\r', '\n')); + } + catch (Exception ex) + { + Console.WriteLine($"-- exception -- {ex}"); + throw; + } + + void LogData(string label, string? message) + { + if (logToXUnit && message != null) + { + _testOutput.WriteLine($"{label} {message}"); + Console.WriteLine($"{label} {message}"); + } + outputBuilder.AppendLine($"{label} {message}"); + } + } + + public void Dispose() + { + if (s_skipProjectCleanup || !_enablePerTestCleanup) + return; + + if (_projectDir != null) + _buildContext.RemoveFromCache(_projectDir); + } + + protected static string s_directoryBuildProps = @" + + <_WasmTargetsDir Condition=""'$(RuntimeSrcDir)' != ''"">$(RuntimeSrcDir)\src\mono\wasm\build\ + <_WasmTargetsDir Condition=""'$(WasmBuildSupportDir)' != ''"">$(WasmBuildSupportDir)\wasm\ + $(WasmBuildSupportDir)\emsdk\ + + + + + + + PrepareForWasmBuild;$(WasmBuildAppDependsOn) + +"; + + protected static string s_directoryBuildTargets = @" + + + + + + + + + + + + + + + + + + +"; + + } + + public record BuildArgs(string ProjectName, string Config, bool AOT, string ProjectFileContents, string? ExtraBuildArgs); + public record BuildProduct(string BuildPath, string LogFile, bool Result); + } diff --git a/src/tests/BuildWasmApps/Wasm.Build.Tests/HelperExtensions.cs b/src/tests/BuildWasmApps/Wasm.Build.Tests/HelperExtensions.cs new file mode 100644 index 00000000000000..930afc69d038e9 --- /dev/null +++ b/src/tests/BuildWasmApps/Wasm.Build.Tests/HelperExtensions.cs @@ -0,0 +1,72 @@ +// 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.Linq; +using System.IO; + +namespace Wasm.Build.Tests +{ + public static class HelperExtensions + { + public static IEnumerable UnwrapItemsAsArrays(this IEnumerable> enumerable) + => enumerable.Select(e => e.ToArray()); + + /// + /// Cartesian product + /// + /// Say we want to provide test data for: + /// [MemberData(nameof(TestData))] + /// public void Test(string name, int num) { } + /// + /// And we want to test with `names = object[] { "Name0", "Name1" }` + /// + /// And for each of those names, we want to test with some numbers, + /// say `numbers = object[] { 1, 4 }` + /// + /// So, we want the final test data to be: + /// + /// { "Name0", 1 } + /// { "Name0", 4 } + /// { "Name1", 1 } + /// { "Name1", 4 } + /// + /// Then we can use: names.Combine(numbers) + /// + /// + /// + /// + /// + public static IEnumerable> Multiply(this IEnumerable> data, object?[] options) + => data?.SelectMany(d => options.Select(o => d.Append(o))); + + public static object?[] Enumerate(this RunHost host) + { + var list = new List(); + foreach (var value in Enum.GetValues()) + { + // Ignore any combos like RunHost.All from Enum.GetValues + // by ignoring any @value that has more than 1 bit set + if (((int)value & ((int)value - 1)) != 0) + continue; + + if ((host & value) == value) + list.Add(value); + } + return list.ToArray(); + } + + public static IEnumerable> WithRunHosts(this IEnumerable> data, RunHost hosts) + { + IEnumerable hostsEnumerable = hosts.Enumerate(); + return data?.SelectMany(d => + { + string runId = Path.GetRandomFileName(); + return hostsEnumerable.Select(o => + d.Append((object?)o) + .Append((object?)runId)); + }); + } + } +} diff --git a/src/tests/BuildWasmApps/Wasm.Build.Tests/InvariantGlobalizationTests.cs b/src/tests/BuildWasmApps/Wasm.Build.Tests/InvariantGlobalizationTests.cs new file mode 100644 index 00000000000000..66ba088c7802b8 --- /dev/null +++ b/src/tests/BuildWasmApps/Wasm.Build.Tests/InvariantGlobalizationTests.cs @@ -0,0 +1,57 @@ +using System.Collections.Generic; +using System.IO; +using System.Text; +using Xunit; +using Xunit.Abstractions; + +namespace Wasm.Build.Tests +{ + public class InvariantGlobalizationTests : BuildTestBase + { + public InvariantGlobalizationTests(ITestOutputHelper output, SharedBuildPerTestClassFixture buildContext) + : base(output, buildContext) + { + } + + public static IEnumerable InvariantGlobalizationTestData(bool aot, RunHost host) + => ConfigWithAOTData(aot) + .Multiply(new object?[] { null, false, true }) + .WithRunHosts(host) + .UnwrapItemsAsArrays(); + + // TODO: check that icu bits have been linked out + [Theory] + [MemberData(nameof(InvariantGlobalizationTestData), parameters: new object[] { /*aot*/ false, RunHost.All })] + [MemberData(nameof(InvariantGlobalizationTestData), parameters: new object[] { /*aot*/ true, RunHost.All })] + public void InvariantGlobalization(BuildArgs buildArgs, bool? invariantGlobalization, RunHost host, string id) + { + string projectName = $"invariant_{invariantGlobalization?.ToString() ?? "unset"}"; + string? extraProperties = null; + if (invariantGlobalization != null) + extraProperties = $"{invariantGlobalization}"; + + buildArgs = buildArgs with { ProjectName = projectName }; + buildArgs = GetBuildArgsWith(buildArgs, extraProperties); + + string programText = @" + using System; + using System.Threading.Tasks; + + public class TestClass { + public static int Main() + { + Console.WriteLine(""Hello, World!""); + return 42; + } + }"; + + BuildProject(buildArgs, + initProject: () => File.WriteAllText(Path.Combine(_projectDir!, "Program.cs"), programText), + id: id, + hasIcudt: invariantGlobalization == null || invariantGlobalization.Value == false); + + RunAndTestWasmApp(buildArgs, expectedExitCode: 42, + test: output => Assert.Contains("Hello, World!", output), host: host, id: id); + } + } +} diff --git a/src/tests/BuildWasmApps/Wasm.Build.Tests/MainWithArgsTests.cs b/src/tests/BuildWasmApps/Wasm.Build.Tests/MainWithArgsTests.cs new file mode 100644 index 00000000000000..e932fefa85f1b8 --- /dev/null +++ b/src/tests/BuildWasmApps/Wasm.Build.Tests/MainWithArgsTests.cs @@ -0,0 +1,93 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.IO; +using Xunit; +using Xunit.Abstractions; + +#nullable enable + +namespace Wasm.Build.Tests +{ + public class MainWithArgsTests : BuildTestBase + { + public MainWithArgsTests(ITestOutputHelper output, SharedBuildPerTestClassFixture buildContext) + : base(output, buildContext) + { + _buildContext = buildContext; + } + + public static IEnumerable MainWithArgsTestData(bool aot, RunHost host) + => ConfigWithAOTData(aot).Multiply( + new object?[] { + new object?[] { "abc", "foobar"}, + new object?[0] + } + ).WithRunHosts(host).UnwrapItemsAsArrays(); + + [Theory] + [MemberData(nameof(MainWithArgsTestData), parameters: new object[] { /*aot*/ false, RunHost.All })] + [MemberData(nameof(MainWithArgsTestData), parameters: new object[] { /*aot*/ true, RunHost.All })] + public void AsyncMainWithArgs(BuildArgs buildArgs, string[] args, RunHost host, string id) + => TestMainWithArgs("async_main_with_args", @" + public class TestClass { + public static async System.Threading.Tasks.Task Main(string[] args) + { + ##CODE## + return await System.Threading.Tasks.Task.FromResult(42 + count); + } + }", + buildArgs, args, host, id); + + [Theory] + [MemberData(nameof(MainWithArgsTestData), parameters: new object[] { /*aot*/ false, RunHost.All })] + [MemberData(nameof(MainWithArgsTestData), parameters: new object[] { /*aot*/ true, RunHost.All })] + public void TopLevelWithArgs(BuildArgs buildArgs, string[] args, RunHost host, string id) + => TestMainWithArgs("top_level_args", + @"##CODE## return await System.Threading.Tasks.Task.FromResult(42 + count);", + buildArgs, args, host, id); + + [Theory] + [MemberData(nameof(MainWithArgsTestData), parameters: new object[] { /*aot*/ false, RunHost.All })] + [MemberData(nameof(MainWithArgsTestData), parameters: new object[] { /*aot*/ true, RunHost.All })] + public void NonAsyncMainWithArgs(BuildArgs buildArgs, string[] args, RunHost host, string id) + => TestMainWithArgs("non_async_main_args", @" + public class TestClass { + public static int Main(string[] args) + { + ##CODE## + return 42 + count; + } + }", buildArgs, args, host, id); + + void TestMainWithArgs(string projectNamePrefix, string projectContents, BuildArgs buildArgs, string[] args, RunHost host, string id) + { + string projectName = $"{projectNamePrefix}_{buildArgs.Config}_{buildArgs.AOT}"; + string code = @" + int count = args == null ? 0 : args.Length; + System.Console.WriteLine($""args#: {args?.Length}""); + foreach (var arg in args ?? System.Array.Empty()) + System.Console.WriteLine($""arg: {arg}""); + "; + string programText = projectContents.Replace("##CODE##", code); + + buildArgs = buildArgs with { ProjectName = projectName, ProjectFileContents = programText }; + buildArgs = GetBuildArgsWith(buildArgs); + Console.WriteLine ($"-- args: {buildArgs}, name: {projectName}"); + + BuildProject(buildArgs, + initProject: () => File.WriteAllText(Path.Combine(_projectDir!, "Program.cs"), programText), + id: id); + + RunAndTestWasmApp(buildArgs, buildDir: _projectDir, expectedExitCode: 42 + args.Length, args: string.Join(' ', args), + test: output => + { + Assert.Contains($"args#: {args.Length}", output); + foreach (var arg in args) + Assert.Contains($"arg: {arg}", output); + }, host: host, id: id); + } + } +} diff --git a/src/tests/BuildWasmApps/Wasm.Build.Tests/RunHost.cs b/src/tests/BuildWasmApps/Wasm.Build.Tests/RunHost.cs new file mode 100644 index 00000000000000..b7326a6332bd48 --- /dev/null +++ b/src/tests/BuildWasmApps/Wasm.Build.Tests/RunHost.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace Wasm.Build.Tests +{ + [Flags] + public enum RunHost + { + V8 = 1, + Chrome = 2, + Safari = 4, + Firefox = 8, + + All = V8 | Chrome//| Firefox//Safari + } +} diff --git a/src/tests/BuildWasmApps/Wasm.Build.Tests/SharedBuildPerTestClassFixture.cs b/src/tests/BuildWasmApps/Wasm.Build.Tests/SharedBuildPerTestClassFixture.cs new file mode 100644 index 00000000000000..a491d972af28c5 --- /dev/null +++ b/src/tests/BuildWasmApps/Wasm.Build.Tests/SharedBuildPerTestClassFixture.cs @@ -0,0 +1,57 @@ +// 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.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; + +#nullable enable + +namespace Wasm.Build.Tests +{ + public class SharedBuildPerTestClassFixture : IDisposable + { + public Dictionary _buildPaths = new(); + + public void CacheBuild(BuildArgs buildArgs, BuildProduct product) + => _buildPaths.Add(buildArgs, product); + + public void RemoveFromCache(string buildPath) + { + KeyValuePair? foundKvp = _buildPaths.Where(kvp => kvp.Value.BuildPath == buildPath).SingleOrDefault(); + if (foundKvp == null) + throw new Exception($"Could not find build path {buildPath} in cache to remove."); + + _buildPaths.Remove(foundKvp.Value.Key); + RemoveDirectory(buildPath); + } + + public bool TryGetBuildFor(BuildArgs buildArgs, [NotNullWhen(true)] out BuildProduct? product) + => _buildPaths.TryGetValue(buildArgs, out product); + + public void Dispose() + { + Console.WriteLine ($"============== DELETING THE BUILDS ============="); + foreach (var kvp in _buildPaths.Values) + { + RemoveDirectory(kvp.BuildPath); + } + } + + private void RemoveDirectory(string path) + { + try + { + Directory.Delete(path, recursive: true); + } + catch (Exception ex) + { + Console.Error.WriteLine($"Failed to delete '{path}' during test cleanup: {ex}"); + throw; + } + } + + } +} diff --git a/src/tests/BuildWasmApps/Wasm.Build.Tests/Wasm.Build.Tests.csproj b/src/tests/BuildWasmApps/Wasm.Build.Tests/Wasm.Build.Tests.csproj index a6fe3e49f375da..a6314a83b7bde7 100644 --- a/src/tests/BuildWasmApps/Wasm.Build.Tests/Wasm.Build.Tests.csproj +++ b/src/tests/BuildWasmApps/Wasm.Build.Tests/Wasm.Build.Tests.csproj @@ -7,6 +7,7 @@ BuildAndRun xunit false + true @@ -27,8 +28,6 @@ - - diff --git a/src/tests/BuildWasmApps/Wasm.Build.Tests/WasmBuildAppTest.cs b/src/tests/BuildWasmApps/Wasm.Build.Tests/WasmBuildAppTest.cs index c6ff9aa45d312f..a94cd5c64edc83 100644 --- a/src/tests/BuildWasmApps/Wasm.Build.Tests/WasmBuildAppTest.cs +++ b/src/tests/BuildWasmApps/Wasm.Build.Tests/WasmBuildAppTest.cs @@ -3,10 +3,7 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.IO; -using System.Linq; -using System.Text; using Xunit; using Xunit.Abstractions; @@ -14,184 +11,28 @@ namespace Wasm.Build.Tests { - public class WasmBuildAppTest : IDisposable + public class WasmBuildAppTest : BuildTestBase { - private const string TestLogPathEnvVar = "TEST_LOG_PATH"; - private const string SkipProjectCleanupEnvVar = "SKIP_PROJECT_CLEANUP"; - private const string XHarnessRunnerCommandEnvVar = "XHARNESS_CLI_PATH"; + public WasmBuildAppTest(ITestOutputHelper output, SharedBuildPerTestClassFixture buildContext) : base(output, buildContext) + {} - private readonly string _tempDir; - private readonly ITestOutputHelper _testOutput; - private readonly string _id; - private readonly string _logPath; - - private const string s_targetFramework = "net5.0"; - private static string s_runtimeConfig = "Release"; - private static string s_runtimePackDir; - private static string s_defaultBuildArgs; - private static readonly string s_logRoot; - private static readonly string s_emsdkPath; - private static readonly bool s_skipProjectCleanup; - private static readonly string s_xharnessRunnerCommand; - - static WasmBuildAppTest() - { - DirectoryInfo? solutionRoot = new (AppContext.BaseDirectory); - while (solutionRoot != null) - { - if (File.Exists(Path.Combine(solutionRoot.FullName, "NuGet.config"))) - { - break; - } - - solutionRoot = solutionRoot.Parent; - } - - if (solutionRoot == null) - { - string? buildDir = Environment.GetEnvironmentVariable("WasmBuildSupportDir"); - - if (buildDir == null || !Directory.Exists(buildDir)) - throw new Exception($"Could not find the solution root, or a build dir: {buildDir}"); - - s_emsdkPath = Path.Combine(buildDir, "emsdk"); - s_runtimePackDir = Path.Combine(buildDir, "microsoft.netcore.app.runtime.browser-wasm"); - s_defaultBuildArgs = $" /p:WasmBuildSupportDir={buildDir} /p:EMSDK_PATH={s_emsdkPath} "; - } - else - { - string artifactsBinDir = Path.Combine(solutionRoot.FullName, "artifacts", "bin"); - s_runtimePackDir = Path.Combine(artifactsBinDir, "microsoft.netcore.app.runtime.browser-wasm", s_runtimeConfig); - - string? emsdk = Environment.GetEnvironmentVariable("EMSDK_PATH"); - if (string.IsNullOrEmpty(emsdk)) - emsdk = Path.Combine(solutionRoot.FullName, "src", "mono", "wasm", "emsdk"); - s_emsdkPath = emsdk; - - s_defaultBuildArgs = $" /p:RuntimeSrcDir={solutionRoot.FullName} /p:RuntimeConfig={s_runtimeConfig} /p:EMSDK_PATH={s_emsdkPath} "; - } - - string? logPathEnvVar = Environment.GetEnvironmentVariable(TestLogPathEnvVar); - if (!string.IsNullOrEmpty(logPathEnvVar)) - { - s_logRoot = logPathEnvVar; - if (!Directory.Exists(s_logRoot)) - { - Directory.CreateDirectory(s_logRoot); - } - } - else - { - s_logRoot = Environment.CurrentDirectory; - } - - string? cleanupVar = Environment.GetEnvironmentVariable(SkipProjectCleanupEnvVar); - s_skipProjectCleanup = !string.IsNullOrEmpty(cleanupVar) && cleanupVar == "1"; - - string? harnessVar = Environment.GetEnvironmentVariable(XHarnessRunnerCommandEnvVar); - if (string.IsNullOrEmpty(harnessVar)) - { - throw new Exception($"{XHarnessRunnerCommandEnvVar} not set"); - } - - s_xharnessRunnerCommand = harnessVar; - } - - public WasmBuildAppTest(ITestOutputHelper output) - { - _testOutput = output; - _id = Path.GetRandomFileName(); - _tempDir = Path.Combine(AppContext.BaseDirectory, _id); - Directory.CreateDirectory(_tempDir); - - _logPath = Path.Combine(s_logRoot, _id); - Directory.CreateDirectory(_logPath); - - _testOutput.WriteLine($"Test Id: {_id}"); - } - - - /* - * TODO: - - AOT modes - - llvmonly - - aotinterp - - skipped assemblies should get have their pinvoke/icall stuff scanned - - - only buildNative - - aot but no wrapper - check that AppBundle wasn't generated - */ - - - public static TheoryData ConfigWithAOTData(bool include_aot=true) - { - TheoryData data = new() - { - { "Debug", false }, - { "Release", false } - }; - - if (include_aot) - { - data.Add("Debug", true); - data.Add("Release", true); - } - - return data; - } - - public static TheoryData InvariantGlobalizationTestData() - { - var data = new TheoryData(); - foreach (var configData in ConfigWithAOTData()) - { - data.Add((string)configData[0], (bool)configData[1], null); - data.Add((string)configData[0], (bool)configData[1], true); - data.Add((string)configData[0], (bool)configData[1], false); - } - return data; - } - - // TODO: check that icu bits have been linked out - [Theory] - [MemberData(nameof(InvariantGlobalizationTestData))] - public void InvariantGlobalization(string config, bool aot, bool? invariantGlobalization) - { - File.WriteAllText(Path.Combine(_tempDir, "Program.cs"), @" - using System; - using System.Threading.Tasks; - - public class TestClass { - public static int Main() - { - Console.WriteLine(""Hello, World!""); - return 42; - } - } - "); - - string? extraProperties = null; - if (invariantGlobalization != null) - extraProperties = $"{invariantGlobalization}"; - - string projectName = $"invariant_{invariantGlobalization?.ToString() ?? "unset"}"; - BuildProject(projectName, config, aot: aot, extraProperties: extraProperties, - hasIcudt: invariantGlobalization == null || invariantGlobalization.Value == false); - - RunAndTestWasmApp(projectName, config, isAOT: aot, expectedExitCode: 42, - test: output => Assert.Contains("Hello, World!", output)); - } + public static IEnumerable MainMethodTestData(bool aot, RunHost host) + => ConfigWithAOTData(aot) + .WithRunHosts(host) + .UnwrapItemsAsArrays(); [Theory] - [MemberData(nameof(ConfigWithAOTData), parameters: /*aot*/ true)] - public void TopLevelMain(string config, bool aot) + [MemberData(nameof(MainMethodTestData), parameters: new object[] { /*aot*/ true, RunHost.All })] + [MemberData(nameof(MainMethodTestData), parameters: new object[] { /*aot*/ false, RunHost.All })] + public void TopLevelMain(BuildArgs buildArgs, RunHost host, string id) => TestMain("top_level", @"System.Console.WriteLine(""Hello, World!""); return await System.Threading.Tasks.Task.FromResult(42);", - config, aot); + buildArgs, host, id); [Theory] - [MemberData(nameof(ConfigWithAOTData), parameters: /*aot*/ true)] - public void AsyncMain(string config, bool aot) + [MemberData(nameof(MainMethodTestData), parameters: new object[] { /*aot*/ true, RunHost.All })] + [MemberData(nameof(MainMethodTestData), parameters: new object[] { /*aot*/ false, RunHost.All })] + public void AsyncMain(BuildArgs buildArgs, RunHost host, string id) => TestMain("async_main", @" using System; using System.Threading.Tasks; @@ -202,11 +43,12 @@ public static async Task Main() Console.WriteLine(""Hello, World!""); return await Task.FromResult(42); } - }", config, aot); + }", buildArgs, host, id); [Theory] - [MemberData(nameof(ConfigWithAOTData), parameters: /*aot*/ true)] - public void NonAsyncMain(string config, bool aot) + [MemberData(nameof(MainMethodTestData), parameters: new object[] { /*aot*/ true, RunHost.All })] + [MemberData(nameof(MainMethodTestData), parameters: new object[] { /*aot*/ false, RunHost.All })] + public void NonAsyncMain(BuildArgs buildArgs, RunHost host, string id) => TestMain("non_async_main", @" using System; using System.Threading.Tasks; @@ -217,447 +59,20 @@ public static int Main() Console.WriteLine(""Hello, World!""); return 42; } - }", config, aot); - - public static TheoryData MainWithArgsTestData() - { - var data = new TheoryData(); - foreach (var configData in ConfigWithAOTData()) - { - data.Add((string)configData[0], (bool)configData[1], new string[] { "abc", "foobar" }); - data.Add((string)configData[0], (bool)configData[1], new string[0]); - } - - return data; - } - - [Theory] - [MemberData(nameof(MainWithArgsTestData))] - public void NonAsyncMainWithArgs(string config, bool aot, string[] args) - => TestMainWithArgs("non_async_main_args", @" - public class TestClass { - public static int Main(string[] args) - { - ##CODE## - return 42 + count; - } - }", config, aot, args); - - [Theory] - [MemberData(nameof(MainWithArgsTestData))] - public void AsyncMainWithArgs(string config, bool aot, string[] args) - => TestMainWithArgs("async_main_args", @" - public class TestClass { - public static async System.Threading.Tasks.Task Main(string[] args) - { - ##CODE## - return await System.Threading.Tasks.Task.FromResult(42 + count); - } - }", config, aot, args); - - [Theory] - [MemberData(nameof(MainWithArgsTestData))] - public void TopLevelWithArgs(string config, bool aot, string[] args) - => TestMainWithArgs("top_level_args", - @"##CODE## return await System.Threading.Tasks.Task.FromResult(42 + count);", - config, aot, args); - - void TestMain(string projectName, string programText, string config, bool aot) - { - File.WriteAllText(Path.Combine(_tempDir, "Program.cs"), programText); - BuildProject(projectName, config, aot: aot); - RunAndTestWasmApp(projectName, config, isAOT: aot, expectedExitCode: 42, - test: output => Assert.Contains("Hello, World!", output)); - } - - void TestMainWithArgs(string projectName, string programFormatString, string config, bool aot, string[] args) - { - string code = @" - int count = args == null ? 0 : args.Length; - System.Console.WriteLine($""args#: {args?.Length}""); - foreach (var arg in args ?? System.Array.Empty()) - System.Console.WriteLine($""arg: {arg}""); - "; - string programText = programFormatString.Replace("##CODE##", code); - - File.WriteAllText(Path.Combine(_tempDir, "Program.cs"), programText); - BuildProject(projectName, config, aot: aot); - RunAndTestWasmApp(projectName, config, isAOT: aot, expectedExitCode: 42 + args.Length, args: string.Join(' ', args), - test: output => - { - Assert.Contains($"args#: {args.Length}", output); - foreach (var arg in args) - Assert.Contains($"arg: {arg}", output); - }); - } - - private void RunAndTestWasmApp(string projectName, string config, bool isAOT, Action test, int expectedExitCode=0, string? args=null) - { - Dictionary? envVars = new(); - envVars["XHARNESS_DISABLE_COLORED_OUTPUT"] = "true"; - if (isAOT) - { - envVars["EMSDK_PATH"] = s_emsdkPath; - envVars["MONO_LOG_LEVEL"] = "debug"; - envVars["MONO_LOG_MASK"] = "aot"; - } - - string bundleDir = Path.Combine(GetBinDir(config: config), "AppBundle"); - string v8output = RunWasmTest(projectName, bundleDir, envVars, expectedExitCode, appArgs: args); - Test(v8output); - - string browserOutput = RunWasmTestBrowser(projectName, bundleDir, envVars, expectedExitCode, appArgs: args); - Test(browserOutput); - - void Test(string output) - { - if (isAOT) - { - Assert.Contains("AOT: image 'System.Private.CoreLib' found.", output); - Assert.Contains($"AOT: image '{projectName}' found.", output); - } - else - { - Assert.DoesNotContain("AOT: image 'System.Private.CoreLib' found.", output); - Assert.DoesNotContain($"AOT: image '{projectName}' found.", output); - } - } - } - - private string RunWithXHarness(string testCommand, string relativeLogPath, string projectName, string bundleDir, IDictionary? envVars=null, - int expectedAppExitCode=0, int xharnessExitCode=0, string? extraXHarnessArgs=null, string? appArgs=null) - { - _testOutput.WriteLine($"============== {testCommand} ============="); - Console.WriteLine($"============== {testCommand} ============="); - string testLogPath = Path.Combine(_logPath, relativeLogPath); - - StringBuilder args = new(); - args.Append($"exec {s_xharnessRunnerCommand}"); - args.Append($" {testCommand}"); - args.Append($" --app=."); - args.Append($" --output-directory={testLogPath}"); - args.Append($" --expected-exit-code={expectedAppExitCode}"); - args.Append($" {extraXHarnessArgs ?? string.Empty}"); - - args.Append(" -- "); - // App arguments - - if (envVars != null) - { - var setenv = string.Join(' ', envVars.Select(kvp => $"--setenv={kvp.Key}={kvp.Value}").ToArray()); - args.Append($" {setenv}"); - } - - args.Append($" --run {projectName}.dll"); - args.Append($" {appArgs ?? string.Empty}"); - - var (exitCode, output) = RunProcess("dotnet", - args: args.ToString(), - workingDir: bundleDir, - envVars: envVars, - label: testCommand); - - File.WriteAllText(Path.Combine(testLogPath, $"xharness.log"), output); - - if (exitCode != xharnessExitCode) - { - _testOutput.WriteLine($"Exit code: {exitCode}"); - Assert.True(exitCode == expectedAppExitCode, $"[{testCommand}] Exit code, expected {expectedAppExitCode} but got {exitCode}"); - } - - return output; - } - private string RunWasmTest(string projectName, string bundleDir, IDictionary? envVars=null, int expectedAppExitCode=0, int xharnessExitCode=0, string? appArgs=null) - => RunWithXHarness("wasm test", "wasm-test", projectName, bundleDir, - envVars: envVars, - expectedAppExitCode: expectedAppExitCode, - extraXHarnessArgs: "--js-file=runtime.js --engine=V8 -v trace", - appArgs: appArgs); - - private string RunWasmTestBrowser(string projectName, string bundleDir, IDictionary? envVars=null, int expectedAppExitCode=0, int xharnessExitCode=0, string? appArgs=null) - => RunWithXHarness("wasm test-browser", "wasm-test-browser", projectName, bundleDir, - envVars: envVars, - expectedAppExitCode: expectedAppExitCode, - extraXHarnessArgs: "-v trace", // needed to get messages like those for AOT loading - appArgs: appArgs); - - private static void InitProjectDir(string dir) - { - File.WriteAllText(Path.Combine(dir, "Directory.Build.props"), s_directoryBuildProps); - File.WriteAllText(Path.Combine(dir, "Directory.Build.targets"), s_directoryBuildTargets); - } - - private void BuildProject(string projectName, - string config, - string? extraBuildArgs = null, - string? extraProperties = null, - bool aot = false, - bool? dotnetWasmFromRuntimePack = null, - bool hasIcudt = true) - { - if (aot) - extraProperties = $"{extraProperties}\ntrue\n"; - - InitProjectDir(_tempDir); - - File.WriteAllText(Path.Combine(_tempDir, $"{projectName}.csproj"), -@$" - - {s_targetFramework} - Exe - true - runtime-test.js - {extraProperties ?? string.Empty} - -"); - - File.Copy(Path.Combine(AppContext.BaseDirectory, "runtime-test.js"), Path.Combine(_tempDir, "runtime-test.js")); - - StringBuilder sb = new(); - sb.Append("publish"); - sb.Append(s_defaultBuildArgs); - - sb.Append($" /p:Configuration={config}"); - - string logFilePath = Path.Combine(_logPath, $"{projectName}.binlog"); - _testOutput.WriteLine($"Binlog path: {logFilePath}"); - sb.Append($" /bl:\"{logFilePath}\" /v:minimal /nologo"); - if (extraBuildArgs != null) - sb.Append($" {extraBuildArgs} "); - - AssertBuild(sb.ToString()); - - string bundleDir = Path.Combine(GetBinDir(config: config), "AppBundle"); - AssertBasicAppBundle(bundleDir, projectName, config, hasIcudt); - - dotnetWasmFromRuntimePack ??= !aot; - AssertDotNetWasmJs(bundleDir, fromRuntimePack: dotnetWasmFromRuntimePack.Value); - } - - private static void AssertBasicAppBundle(string bundleDir, string projectName, string config, bool hasIcudt=true) - { - AssertFilesExist(bundleDir, new [] - { - "index.html", - "runtime.js", - "dotnet.timezones.blat", - "dotnet.wasm", - "mono-config.js", - "dotnet.js", - "run-v8.sh" - }); - - AssertFilesExist(bundleDir, new[] { "icudt.dat" }, expectToExist: hasIcudt); - - string managedDir = Path.Combine(bundleDir, "managed"); - AssertFilesExist(managedDir, new[] { $"{projectName}.dll" }); - - bool is_debug = 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}"); - //} - } - } - - private void AssertDotNetWasmJs(string bundleDir, bool fromRuntimePack) - { - string nativeDir = GetRuntimeNativeDir(); - - AssertFile(Path.Combine(nativeDir, "dotnet.wasm"), Path.Combine(bundleDir, "dotnet.wasm"), "Expected dotnet.wasm to be same as the runtime pack", same: fromRuntimePack); - AssertFile(Path.Combine(nativeDir, "dotnet.js"), Path.Combine(bundleDir, "dotnet.js"), "Expected dotnet.js to be same as the runtime pack", same: fromRuntimePack); - } - - private static void AssertFilesDontExist(string dir, string[] filenames, string? label = null) - => AssertFilesExist(dir, filenames, label, expectToExist: false); - - private static void AssertFilesExist(string dir, string[] filenames, string? label = null, bool expectToExist=true) - { - Assert.True(Directory.Exists(dir), $"[{label}] {dir} not found"); - foreach (string filename in filenames) - { - string path = Path.Combine(dir, filename); - - if (expectToExist) - { - Assert.True(File.Exists(path), - label != null - ? $"{label}: {path} doesn't exist" - : $"{path} doesn't exist"); - } - else - { - Assert.False(File.Exists(path), - label != null - ? $"{label}: {path} should not exist" - : $"{path} should not exist"); - } - } - } - - private static void AssertSameFile(string file0, string file1, string? label=null) => AssertFile(file0, file1, label, same: true); - private static void AssertNotSameFile(string file0, string file1, string? label=null) => AssertFile(file0, file1, label, same: false); - - private static void AssertFile(string file0, string file1, string? label=null, bool same=true) - { - Assert.True(File.Exists(file0), $"{label}: Expected to find {file0}"); - Assert.True(File.Exists(file1), $"{label}: Expected to find {file1}"); - - FileInfo finfo0 = new(file0); - FileInfo finfo1 = new(file1); - - if (same) - Assert.True(finfo0.Length == finfo1.Length, $"{label}: File sizes don't match for {file0} ({finfo0.Length}), and {file1} ({finfo1.Length})"); - else - Assert.True(finfo0.Length != finfo1.Length, $"{label}: File sizes should not match for {file0} ({finfo0.Length}), and {file1} ({finfo1.Length})"); - } - - private void AssertBuild(string args) - { - (int exitCode, _) = RunProcess("dotnet", args, workingDir: _tempDir, label: "build"); - Assert.True(0 == exitCode, $"Build process exited with non-zero exit code: {exitCode}"); - } - - private string GetObjDir(string targetFramework=s_targetFramework, string? baseDir=null, string config="Debug") - => Path.Combine(baseDir ?? _tempDir, "obj", config, targetFramework, "browser-wasm", "wasm"); - - private string GetBinDir(string targetFramework=s_targetFramework, string? baseDir=null, string config="Debug") - => Path.Combine(baseDir ?? _tempDir, "bin", config, targetFramework, "browser-wasm"); - - private string GetRuntimePackDir() => s_runtimePackDir; - - private string GetRuntimeNativeDir() - => Path.Combine(GetRuntimePackDir(), "runtimes", "browser-wasm", "native"); - - public void Dispose() - { - if (s_skipProjectCleanup) - return; - - try - { - Directory.Delete(_tempDir, recursive: true); - } - catch - { - Console.Error.WriteLine($"Failed to delete '{_tempDir}' during test cleanup"); - } - } + }", buildArgs, host, id); - private (int, string) RunProcess(string path, - string args = "", - IDictionary? envVars = null, - string? workingDir = null, - string? label = null, - bool logToXUnit = true) + void TestMain(string projectName, string programText, BuildArgs buildArgs, RunHost host, string id) { - _testOutput.WriteLine($"Running: {path} {args}"); - StringBuilder outputBuilder = new (); - var processStartInfo = new ProcessStartInfo - { - FileName = path, - UseShellExecute = false, - CreateNoWindow = true, - RedirectStandardError = true, - RedirectStandardOutput = true, - Arguments = args, - }; + buildArgs = buildArgs with { ProjectName = projectName }; + buildArgs = GetBuildArgsWith(buildArgs); - if (workingDir != null) - processStartInfo.WorkingDirectory = workingDir; + BuildProject(buildArgs, + initProject: () => File.WriteAllText(Path.Combine(_projectDir!, "Program.cs"), programText), + id: id); - if (envVars != null) - { - if (envVars.Count > 0) - _testOutput.WriteLine("Setting environment variables for execution:"); - - foreach (KeyValuePair envVar in envVars) - { - processStartInfo.EnvironmentVariables[envVar.Key] = envVar.Value; - _testOutput.WriteLine($"\t{envVar.Key} = {envVar.Value}"); - } - } - - Process? process = Process.Start(processStartInfo); - if (process == null) - throw new ArgumentException($"Process.Start({path} {args}) returned null process"); - - process.ErrorDataReceived += (sender, e) => LogData("[stderr]", e.Data); - process.OutputDataReceived += (sender, e) => LogData("[stdout]", e.Data); - - try - { - process.BeginOutputReadLine(); - process.BeginErrorReadLine(); - process.WaitForExit(); - - return (process.ExitCode, outputBuilder.ToString().Trim('\r', '\n')); - } - catch - { - Console.WriteLine(outputBuilder.ToString()); - throw; - } - - void LogData(string label, string? message) - { - if (logToXUnit && message != null) - { - _testOutput.WriteLine($"{label} {message}"); - } - outputBuilder.AppendLine($"{label} {message}"); - } + RunAndTestWasmApp(buildArgs, expectedExitCode: 42, + test: output => Assert.Contains("Hello, World!", output), host: host, id: id); } - - private static string s_directoryBuildProps = @" - - <_WasmTargetsDir Condition=""'$(RuntimeSrcDir)' != ''"">$(RuntimeSrcDir)\src\mono\wasm\build\ - <_WasmTargetsDir Condition=""'$(WasmBuildSupportDir)' != ''"">$(WasmBuildSupportDir)\wasm\ - $(WasmBuildSupportDir)\emsdk\ - - - - - - - PrepareForWasmBuild;$(WasmBuildAppDependsOn) - -"; - - private static string s_directoryBuildTargets = @" - - - - - - - - - - - - - - - - - - -"; - } } From af44fea669106d49b7f77e85f52a5069b5b9358f Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Wed, 10 Mar 2021 17:48:26 -0500 Subject: [PATCH 02/18] [wasm] Wasm.Build.Tests: Add test for relinking with InvariantGlobalization --- .../Wasm.Build.Tests/BuildTestBase.cs | 12 ++++++------ .../InvariantGlobalizationTests.cs | 18 +++++++++++++++--- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/src/tests/BuildWasmApps/Wasm.Build.Tests/BuildTestBase.cs b/src/tests/BuildWasmApps/Wasm.Build.Tests/BuildTestBase.cs index 02221889fde9ca..70e8448bc74583 100644 --- a/src/tests/BuildWasmApps/Wasm.Build.Tests/BuildTestBase.cs +++ b/src/tests/BuildWasmApps/Wasm.Build.Tests/BuildTestBase.cs @@ -244,7 +244,7 @@ protected static void InitProjectDir(string dir) "; - protected BuildArgs GetBuildArgsWith(BuildArgs buildArgs, string? extraProperties=null, string projectTemplate=SimpleProjectTemplate) + protected static BuildArgs GetBuildArgsWith(BuildArgs buildArgs, string? extraProperties=null, string projectTemplate=SimpleProjectTemplate) { if (buildArgs.AOT) extraProperties = $"{extraProperties}\ntrue\n"; @@ -295,7 +295,7 @@ public string BuildProject(BuildArgs buildArgs, try { - AssertBuild(sb.ToString()); + AssertBuild(sb.ToString(), id); if (useCache) { _buildContext.CacheBuild(buildArgs, new BuildProduct(_projectDir, logFilePath, true)); @@ -403,9 +403,9 @@ protected static void AssertFile(string file0, string file1, string? label=null, Assert.True(finfo0.Length != finfo1.Length, $"{label}: File sizes should not match for {file0} ({finfo0.Length}), and {file1} ({finfo1.Length})"); } - protected void AssertBuild(string args) + protected void AssertBuild(string args, string label="build") { - (int exitCode, _) = RunProcess("dotnet", _testOutput, args, workingDir: _projectDir, label: "build"); + (int exitCode, _) = RunProcess("dotnet", _testOutput, args, workingDir: _projectDir, label: label); Assert.True(0 == exitCode, $"Build process exited with non-zero exit code: {exitCode}"); } @@ -469,8 +469,8 @@ public static (int, string) RunProcess(string path, process.StartInfo = processStartInfo; process.EnableRaisingEvents = true; - process.ErrorDataReceived += (sender, e) => LogData("[stderr]", e.Data); - process.OutputDataReceived += (sender, e) => LogData("[stdout]", e.Data); + process.ErrorDataReceived += (sender, e) => LogData($"[{label}-stderr]", e.Data); + process.OutputDataReceived += (sender, e) => LogData($"[{label}]", e.Data); // AutoResetEvent resetEvent = new (false); // process.Exited += (_, _) => { Console.WriteLine ($"- exited called"); resetEvent.Set(); }; diff --git a/src/tests/BuildWasmApps/Wasm.Build.Tests/InvariantGlobalizationTests.cs b/src/tests/BuildWasmApps/Wasm.Build.Tests/InvariantGlobalizationTests.cs index 66ba088c7802b8..f7678a1db86902 100644 --- a/src/tests/BuildWasmApps/Wasm.Build.Tests/InvariantGlobalizationTests.cs +++ b/src/tests/BuildWasmApps/Wasm.Build.Tests/InvariantGlobalizationTests.cs @@ -23,12 +23,23 @@ public InvariantGlobalizationTests(ITestOutputHelper output, SharedBuildPerTestC [Theory] [MemberData(nameof(InvariantGlobalizationTestData), parameters: new object[] { /*aot*/ false, RunHost.All })] [MemberData(nameof(InvariantGlobalizationTestData), parameters: new object[] { /*aot*/ true, RunHost.All })] - public void InvariantGlobalization(BuildArgs buildArgs, bool? invariantGlobalization, RunHost host, string id) + public void AOT_InvariantGlobalization(BuildArgs buildArgs, bool? invariantGlobalization, RunHost host, string id) + => TestInvariantGlobalization(buildArgs, invariantGlobalization, host, id); + + // TODO: What else should we use to verify a relinked build? + [Theory] + [MemberData(nameof(InvariantGlobalizationTestData), parameters: new object[] { /*aot*/ false, RunHost.All })] + public void RelinkingWithoutAOT(BuildArgs buildArgs, bool? invariantGlobalization, RunHost host, string id) + => TestInvariantGlobalization(buildArgs, invariantGlobalization, host, id, + extraProperties: "true", + dotnetWasmFromRuntimePack: false); + + private void TestInvariantGlobalization(BuildArgs buildArgs, bool? invariantGlobalization, + RunHost host, string id, string extraProperties="", bool dotnetWasmFromRuntimePack=true) { string projectName = $"invariant_{invariantGlobalization?.ToString() ?? "unset"}"; - string? extraProperties = null; if (invariantGlobalization != null) - extraProperties = $"{invariantGlobalization}"; + extraProperties = $"{extraProperties}{invariantGlobalization}"; buildArgs = buildArgs with { ProjectName = projectName }; buildArgs = GetBuildArgsWith(buildArgs, extraProperties); @@ -48,6 +59,7 @@ public static int Main() BuildProject(buildArgs, initProject: () => File.WriteAllText(Path.Combine(_projectDir!, "Program.cs"), programText), id: id, + dotnetWasmFromRuntimePack: dotnetWasmFromRuntimePack, hasIcudt: invariantGlobalization == null || invariantGlobalization.Value == false); RunAndTestWasmApp(buildArgs, expectedExitCode: 42, From 1b102b34edb2f1bc785bc8360e253f4036f9a5ea Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Thu, 11 Mar 2021 02:49:21 -0500 Subject: [PATCH 03/18] [wasm] Wasm.Build.Tests - Check `CultureInfo` for invariant culture .. tests. Code stolen from @maximlipin's https://github.com/dotnet/runtime/pull/49204/ --- .../Wasm.Build.Tests/InvariantGlobalizationTests.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/tests/BuildWasmApps/Wasm.Build.Tests/InvariantGlobalizationTests.cs b/src/tests/BuildWasmApps/Wasm.Build.Tests/InvariantGlobalizationTests.cs index f7678a1db86902..d3ab78e56ddd54 100644 --- a/src/tests/BuildWasmApps/Wasm.Build.Tests/InvariantGlobalizationTests.cs +++ b/src/tests/BuildWasmApps/Wasm.Build.Tests/InvariantGlobalizationTests.cs @@ -46,12 +46,15 @@ private void TestInvariantGlobalization(BuildArgs buildArgs, bool? invariantGlob string programText = @" using System; + using System.Globalization; using System.Threading.Tasks; public class TestClass { public static int Main() { - Console.WriteLine(""Hello, World!""); + var culture = new CultureInfo(""en-ES"", false); + // https://github.com/dotnet/runtime/blob/main/docs/design/features/globalization-invariant-mode.md#cultures-and-culture-data + Console.WriteLine($""{culture.LCID == 0x1000} - {culture.NativeName}""); return 42; } }"; @@ -62,8 +65,11 @@ public static int Main() dotnetWasmFromRuntimePack: dotnetWasmFromRuntimePack, hasIcudt: invariantGlobalization == null || invariantGlobalization.Value == false); + string expectedOutputString = invariantGlobalization == true + ? "False - en (ES)" + : "True - Invariant Language (Invariant Country)"; RunAndTestWasmApp(buildArgs, expectedExitCode: 42, - test: output => Assert.Contains("Hello, World!", output), host: host, id: id); + test: output => Assert.Contains(expectedOutputString, output), host: host, id: id); } } } From 33d87f2543f7d36c89e15358b74d9250aa283ef7 Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Thu, 11 Mar 2021 03:07:14 -0500 Subject: [PATCH 04/18] fix invariant+aot test --- .../Wasm.Build.Tests/InvariantGlobalizationTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tests/BuildWasmApps/Wasm.Build.Tests/InvariantGlobalizationTests.cs b/src/tests/BuildWasmApps/Wasm.Build.Tests/InvariantGlobalizationTests.cs index d3ab78e56ddd54..8ea9d1600b829c 100644 --- a/src/tests/BuildWasmApps/Wasm.Build.Tests/InvariantGlobalizationTests.cs +++ b/src/tests/BuildWasmApps/Wasm.Build.Tests/InvariantGlobalizationTests.cs @@ -24,7 +24,7 @@ public InvariantGlobalizationTests(ITestOutputHelper output, SharedBuildPerTestC [MemberData(nameof(InvariantGlobalizationTestData), parameters: new object[] { /*aot*/ false, RunHost.All })] [MemberData(nameof(InvariantGlobalizationTestData), parameters: new object[] { /*aot*/ true, RunHost.All })] public void AOT_InvariantGlobalization(BuildArgs buildArgs, bool? invariantGlobalization, RunHost host, string id) - => TestInvariantGlobalization(buildArgs, invariantGlobalization, host, id); + => TestInvariantGlobalization(buildArgs, invariantGlobalization, host, id, dotnetWasmFromRuntimePack: !buildArgs.AOT); // TODO: What else should we use to verify a relinked build? [Theory] From 995cc89676aa7d867f639fbcf1c47047a02cb038 Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Thu, 11 Mar 2021 15:49:00 -0500 Subject: [PATCH 05/18] [wasm] Fix the order of include paths For AOT we generate `pinvoke-table.h` in the obj directory. But there is one present in the runtime pack too. In my earlier changes the order in which these were passed as include search paths was changed from: `"-I/runtime/pack/microsoft.netcore.app.runtime.browser-wasm/Release/runtimes/browser-wasm/native/include/wasm" "-Iartifacts/obj/mono/Wasm.Console.Sample/wasm/Release/browser-wasm/wasm/"` .. which meant that the one from the runtime pack took precedence, and got used. So, fix the order! And change the property names to indicate where they are sourced from. --- src/mono/wasm/build/WasmApp.targets | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/mono/wasm/build/WasmApp.targets b/src/mono/wasm/build/WasmApp.targets index 34732b71be2dc4..7807e1107b85f7 100644 --- a/src/mono/wasm/build/WasmApp.targets +++ b/src/mono/wasm/build/WasmApp.targets @@ -278,8 +278,8 @@ OutputPath="$(_WasmIntermediateOutputPath)icall-table.h" /> $(EmccFlags) -DLINK_ICALLS=1 - <_WasmIncludeDir>$([MSBuild]::NormalizeDirectory($(MicrosoftNetCoreAppRuntimePackRidNativeDir), 'include')) - <_WasmSrcDir>$([MSBuild]::NormalizeDirectory($(MicrosoftNetCoreAppRuntimePackRidNativeDir), 'src')) + <_WasmRuntimePackIncludeDir>$([MSBuild]::NormalizeDirectory($(MicrosoftNetCoreAppRuntimePackRidNativeDir), 'include')) + <_WasmRuntimePackSrcDir>$([MSBuild]::NormalizeDirectory($(MicrosoftNetCoreAppRuntimePackRidNativeDir), 'src')) @@ -298,15 +298,15 @@ <_WasmObjectsToBuild Include="$(_WasmIntermediateOutputPath)driver.o"/> <_WasmObjectsToBuild Include="$(_WasmIntermediateOutputPath)pinvoke.o"/> <_WasmObjectsToBuild Include="$(_WasmIntermediateOutputPath)corebindings.o"/> - <_WasmObjectsToBuild SourcePath="$(_WasmSrcDir)%(FileName).c" /> + <_WasmObjectsToBuild SourcePath="$(_WasmRuntimePackSrcDir)%(FileName).c" /> <_WasmObjects Include="@(_WasmRuntimePackNativeLibs->'$(MicrosoftNetCoreAppRuntimePackRidNativeDir)%(FileName)%(Extension)')" /> <_WasmObjects Include="@(_WasmObjectsToBuild)" /> - <_DotnetJSSrcFile Include="$(_WasmSrcDir)library_mono.js" /> - <_DotnetJSSrcFile Include="$(_WasmSrcDir)binding_support.js" /> - <_DotnetJSSrcFile Include="$(_WasmSrcDir)dotnet_support.js" /> - <_DotnetJSSrcFile Include="$(_WasmSrcDir)pal_random.js" /> + <_DotnetJSSrcFile Include="$(_WasmRuntimePackSrcDir)library_mono.js" /> + <_DotnetJSSrcFile Include="$(_WasmRuntimePackSrcDir)binding_support.js" /> + <_DotnetJSSrcFile Include="$(_WasmRuntimePackSrcDir)dotnet_support.js" /> + <_DotnetJSSrcFile Include="$(_WasmRuntimePackSrcDir)pal_random.js" /> <_AOTAssemblies Include="@(_WasmAssembliesInternal)" Condition="'%(_WasmAssembliesInternal._InternalForceInterpret)' != 'true'" /> <_BitcodeFile Include="%(_WasmAssembliesInternal.LlvmBitcodeFile)" /> @@ -316,7 +316,7 @@ Text="Bug: Number of aot assemblies doesn't match the number of generated bitcode files. BitcodeFiles: @(_BitcodeFile->Count()) vs Assemblies: @(_AOTAssemblies->Count())" /> - $(EmccFlags) -DCORE_BINDINGS -DGEN_PINVOKE=1 "-I$(_WasmIncludeDir)mono-2.0" "-I$(_WasmIncludeDir)wasm" + $(EmccFlags) -DCORE_BINDINGS -DGEN_PINVOKE=1 $(EmccCFlags) -g From 3cd29e0cde48d04425dfb04b2c57cf13f957dbd0 Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Thu, 11 Mar 2021 18:15:34 -0500 Subject: [PATCH 07/18] [wasm] Fallback to `dotnet xharness` if `XHARNESS_CLI_PATH` is not set. The environment variable is set on helix. During local testing it can be useful when using a locally built xharness. --- .../BuildWasmApps/Wasm.Build.Tests/BuildTestBase.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/tests/BuildWasmApps/Wasm.Build.Tests/BuildTestBase.cs b/src/tests/BuildWasmApps/Wasm.Build.Tests/BuildTestBase.cs index cc6fb1c71759a0..c636870f4e9804 100644 --- a/src/tests/BuildWasmApps/Wasm.Build.Tests/BuildTestBase.cs +++ b/src/tests/BuildWasmApps/Wasm.Build.Tests/BuildTestBase.cs @@ -93,10 +93,12 @@ static BuildTestBase() string? harnessVar = Environment.GetEnvironmentVariable(XHarnessRunnerCommandEnvVar); if (string.IsNullOrEmpty(harnessVar)) { - throw new Exception($"{XHarnessRunnerCommandEnvVar} not set"); + s_xharnessRunnerCommand = "xharness"; + } + else + { + s_xharnessRunnerCommand = $"exec {harnessVar}"; } - - s_xharnessRunnerCommand = harnessVar; } public BuildTestBase(ITestOutputHelper output, SharedBuildPerTestClassFixture buildContext) @@ -183,7 +185,7 @@ protected static string RunWithXHarness(string testCommand, string testLogPath, Directory.CreateDirectory(testLogPath); StringBuilder args = new(); - args.Append($"exec {s_xharnessRunnerCommand}"); + args.Append(s_xharnessRunnerCommand); args.Append($" {testCommand}"); args.Append($" --app=."); args.Append($" --output-directory={testLogPath}"); From 739881fd20d7502d1d0218b21036f5f0210eeeee Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Thu, 11 Mar 2021 18:17:07 -0500 Subject: [PATCH 08/18] [wasm] fix invariant test - 'en-ES' -> 'es-ES' --- .../Wasm.Build.Tests/InvariantGlobalizationTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tests/BuildWasmApps/Wasm.Build.Tests/InvariantGlobalizationTests.cs b/src/tests/BuildWasmApps/Wasm.Build.Tests/InvariantGlobalizationTests.cs index 8ea9d1600b829c..e0d17939b5cd56 100644 --- a/src/tests/BuildWasmApps/Wasm.Build.Tests/InvariantGlobalizationTests.cs +++ b/src/tests/BuildWasmApps/Wasm.Build.Tests/InvariantGlobalizationTests.cs @@ -52,7 +52,7 @@ private void TestInvariantGlobalization(BuildArgs buildArgs, bool? invariantGlob public class TestClass { public static int Main() { - var culture = new CultureInfo(""en-ES"", false); + var culture = new CultureInfo(""es-ES"", false); // https://github.com/dotnet/runtime/blob/main/docs/design/features/globalization-invariant-mode.md#cultures-and-culture-data Console.WriteLine($""{culture.LCID == 0x1000} - {culture.NativeName}""); return 42; From fcf0da694dfc37c8d67328743b29f20d8c56e02b Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Thu, 11 Mar 2021 18:51:51 -0500 Subject: [PATCH 09/18] [wasm] RunWithEmSdkEnv: log the working directory also --- src/tasks/WasmAppBuilder/RunWithEmSdkEnv.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/tasks/WasmAppBuilder/RunWithEmSdkEnv.cs b/src/tasks/WasmAppBuilder/RunWithEmSdkEnv.cs index dfc2e191c2b915..eb1fe1f712f880 100644 --- a/src/tasks/WasmAppBuilder/RunWithEmSdkEnv.cs +++ b/src/tasks/WasmAppBuilder/RunWithEmSdkEnv.cs @@ -36,6 +36,9 @@ public override bool Execute() Command = $"bash -c 'source {envScriptPath} > /dev/null 2>&1 && {Command}'"; } + + var workingDir = string.IsNullOrEmpty(WorkingDirectory) ? Directory.GetCurrentDirectory() : WorkingDirectory; + Log.LogMessage(MessageImportance.Low, $"Working directory: {workingDir}"); Log.LogMessage(MessageImportance.Low, $"Using Command: {Command}"); return base.Execute() && !Log.HasLoggedErrors; From 1ce676fe6c24649006b38f12f495004fbbb7c39d Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Thu, 11 Mar 2021 19:18:47 -0500 Subject: [PATCH 10/18] [wasm] Re-enable wasm build tests --- eng/pipelines/runtime.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/eng/pipelines/runtime.yml b/eng/pipelines/runtime.yml index 2f600b5929e1bc..e1a54ae86e30ff 100644 --- a/eng/pipelines/runtime.yml +++ b/eng/pipelines/runtime.yml @@ -297,6 +297,7 @@ jobs: scenarios: - normal - wasmtestonbrowser + - buildwasmapps condition: >- or( eq(variables['librariesContainsChange'], true), From 522424aaa2395f87a9cf9b197b0d9d5821f71995 Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Mon, 15 Mar 2021 16:05:42 -0400 Subject: [PATCH 11/18] [wasm] Add regression test for issue #49588 --- .../Wasm.Build.Tests/WasmBuildAppTest.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/tests/BuildWasmApps/Wasm.Build.Tests/WasmBuildAppTest.cs b/src/tests/BuildWasmApps/Wasm.Build.Tests/WasmBuildAppTest.cs index a94cd5c64edc83..2355539d6d5390 100644 --- a/src/tests/BuildWasmApps/Wasm.Build.Tests/WasmBuildAppTest.cs +++ b/src/tests/BuildWasmApps/Wasm.Build.Tests/WasmBuildAppTest.cs @@ -61,6 +61,18 @@ public static int Main() } }", buildArgs, host, id); + [Theory] + [MemberData(nameof(MainMethodTestData), parameters: new object[] { /*aot*/ true, RunHost.All })] + public void Bug49588_RegressionTest(BuildArgs buildArgs, RunHost host, string id) + => TestMain("bug49588", @" + public class TestClass { + public static int Main() + { + System.Console.WriteLine($""tc: {Environment.TickCount}, tc64: {Environment.TickCount64}""); + return 42; + } + }", buildArgs, host, id); + void TestMain(string projectName, string programText, BuildArgs buildArgs, RunHost host, string id) { buildArgs = buildArgs with { ProjectName = projectName }; From aae95e21fb675b5fd6b7d91e1b5bbbe9a7632716 Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Tue, 16 Mar 2021 13:13:02 -0400 Subject: [PATCH 12/18] fix test --- src/tests/BuildWasmApps/Wasm.Build.Tests/WasmBuildAppTest.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/tests/BuildWasmApps/Wasm.Build.Tests/WasmBuildAppTest.cs b/src/tests/BuildWasmApps/Wasm.Build.Tests/WasmBuildAppTest.cs index 2355539d6d5390..7802676bfce8db 100644 --- a/src/tests/BuildWasmApps/Wasm.Build.Tests/WasmBuildAppTest.cs +++ b/src/tests/BuildWasmApps/Wasm.Build.Tests/WasmBuildAppTest.cs @@ -65,10 +65,11 @@ public static int Main() [MemberData(nameof(MainMethodTestData), parameters: new object[] { /*aot*/ true, RunHost.All })] public void Bug49588_RegressionTest(BuildArgs buildArgs, RunHost host, string id) => TestMain("bug49588", @" + using Sytem; public class TestClass { public static int Main() { - System.Console.WriteLine($""tc: {Environment.TickCount}, tc64: {Environment.TickCount64}""); + Console.WriteLine($""tc: {Environment.TickCount}, tc64: {Environment.TickCount64}""); return 42; } }", buildArgs, host, id); From 3540462b317aee218edebfff805970b6d8570918 Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Wed, 17 Mar 2021 01:41:43 -0400 Subject: [PATCH 13/18] [wasm] Cleanup, and add more tests --- src/mono/wasm/build/WasmApp.targets | 14 ++-- .../Wasm.Build.Tests/BuildAndRunAttribute.cs | 40 +++++++++++ .../Wasm.Build.Tests/BuildTestBase.cs | 55 ++++++++++----- .../Wasm.Build.Tests/HelperExtensions.cs | 32 +++++++-- .../InvariantGlobalizationTests.cs | 11 ++- .../Wasm.Build.Tests/MainWithArgsTests.cs | 7 +- .../Wasm.Build.Tests/NativeBuildTests.cs | 69 +++++++++++++++++++ .../BuildWasmApps/Wasm.Build.Tests/RunHost.cs | 1 + .../Wasm.Build.Tests/WasmBuildAppTest.cs | 59 ++++++++++++---- 9 files changed, 243 insertions(+), 45 deletions(-) create mode 100644 src/tests/BuildWasmApps/Wasm.Build.Tests/BuildAndRunAttribute.cs create mode 100644 src/tests/BuildWasmApps/Wasm.Build.Tests/NativeBuildTests.cs diff --git a/src/mono/wasm/build/WasmApp.targets b/src/mono/wasm/build/WasmApp.targets index e0b846260692a7..968fc2d5c8b6f3 100644 --- a/src/mono/wasm/build/WasmApp.targets +++ b/src/mono/wasm/build/WasmApp.targets @@ -77,8 +77,8 @@ - - + @@ -199,6 +199,11 @@ + + + true false @@ -265,7 +270,8 @@ - + $([MSBuild]::NormalizePath($(MicrosoftNetCoreAppRuntimePackRidNativeDir), 'src', 'emcc-flags.txt')) @@ -406,7 +412,7 @@ EMSCRIPTEN_KEEPALIVE void mono_wasm_load_profiler_aot (const char *desc) { mono_ - + diff --git a/src/tests/BuildWasmApps/Wasm.Build.Tests/BuildAndRunAttribute.cs b/src/tests/BuildWasmApps/Wasm.Build.Tests/BuildAndRunAttribute.cs new file mode 100644 index 00000000000000..46e7215505f6ab --- /dev/null +++ b/src/tests/BuildWasmApps/Wasm.Build.Tests/BuildAndRunAttribute.cs @@ -0,0 +1,40 @@ +// 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.Linq; +using System.Reflection; +using Xunit.Sdk; + +#nullable enable + +namespace Wasm.Build.Tests +{ + /// + /// Example usage: + /// [BuildAndRun(aot: true, parameters: new object[] { arg1, arg2 })] + /// public void Test(BuildArgs, arg1, arg2, RunHost, id) + /// + [DataDiscoverer("Xunit.Sdk.DataDiscoverer", "xunit.core")] + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = true)] + public class BuildAndRunAttribute : DataAttribute + { + private bool _aot; + private RunHost _host; + private object?[] _parameters; + + public BuildAndRunAttribute(bool aot=false, RunHost host = RunHost.All, params object?[] parameters) + { + this._aot = aot; + this._host = host; + this._parameters = parameters; + } + + public override IEnumerable GetData(MethodInfo testMethod) + => BuildTestBase.ConfigWithAOTData(_aot) + .Multiply(_parameters) + .WithRunHosts(_host) + .UnwrapItemsAsArrays().ToList().Dump(); + } +} diff --git a/src/tests/BuildWasmApps/Wasm.Build.Tests/BuildTestBase.cs b/src/tests/BuildWasmApps/Wasm.Build.Tests/BuildTestBase.cs index c636870f4e9804..2a73ce7a8ebbc1 100644 --- a/src/tests/BuildWasmApps/Wasm.Build.Tests/BuildTestBase.cs +++ b/src/tests/BuildWasmApps/Wasm.Build.Tests/BuildTestBase.cs @@ -131,8 +131,13 @@ public BuildTestBase(ITestOutputHelper output, SharedBuildPerTestClassFixture bu new object?[] { new BuildArgs("placeholder", "Release", aot, "placeholder", string.Empty) }.AsEnumerable() }.AsEnumerable(); - public static IEnumerable> ConfigWithAOTData(bool aot, RunHost host) - => ConfigWithAOTData(aot).WithRunHosts(host); + public static IEnumerable BuildAndRunData(bool aot = false, + RunHost host = RunHost.All, + params object[] parameters) + => ConfigWithAOTData(aot) + .Multiply(parameters) + .WithRunHosts(host) + .UnwrapItemsAsArrays(); protected void RunAndTestWasmApp(BuildArgs buildArgs, RunHost host, string id, Action test, string? buildDir=null, int expectedExitCode=0, string? args=null) { @@ -257,12 +262,13 @@ protected static BuildArgs GetBuildArgsWith(BuildArgs buildArgs, string? extraPr return buildArgs with { ProjectFileContents = projectContents }; } - public string BuildProject(BuildArgs buildArgs, + public (string projectDir, string buildOutput) BuildProject(BuildArgs buildArgs, Action initProject, string id, bool? dotnetWasmFromRuntimePack = null, bool hasIcudt = true, - bool useCache = true) + bool useCache = true, + bool expectSuccess = true) { if (useCache && _buildContext.TryGetBuildFor(buildArgs, out BuildProduct? product)) { @@ -273,7 +279,7 @@ public string BuildProject(BuildArgs buildArgs, // use this test's id for the run logs _logPath = Path.Combine(s_logRoot, id); - return _projectDir; + return (_projectDir, "FIXME"); } InitPaths(id); @@ -297,14 +303,24 @@ public string BuildProject(BuildArgs buildArgs, Console.WriteLine($"Building {buildArgs.ProjectName} in {_projectDir}"); + (int exitCode, string buildOutput) result; try { - AssertBuild(sb.ToString(), id); + result = AssertBuild(sb.ToString(), id, expectSuccess: expectSuccess); + if (expectSuccess) + { + string bundleDir = Path.Combine(GetBinDir(config: buildArgs.Config), "AppBundle"); + dotnetWasmFromRuntimePack ??= !buildArgs.AOT; + AssertBasicAppBundle(bundleDir, buildArgs.ProjectName, buildArgs.Config, hasIcudt, dotnetWasmFromRuntimePack.Value); + } + if (useCache) { _buildContext.CacheBuild(buildArgs, new BuildProduct(_projectDir, logFilePath, true)); Console.WriteLine($"caching build for {buildArgs}"); } + + return (_projectDir, result.buildOutput); } catch { @@ -312,12 +328,6 @@ public string BuildProject(BuildArgs buildArgs, _buildContext.CacheBuild(buildArgs, new BuildProduct(_projectDir, logFilePath, false)); throw; } - - string bundleDir = Path.Combine(GetBinDir(config: buildArgs.Config), "AppBundle"); - dotnetWasmFromRuntimePack ??= !buildArgs.AOT; - AssertBasicAppBundle(bundleDir, buildArgs.ProjectName, buildArgs.Config, hasIcudt, dotnetWasmFromRuntimePack.Value); - - return _projectDir; } protected static void AssertBasicAppBundle(string bundleDir, string projectName, string config, bool hasIcudt=true, bool dotnetWasmFromRuntimePack=true) @@ -407,10 +417,15 @@ protected static void AssertFile(string file0, string file1, string? label=null, Assert.True(finfo0.Length != finfo1.Length, $"{label}: File sizes should not match for {file0} ({finfo0.Length}), and {file1} ({finfo1.Length})"); } - protected void AssertBuild(string args, string label="build") + protected (int exitCode, string buildOutput) AssertBuild(string args, string label="build", bool expectSuccess=true) { - (int exitCode, _) = RunProcess("dotnet", _testOutput, args, workingDir: _projectDir, label: label); - Assert.True(0 == exitCode, $"Build process exited with non-zero exit code: {exitCode}"); + var result = RunProcess("dotnet", _testOutput, args, workingDir: _projectDir, label: label); + if (expectSuccess) + Assert.True(0 == result.exitCode, $"Build process exited with non-zero exit code: {result.exitCode}"); + else + Assert.True(0 != result.exitCode, $"Build should have failed, but it didn't. Process exited with exitCode : {result.exitCode}"); + + return result; } // protected string GetObjDir(string targetFramework=s_targetFramework, string? baseDir=null, string config="Debug") @@ -429,7 +444,7 @@ protected static string GetRuntimeNativeDir() => Path.Combine(GetRuntimePackDir(), "runtimes", "browser-wasm", "native"); - public static (int, string) RunProcess(string path, + public static (int exitCode, string buildOutput) RunProcess(string path, ITestOutputHelper _testOutput, string args = "", IDictionary? envVars = null, @@ -517,6 +532,14 @@ public void Dispose() _buildContext.RemoveFromCache(_projectDir); } + protected static string s_mainReturns42 = @" + public class TestClass { + public static int Main() + { + return 42; + } + }"; + protected static string s_directoryBuildProps = @" <_WasmTargetsDir Condition=""'$(RuntimeSrcDir)' != ''"">$(RuntimeSrcDir)\src\mono\wasm\build\ diff --git a/src/tests/BuildWasmApps/Wasm.Build.Tests/HelperExtensions.cs b/src/tests/BuildWasmApps/Wasm.Build.Tests/HelperExtensions.cs index 930afc69d038e9..e3d1ca3dd37b2a 100644 --- a/src/tests/BuildWasmApps/Wasm.Build.Tests/HelperExtensions.cs +++ b/src/tests/BuildWasmApps/Wasm.Build.Tests/HelperExtensions.cs @@ -6,6 +6,8 @@ using System.Linq; using System.IO; +#nullable enable + namespace Wasm.Build.Tests { public static class HelperExtensions @@ -13,6 +15,18 @@ public static class HelperExtensions public static IEnumerable UnwrapItemsAsArrays(this IEnumerable> enumerable) => enumerable.Select(e => e.ToArray()); + public static IEnumerable Dump(this IEnumerable enumerable) + { + foreach (var row in enumerable) + { + Console.WriteLine ("{"); + foreach (var param in row) + Console.WriteLine ($"\t{param}"); + Console.WriteLine ("}"); + } + return enumerable; + } + /// /// Cartesian product /// @@ -36,16 +50,23 @@ public static class HelperExtensions /// /// /// - /// + /// /// - public static IEnumerable> Multiply(this IEnumerable> data, object?[] options) - => data?.SelectMany(d => options.Select(o => d.Append(o))); + public static IEnumerable> Multiply(this IEnumerable> data, params object?[][] rowsWithColumnArrays) + => data.SelectMany(row => + rowsWithColumnArrays.Select(new_cols => row.Concat(new_cols))); public static object?[] Enumerate(this RunHost host) { + if (host == RunHost.None) + return Array.Empty(); + var list = new List(); foreach (var value in Enum.GetValues()) { + if (value == RunHost.None) + continue; + // Ignore any combos like RunHost.All from Enum.GetValues // by ignoring any @value that has more than 1 bit set if (((int)value & ((int)value - 1)) != 0) @@ -60,7 +81,10 @@ public static class HelperExtensions public static IEnumerable> WithRunHosts(this IEnumerable> data, RunHost hosts) { IEnumerable hostsEnumerable = hosts.Enumerate(); - return data?.SelectMany(d => + if (hosts == RunHost.None) + return data.Select(d => d.Append((object?) Path.GetRandomFileName())); + + return data.SelectMany(d => { string runId = Path.GetRandomFileName(); return hostsEnumerable.Select(o => diff --git a/src/tests/BuildWasmApps/Wasm.Build.Tests/InvariantGlobalizationTests.cs b/src/tests/BuildWasmApps/Wasm.Build.Tests/InvariantGlobalizationTests.cs index e0d17939b5cd56..2107cf07e9b710 100644 --- a/src/tests/BuildWasmApps/Wasm.Build.Tests/InvariantGlobalizationTests.cs +++ b/src/tests/BuildWasmApps/Wasm.Build.Tests/InvariantGlobalizationTests.cs @@ -1,9 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using System.Collections.Generic; using System.IO; -using System.Text; using Xunit; using Xunit.Abstractions; +#nullable enable + namespace Wasm.Build.Tests { public class InvariantGlobalizationTests : BuildTestBase @@ -15,7 +19,10 @@ public InvariantGlobalizationTests(ITestOutputHelper output, SharedBuildPerTestC public static IEnumerable InvariantGlobalizationTestData(bool aot, RunHost host) => ConfigWithAOTData(aot) - .Multiply(new object?[] { null, false, true }) + .Multiply( + new object?[] { null }, + new object?[] { false }, + new object?[] { true }) .WithRunHosts(host) .UnwrapItemsAsArrays(); diff --git a/src/tests/BuildWasmApps/Wasm.Build.Tests/MainWithArgsTests.cs b/src/tests/BuildWasmApps/Wasm.Build.Tests/MainWithArgsTests.cs index e932fefa85f1b8..f0ff1437c98610 100644 --- a/src/tests/BuildWasmApps/Wasm.Build.Tests/MainWithArgsTests.cs +++ b/src/tests/BuildWasmApps/Wasm.Build.Tests/MainWithArgsTests.cs @@ -16,15 +16,12 @@ public class MainWithArgsTests : BuildTestBase public MainWithArgsTests(ITestOutputHelper output, SharedBuildPerTestClassFixture buildContext) : base(output, buildContext) { - _buildContext = buildContext; } public static IEnumerable MainWithArgsTestData(bool aot, RunHost host) => ConfigWithAOTData(aot).Multiply( - new object?[] { - new object?[] { "abc", "foobar"}, - new object?[0] - } + new object?[] { new object?[] { "abc", "foobar"} }, + new object?[] { new object?[0] } ).WithRunHosts(host).UnwrapItemsAsArrays(); [Theory] diff --git a/src/tests/BuildWasmApps/Wasm.Build.Tests/NativeBuildTests.cs b/src/tests/BuildWasmApps/Wasm.Build.Tests/NativeBuildTests.cs new file mode 100644 index 00000000000000..be52074057eb92 --- /dev/null +++ b/src/tests/BuildWasmApps/Wasm.Build.Tests/NativeBuildTests.cs @@ -0,0 +1,69 @@ +// 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 Xunit; +using Xunit.Abstractions; + +#nullable enable + +namespace Wasm.Build.Tests +{ + public class NativeBuildTests : BuildTestBase + { + public NativeBuildTests(ITestOutputHelper output, SharedBuildPerTestClassFixture buildContext) + : base(output, buildContext) + { + } + + // TODO: - check dotnet.wasm, js have changed + // - icall? pinvoke? + // + + [Theory] + [BuildAndRun] + public void SimpleNativeBuild(BuildArgs buildArgs, RunHost host, string id) + => NativeBuild("simple_native_build", s_mainReturns42, buildArgs, host, id); + + [Theory] + [BuildAndRun(host: RunHost.None, parameters: new object[] + { "", "error : Cannot find emscripten sdk, required for native relinking. $(EMSDK_PATH)=" })] + [BuildAndRun(host: RunHost.None, parameters: new object[] + { "/non-existant/foo", "error : Cannot find emscripten sdk, required for native relinking. $(EMSDK_PATH)=/non-existant/foo" })] + public void Relinking_ErrorWhenMissingEMSDK(BuildArgs buildArgs, string emsdkPath, string errorMessage, string id) + { + string projectName = $"simple_native_build"; + buildArgs = buildArgs with { + ProjectName = projectName, + ExtraBuildArgs = $"/p:EMSDK_PATH={emsdkPath}" + }; + buildArgs = GetBuildArgsWith(buildArgs, extraProperties: "true"); + + (_, string buildOutput) = BuildProject(buildArgs, + initProject: () => File.WriteAllText(Path.Combine(_projectDir!, "Program.cs"), s_mainReturns42), + id: id, + expectSuccess: false); + + Assert.Contains(errorMessage, buildOutput); + } + + private void NativeBuild(string projectNamePrefix, string projectContents, BuildArgs buildArgs, RunHost host, string id) + { + string projectName = $"{projectNamePrefix}_{buildArgs.Config}_{buildArgs.AOT}"; + + buildArgs = buildArgs with { ProjectName = projectName, ProjectFileContents = projectContents }; + buildArgs = GetBuildArgsWith(buildArgs, extraProperties: "true"); + Console.WriteLine ($"-- args: {buildArgs}, name: {projectName}"); + + BuildProject(buildArgs, + initProject: () => File.WriteAllText(Path.Combine(_projectDir!, "Program.cs"), projectContents), + dotnetWasmFromRuntimePack: false, + id: id); + + RunAndTestWasmApp(buildArgs, buildDir: _projectDir, expectedExitCode: 42, + test: output => {}, + host: host, id: id); + } + } +} diff --git a/src/tests/BuildWasmApps/Wasm.Build.Tests/RunHost.cs b/src/tests/BuildWasmApps/Wasm.Build.Tests/RunHost.cs index b7326a6332bd48..0aa1ae14ea8120 100644 --- a/src/tests/BuildWasmApps/Wasm.Build.Tests/RunHost.cs +++ b/src/tests/BuildWasmApps/Wasm.Build.Tests/RunHost.cs @@ -8,6 +8,7 @@ namespace Wasm.Build.Tests [Flags] public enum RunHost { + None = 0, V8 = 1, Chrome = 2, Safari = 4, diff --git a/src/tests/BuildWasmApps/Wasm.Build.Tests/WasmBuildAppTest.cs b/src/tests/BuildWasmApps/Wasm.Build.Tests/WasmBuildAppTest.cs index 7802676bfce8db..500ce640a6a1b0 100644 --- a/src/tests/BuildWasmApps/Wasm.Build.Tests/WasmBuildAppTest.cs +++ b/src/tests/BuildWasmApps/Wasm.Build.Tests/WasmBuildAppTest.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; using System.Collections.Generic; using System.IO; using Xunit; @@ -61,27 +60,59 @@ public static int Main() } }", buildArgs, host, id); + [Theory] + [BuildAndRun(aot: true, host: RunHost.None, parameters: new object[] + { "", "error : Cannot find emscripten sdk, required for AOT'ing assemblies. $(EMSDK_PATH)=" })] + [BuildAndRun(aot: true, host: RunHost.None, parameters: new object[] + { "/non-existant/foo", "error : Cannot find emscripten sdk, required for AOT'ing assemblies. $(EMSDK_PATH)=/non-existant/foo" })] + public void AOT_ErrorWhenMissingEMSDK(BuildArgs buildArgs, string emsdkPath, string errorMessage, string id) + { + string projectName = $"missing_emsdk"; + buildArgs = buildArgs with { + ProjectName = projectName, + ExtraBuildArgs = $"/p:EMSDK_PATH={emsdkPath}" + }; + buildArgs = GetBuildArgsWith(buildArgs); + + (_, string buildOutput) = BuildProject(buildArgs, + initProject: () => File.WriteAllText(Path.Combine(_projectDir!, "Program.cs"), s_mainReturns42), + id: id, + expectSuccess: false); + + Assert.Contains(errorMessage, buildOutput); + } + + private static string s_bug49588_ProgramCS = @" + using System; + public class TestClass { + public static int Main() + { + Console.WriteLine($""tc: {Environment.TickCount}, tc64: {Environment.TickCount64}""); + return 42; + } + }"; + [Theory] [MemberData(nameof(MainMethodTestData), parameters: new object[] { /*aot*/ true, RunHost.All })] - public void Bug49588_RegressionTest(BuildArgs buildArgs, RunHost host, string id) - => TestMain("bug49588", @" - using Sytem; - public class TestClass { - public static int Main() - { - Console.WriteLine($""tc: {Environment.TickCount}, tc64: {Environment.TickCount64}""); - return 42; - } - }", buildArgs, host, id); + public void Bug49588_RegressionTest_AOT(BuildArgs buildArgs, RunHost host, string id) + => TestMain("bug49588_aot", s_bug49588_ProgramCS, buildArgs, host, id); - void TestMain(string projectName, string programText, BuildArgs buildArgs, RunHost host, string id) + [Theory] + [MemberData(nameof(MainMethodTestData), parameters: new object[] { /*aot*/ false, RunHost.All })] + public void Bug49588_RegressionTest_NativeRelinking(BuildArgs buildArgs, RunHost host, string id) + => TestMain("bug49588_native_relinking", s_bug49588_ProgramCS, buildArgs, host, id, + extraProperties: "true", + dotnetWasmFromRuntimePack: false); + + void TestMain(string projectName, string programText, BuildArgs buildArgs, RunHost host, string id, string? extraProperties=null, bool? dotnetWasmFromRuntimePack=true) { buildArgs = buildArgs with { ProjectName = projectName }; - buildArgs = GetBuildArgsWith(buildArgs); + buildArgs = GetBuildArgsWith(buildArgs, extraProperties); BuildProject(buildArgs, initProject: () => File.WriteAllText(Path.Combine(_projectDir!, "Program.cs"), programText), - id: id); + id: id, + dotnetWasmFromRuntimePack: dotnetWasmFromRuntimePack); RunAndTestWasmApp(buildArgs, expectedExitCode: 42, test: output => Assert.Contains("Hello, World!", output), host: host, id: id); From c21da911b92368219152e544362a77fe4ae588df Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Wed, 17 Mar 2021 19:48:17 -0400 Subject: [PATCH 14/18] Update tests to track wasm relinking being default in some cases --- .../Wasm.Build.Tests/MainWithArgsTests.cs | 14 ++++++++++++-- .../Wasm.Build.Tests/NativeBuildTests.cs | 9 +++++---- .../Wasm.Build.Tests/WasmBuildAppTest.cs | 11 ++++++++++- 3 files changed, 27 insertions(+), 7 deletions(-) diff --git a/src/tests/BuildWasmApps/Wasm.Build.Tests/MainWithArgsTests.cs b/src/tests/BuildWasmApps/Wasm.Build.Tests/MainWithArgsTests.cs index f0ff1437c98610..df920577cea01b 100644 --- a/src/tests/BuildWasmApps/Wasm.Build.Tests/MainWithArgsTests.cs +++ b/src/tests/BuildWasmApps/Wasm.Build.Tests/MainWithArgsTests.cs @@ -59,7 +59,13 @@ public static int Main(string[] args) } }", buildArgs, args, host, id); - void TestMainWithArgs(string projectNamePrefix, string projectContents, BuildArgs buildArgs, string[] args, RunHost host, string id) + void TestMainWithArgs(string projectNamePrefix, + string projectContents, + BuildArgs buildArgs, + string[] args, + RunHost host, + string id, + bool? dotnetWasmFromRuntimePack=null) { string projectName = $"{projectNamePrefix}_{buildArgs.Config}_{buildArgs.AOT}"; string code = @" @@ -72,11 +78,15 @@ void TestMainWithArgs(string projectNamePrefix, string projectContents, BuildArg buildArgs = buildArgs with { ProjectName = projectName, ProjectFileContents = programText }; buildArgs = GetBuildArgsWith(buildArgs); + if (dotnetWasmFromRuntimePack == null) + dotnetWasmFromRuntimePack = !(buildArgs.AOT || buildArgs.Config == "Release"); + Console.WriteLine ($"-- args: {buildArgs}, name: {projectName}"); BuildProject(buildArgs, initProject: () => File.WriteAllText(Path.Combine(_projectDir!, "Program.cs"), programText), - id: id); + id: id, + dotnetWasmFromRuntimePack: dotnetWasmFromRuntimePack); RunAndTestWasmApp(buildArgs, buildDir: _projectDir, expectedExitCode: 42 + args.Length, args: string.Join(' ', args), test: output => diff --git a/src/tests/BuildWasmApps/Wasm.Build.Tests/NativeBuildTests.cs b/src/tests/BuildWasmApps/Wasm.Build.Tests/NativeBuildTests.cs index be52074057eb92..46d42adf86445f 100644 --- a/src/tests/BuildWasmApps/Wasm.Build.Tests/NativeBuildTests.cs +++ b/src/tests/BuildWasmApps/Wasm.Build.Tests/NativeBuildTests.cs @@ -19,6 +19,7 @@ public NativeBuildTests(ITestOutputHelper output, SharedBuildPerTestClassFixture // TODO: - check dotnet.wasm, js have changed // - icall? pinvoke? + // - test defaults // [Theory] @@ -28,10 +29,10 @@ public void SimpleNativeBuild(BuildArgs buildArgs, RunHost host, string id) [Theory] [BuildAndRun(host: RunHost.None, parameters: new object[] - { "", "error : Cannot find emscripten sdk, required for native relinking. $(EMSDK_PATH)=" })] + { "", "error.*emscripten.*\\(EMSDK_PATH\\)=" })] [BuildAndRun(host: RunHost.None, parameters: new object[] - { "/non-existant/foo", "error : Cannot find emscripten sdk, required for native relinking. $(EMSDK_PATH)=/non-existant/foo" })] - public void Relinking_ErrorWhenMissingEMSDK(BuildArgs buildArgs, string emsdkPath, string errorMessage, string id) + { "/non-existant/foo", "error.*emscripten sdk.*\\(EMSDK_PATH\\)=/non-existant/foo" })] + public void Relinking_ErrorWhenMissingEMSDK(BuildArgs buildArgs, string emsdkPath, string errorPattern, string id) { string projectName = $"simple_native_build"; buildArgs = buildArgs with { @@ -45,7 +46,7 @@ public void Relinking_ErrorWhenMissingEMSDK(BuildArgs buildArgs, string emsdkPat id: id, expectSuccess: false); - Assert.Contains(errorMessage, buildOutput); + Assert.Matches(errorPattern, buildOutput); } private void NativeBuild(string projectNamePrefix, string projectContents, BuildArgs buildArgs, RunHost host, string id) diff --git a/src/tests/BuildWasmApps/Wasm.Build.Tests/WasmBuildAppTest.cs b/src/tests/BuildWasmApps/Wasm.Build.Tests/WasmBuildAppTest.cs index 500ce640a6a1b0..43a4b2fdb7bcd1 100644 --- a/src/tests/BuildWasmApps/Wasm.Build.Tests/WasmBuildAppTest.cs +++ b/src/tests/BuildWasmApps/Wasm.Build.Tests/WasmBuildAppTest.cs @@ -104,11 +104,20 @@ public void Bug49588_RegressionTest_NativeRelinking(BuildArgs buildArgs, RunHost extraProperties: "true", dotnetWasmFromRuntimePack: false); - void TestMain(string projectName, string programText, BuildArgs buildArgs, RunHost host, string id, string? extraProperties=null, bool? dotnetWasmFromRuntimePack=true) + void TestMain(string projectName, + string programText, + BuildArgs buildArgs, + RunHost host, + string id, + string? extraProperties = null, + bool? dotnetWasmFromRuntimePack = null) { buildArgs = buildArgs with { ProjectName = projectName }; buildArgs = GetBuildArgsWith(buildArgs, extraProperties); + if (dotnetWasmFromRuntimePack == null) + dotnetWasmFromRuntimePack = !(buildArgs.AOT || buildArgs.Config == "Release"); + BuildProject(buildArgs, initProject: () => File.WriteAllText(Path.Combine(_projectDir!, "Program.cs"), programText), id: id, From d6e97323c2d594468ed583d8f984397c54bfc11d Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Thu, 18 Mar 2021 00:53:04 -0400 Subject: [PATCH 15/18] Fix InvariantGlobalization to track change in wasm relinking defaults --- .../Wasm.Build.Tests/InvariantGlobalizationTests.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/tests/BuildWasmApps/Wasm.Build.Tests/InvariantGlobalizationTests.cs b/src/tests/BuildWasmApps/Wasm.Build.Tests/InvariantGlobalizationTests.cs index 2107cf07e9b710..6d01f20a8420e8 100644 --- a/src/tests/BuildWasmApps/Wasm.Build.Tests/InvariantGlobalizationTests.cs +++ b/src/tests/BuildWasmApps/Wasm.Build.Tests/InvariantGlobalizationTests.cs @@ -31,7 +31,7 @@ public InvariantGlobalizationTests(ITestOutputHelper output, SharedBuildPerTestC [MemberData(nameof(InvariantGlobalizationTestData), parameters: new object[] { /*aot*/ false, RunHost.All })] [MemberData(nameof(InvariantGlobalizationTestData), parameters: new object[] { /*aot*/ true, RunHost.All })] public void AOT_InvariantGlobalization(BuildArgs buildArgs, bool? invariantGlobalization, RunHost host, string id) - => TestInvariantGlobalization(buildArgs, invariantGlobalization, host, id, dotnetWasmFromRuntimePack: !buildArgs.AOT); + => TestInvariantGlobalization(buildArgs, invariantGlobalization, host, id); // TODO: What else should we use to verify a relinked build? [Theory] @@ -42,7 +42,7 @@ public void RelinkingWithoutAOT(BuildArgs buildArgs, bool? invariantGlobalizatio dotnetWasmFromRuntimePack: false); private void TestInvariantGlobalization(BuildArgs buildArgs, bool? invariantGlobalization, - RunHost host, string id, string extraProperties="", bool dotnetWasmFromRuntimePack=true) + RunHost host, string id, string extraProperties="", bool? dotnetWasmFromRuntimePack=null) { string projectName = $"invariant_{invariantGlobalization?.ToString() ?? "unset"}"; if (invariantGlobalization != null) @@ -51,6 +51,9 @@ private void TestInvariantGlobalization(BuildArgs buildArgs, bool? invariantGlob buildArgs = buildArgs with { ProjectName = projectName }; buildArgs = GetBuildArgsWith(buildArgs, extraProperties); + if (dotnetWasmFromRuntimePack == null) + dotnetWasmFromRuntimePack = !(buildArgs.AOT || buildArgs.Config == "Release"); + string programText = @" using System; using System.Globalization; From 97b83b892e3c6120daa63bde83f60550b266f2c0 Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Thu, 25 Mar 2021 14:15:28 -0400 Subject: [PATCH 16/18] [wasm] Update emsdk check message to track changes --- src/mono/wasm/build/WasmApp.targets | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/mono/wasm/build/WasmApp.targets b/src/mono/wasm/build/WasmApp.targets index 8ccb4d701931a0..287f963beabd76 100644 --- a/src/mono/wasm/build/WasmApp.targets +++ b/src/mono/wasm/build/WasmApp.targets @@ -395,7 +395,8 @@ - + $([MSBuild]::NormalizePath($(MicrosoftNetCoreAppRuntimePackRidNativeDir), 'src', 'emcc-flags.txt')) From f4637141f87649792d8ba76fdd8d0dd73ce81454 Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Thu, 25 Mar 2021 14:15:42 -0400 Subject: [PATCH 17/18] [wasm] Update tests to track changes --- .../BuildWasmApps/Wasm.Build.Tests/NativeBuildTests.cs | 4 ++-- .../BuildWasmApps/Wasm.Build.Tests/WasmBuildAppTest.cs | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/tests/BuildWasmApps/Wasm.Build.Tests/NativeBuildTests.cs b/src/tests/BuildWasmApps/Wasm.Build.Tests/NativeBuildTests.cs index 46d42adf86445f..da7b33d0a0c351 100644 --- a/src/tests/BuildWasmApps/Wasm.Build.Tests/NativeBuildTests.cs +++ b/src/tests/BuildWasmApps/Wasm.Build.Tests/NativeBuildTests.cs @@ -29,9 +29,9 @@ public void SimpleNativeBuild(BuildArgs buildArgs, RunHost host, string id) [Theory] [BuildAndRun(host: RunHost.None, parameters: new object[] - { "", "error.*emscripten.*\\(EMSDK_PATH\\)=" })] + { "", "error.*emscripten.*required for building native files" })] [BuildAndRun(host: RunHost.None, parameters: new object[] - { "/non-existant/foo", "error.*emscripten sdk.*\\(EMSDK_PATH\\)=/non-existant/foo" })] + { "/non-existant/foo", "error.*\\(EMSDK_PATH\\)=/non-existant/foo.*required for building native files" })] public void Relinking_ErrorWhenMissingEMSDK(BuildArgs buildArgs, string emsdkPath, string errorPattern, string id) { string projectName = $"simple_native_build"; diff --git a/src/tests/BuildWasmApps/Wasm.Build.Tests/WasmBuildAppTest.cs b/src/tests/BuildWasmApps/Wasm.Build.Tests/WasmBuildAppTest.cs index 43a4b2fdb7bcd1..3778d1fcdaae30 100644 --- a/src/tests/BuildWasmApps/Wasm.Build.Tests/WasmBuildAppTest.cs +++ b/src/tests/BuildWasmApps/Wasm.Build.Tests/WasmBuildAppTest.cs @@ -62,10 +62,10 @@ public static int Main() [Theory] [BuildAndRun(aot: true, host: RunHost.None, parameters: new object[] - { "", "error : Cannot find emscripten sdk, required for AOT'ing assemblies. $(EMSDK_PATH)=" })] + { "", "error :.*emscripten.*required for AOT" })] [BuildAndRun(aot: true, host: RunHost.None, parameters: new object[] - { "/non-existant/foo", "error : Cannot find emscripten sdk, required for AOT'ing assemblies. $(EMSDK_PATH)=/non-existant/foo" })] - public void AOT_ErrorWhenMissingEMSDK(BuildArgs buildArgs, string emsdkPath, string errorMessage, string id) + { "/non-existant/foo", "error.*\\(EMSDK_PATH\\)=/non-existant/foo.*required for AOT" })] + public void AOT_ErrorWhenMissingEMSDK(BuildArgs buildArgs, string emsdkPath, string errorPattern, string id) { string projectName = $"missing_emsdk"; buildArgs = buildArgs with { @@ -79,7 +79,7 @@ public void AOT_ErrorWhenMissingEMSDK(BuildArgs buildArgs, string emsdkPath, str id: id, expectSuccess: false); - Assert.Contains(errorMessage, buildOutput); + Assert.Matches(errorPattern, buildOutput); } private static string s_bug49588_ProgramCS = @" From 7caed5ab609f499e471a2012192ccb225a5714f9 Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Fri, 26 Mar 2021 13:20:10 -0400 Subject: [PATCH 18/18] [wasm] Move Scenario=BuildWasmApps to be submitted first TLDR; - this might help with the job getting scheduled first, and thus having a chance at completing at the same time as others. Reasoning: The problem this is trying to fix is: 1. The helix step submits 3 jobs: a. library tests to be run with v8 b. library tests to be run with browser (scenario=wasmtestonbrowser) c. Wasm.Build.Tests (scenario=buildwasmapps) 2. The 3 jobs, individually take roughly 30mins each 3. And they get submitted at roughly the same time 4. But .. the first two seem to complete earlier, and the 3rd one completes 25-30mins later. The hypothesis is that all the machines might be busy processing the 200+ work items from each of the first two steps, and so Wasm.Build.Tests get scheduled pretty late. So, here we move that to be submitted first, in the hope that it would be able to run in parallel with the other jobs. --- eng/pipelines/runtime.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eng/pipelines/runtime.yml b/eng/pipelines/runtime.yml index 05858a91413380..df3f7ca8184b61 100644 --- a/eng/pipelines/runtime.yml +++ b/eng/pipelines/runtime.yml @@ -293,9 +293,9 @@ jobs: creator: dotnet-bot testRunNamePrefixSuffix: Mono_$(_BuildConfig) scenarios: + - buildwasmapps - normal - wasmtestonbrowser - - buildwasmapps condition: >- or( eq(variables['librariesContainsChange'], true),