diff --git a/eng/pipelines/runtime.yml b/eng/pipelines/runtime.yml index 931da897d04d5..eaaec1b2eb939 100644 --- a/eng/pipelines/runtime.yml +++ b/eng/pipelines/runtime.yml @@ -283,7 +283,7 @@ jobs: testGroup: innerloop nameSuffix: AllSubsets_Mono buildArgs: -s mono+libs+host+packs+libs.tests -c $(_BuildConfig) /p:ArchiveTests=true - timeoutInMinutes: 120 + timeoutInMinutes: 180 condition: >- or( eq(dependencies.evaluate_paths.outputs['SetPathVars_libraries.containsChange'], true), @@ -298,6 +298,7 @@ jobs: scenarios: - normal - wasmtestonbrowser + - buildwasmapps condition: >- or( eq(variables['librariesContainsChange'], true), diff --git a/eng/testing/tests.mobile.targets b/eng/testing/tests.mobile.targets index 5a9e56c207fce..b2e875f120bb6 100644 --- a/eng/testing/tests.mobile.targets +++ b/eng/testing/tests.mobile.targets @@ -12,7 +12,7 @@ - $HARNESS_RUNNER wasm $XHARNESS_COMMAND --app=. --engine=$(JSEngine) $(JSEngineArgs) --js-file=runtime.js --output-directory=$XHARNESS_OUT -- $(RunTestsJSArguments) --run WasmTestRunner.dll $(AssemblyName).dll + $HARNESS_RUNNER wasm $XHARNESS_COMMAND --app=. --engine=$(JSEngine) $(JSEngineArgs) --js-file=runtime.js --output-directory=$XHARNESS_OUT -- $(RunTestsJSArguments) --run WasmTestRunner.dll $(AssemblyName).dll $HARNESS_RUNNER wasm $XHARNESS_COMMAND --app=. --engine=$(JSEngine) $(JSEngineArgs) --js-file=runtime.js --output-directory=$XHARNESS_OUT --expected-exit-code=$(ExpectedExitCode) -- $(RunTestsJSArguments) --run $(AssemblyName).dll --testing false true @@ -232,9 +232,24 @@ PrepareForWasmBuildApp;$(WasmBuildAppDependsOn) + $([MSBuild]::NormalizeDirectory('$(RepoRoot)', 'src', 'mono', 'wasm', 'emsdk')) - + + + + + + + + + + + + + diff --git a/eng/testing/xunit/xunit.console.targets b/eng/testing/xunit/xunit.console.targets index ee700551c76b8..a3e7d2614a4f5 100644 --- a/eng/testing/xunit/xunit.console.targets +++ b/eng/testing/xunit/xunit.console.targets @@ -62,7 +62,7 @@ - + $(ArtifactsDir)helix/ $(TestArchiveRoot)tests/ $(TestArchiveRoot)runonly/ + $(TestArchiveRoot)buildwasmapps/ $(TestArchiveTestsRoot)$(OSPlatformConfig)/ $(TestArchiveRoot)runtime/ @@ -170,7 +171,7 @@ false - + diff --git a/src/libraries/sendtohelixhelp.proj b/src/libraries/sendtohelixhelp.proj index 3269b7c389500..4efa7087a45cc 100644 --- a/src/libraries/sendtohelixhelp.proj +++ b/src/libraries/sendtohelixhelp.proj @@ -28,6 +28,7 @@ <_workItemTimeout Condition="'$(_workItemTimeout)' == '' and ('$(TargetOS)' == 'iOS' or '$(TargetOS)' == 'tvOS' or '$(TargetOS)' == 'Android')">00:30:00 <_workItemTimeout Condition="'$(Scenario)' == '' and '$(_workItemTimeout)' == '' and ('$(TargetArchitecture)' == 'arm64' or '$(TargetArchitecture)' == 'arm')">00:45:00 <_workItemTimeout Condition="'$(Scenario)' != '' and '$(_workItemTimeout)' == '' and ('$(TargetArchitecture)' == 'arm64' or '$(TargetArchitecture)' == 'arm')">01:00:00 + <_workItemTimeout Condition="'$(Scenario)' == 'BuildWasmApps' and '$(_workItemTimeout)' == ''">01:00:00 <_workItemTimeout Condition="'$(Scenario)' == '' and '$(_workItemTimeout)' == ''">00:15:00 <_workItemTimeout Condition="'$(Scenario)' != '' and '$(_workItemTimeout)' == ''">00:30:00 @@ -98,11 +99,19 @@ - + + + true + true + sdk + $([System.IO.File]::ReadAllText('$(RepoRoot)global.json')) + $([System.Text.RegularExpressions.Regex]::Match($(GlobalJsonContent), '(%3F<="dotnet": ").*(%3F=")')) + + @@ -224,6 +233,8 @@ 768968 https://storage.googleapis.com/chromium-browser-snapshots/Linux_x64/$(ChromiumRevision)/chrome-linux.zip https://storage.googleapis.com/chromium-browser-snapshots/Linux_x64/$(ChromiumRevision)/chromedriver_linux64.zip + $([MSBuild]::NormalizeDirectory('$(RepoRoot)', 'src', 'mono', 'wasm', 'emsdk')) + $([MSBuild]::NormalizeDirectory('$(RepoRoot)', 'src', 'mono', 'wasm', 'build')) @@ -237,6 +248,14 @@ + + + + + + + + @@ -244,7 +263,7 @@ <_WorkItem Include="$(WorkItemArchiveWildCard)" Exclude="$(HelixCorrelationPayload)" /> - <_WorkItem Include="$(TestArchiveRoot)runonly/**/WebAssembly.Console.*.Test.zip" Condition="'$(TargetOS)' == 'Browser' and '$(Scenario)' != 'WasmTestOnBrowser'" /> + <_WorkItem Include="$(TestArchiveRoot)runonly/**/WebAssembly.Console.*.Test.zip" Condition="'$(TargetOS)' == 'Browser' and '$(Scenario)' != 'WasmTestOnBrowser' and '$(Scenario)' != 'BuildWasmApps'" /> <_WorkItem Include="$(TestArchiveRoot)runonly/**/WebAssembly.Browser.*.Test.zip" Condition="'$(TargetOS)' == 'Browser' and '$(Scenario)' == 'WasmTestOnBrowser'" /> @@ -254,7 +273,7 @@ - + <_RunOnlyWorkItem Include="$(TestArchiveRoot)runonly/**/*.Console.Sample.zip" /> diff --git a/src/libraries/tests.proj b/src/libraries/tests.proj index b920de0d7c057..4f841f88249f5 100644 --- a/src/libraries/tests.proj +++ b/src/libraries/tests.proj @@ -266,6 +266,10 @@ Condition="'$(TestTrimming)' == 'true'" AdditionalProperties="%(AdditionalProperties);SkipTrimmingProjectsRestore=true" /> + diff --git a/src/mono/wasm/Makefile b/src/mono/wasm/Makefile index a3e15f8454b0a..cbb7e16251fca 100644 --- a/src/mono/wasm/Makefile +++ b/src/mono/wasm/Makefile @@ -182,6 +182,9 @@ run-tests-jsc-%: run-tests-%: PATH="$(JSVU):$(PATH)" $(DOTNET) build $(TOP)/src/libraries/$*/tests/ /t:Test $(_MSBUILD_WASM_BUILD_ARGS) $(MSBUILD_ARGS) +run-build-tests: + PATH="$(JSVU):$(PATH)" $(DOTNET) build $(TOP)/src/tests/BuildWasmApps/Wasm.Build.Tests/ /t:Test $(_MSBUILD_WASM_BUILD_ARGS) $(MSBUILD_ARGS) + run-browser-tests-%: PATH="$(GECKODRIVER):$(CHROMEDRIVER):$(PATH)" XHARNESS_COMMAND="test-browser --browser=$(XHARNESS_BROWSER)" $(DOTNET) build $(TOP)/src/libraries/$*/tests/ /t:Test $(_MSBUILD_WASM_BUILD_ARGS) $(MSBUILD_ARGS) diff --git a/src/mono/wasm/build/WasmApp.LocalBuild.props b/src/mono/wasm/build/WasmApp.LocalBuild.props index b545e5764d8f8..a56d7137dac89 100644 --- a/src/mono/wasm/build/WasmApp.LocalBuild.props +++ b/src/mono/wasm/build/WasmApp.LocalBuild.props @@ -19,8 +19,10 @@ $(WasmBuildSupportDir) - directory which has all the tasks, targets, and runtimepack --> + + - <_NetCoreAppToolCurrent>net5.0 + <_NetCoreAppToolCurrent>net6.0 @@ -52,6 +54,4 @@ $([MSBuild]::NormalizePath('$(WasmBuildTasksDir)', 'WasmBuildTasks.dll')) $([MSBuild]::NormalizePath('$(MonoAOTCompilerDir)', 'MonoAOTCompiler.dll')) - - diff --git a/src/mono/wasm/build/WasmApp.targets b/src/mono/wasm/build/WasmApp.targets index 0bd700e379a36..a903819357228 100644 --- a/src/mono/wasm/build/WasmApp.targets +++ b/src/mono/wasm/build/WasmApp.targets @@ -58,8 +58,10 @@ --> - false - $(RunAOTCompilation) + false + + + <_ExeExt Condition="$([MSBuild]::IsOSPlatform('WINDOWS'))">.exe @@ -69,6 +71,7 @@ + @@ -323,7 +326,7 @@ EMSCRIPTEN_KEEPALIVE void mono_wasm_load_profiler_aot (const char *desc) { mono_ diff --git a/src/tests/BuildWasmApps/Directory.Build.props b/src/tests/BuildWasmApps/Directory.Build.props new file mode 100644 index 0000000000000..68eaef4190505 --- /dev/null +++ b/src/tests/BuildWasmApps/Directory.Build.props @@ -0,0 +1,6 @@ + + + BuildWasmApps + + + diff --git a/src/tests/BuildWasmApps/Directory.Build.targets b/src/tests/BuildWasmApps/Directory.Build.targets new file mode 100644 index 0000000000000..fa2efe47c0815 --- /dev/null +++ b/src/tests/BuildWasmApps/Directory.Build.targets @@ -0,0 +1,8 @@ + + + + + $(OutDir) + $(OutDir)\RunTests.sh + + diff --git a/src/tests/BuildWasmApps/Wasm.Build.Tests/Wasm.Build.Tests.csproj b/src/tests/BuildWasmApps/Wasm.Build.Tests/Wasm.Build.Tests.csproj new file mode 100644 index 0000000000000..a6fe3e49f375d --- /dev/null +++ b/src/tests/BuildWasmApps/Wasm.Build.Tests/Wasm.Build.Tests.csproj @@ -0,0 +1,34 @@ + + + $(NetCoreAppToolCurrent) + true + true + true + BuildAndRun + xunit + false + + + + + + + <_PreCommand>WasmBuildSupportDir=%24{HELIX_CORRELATION_PAYLOAD}/build + <_PreCommand>$(_PreCommand) DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1 + + + + <_PreCommand>$(_PreCommand) TEST_LOG_PATH=%24{XHARNESS_OUT}/logs + <_PreCommand>$(_PreCommand) HARNESS_RUNNER=%24{HARNESS_RUNNER} + + $(_PreCommand) dotnet exec xunit.console.dll $(AssemblyName).dll -xml %24XHARNESS_OUT/testResults.xml + $(RunScriptCommand) -nocolor + $(RunScriptCommand) -verbose + + + + + + + + diff --git a/src/tests/BuildWasmApps/Wasm.Build.Tests/WasmBuildAppTest.cs b/src/tests/BuildWasmApps/Wasm.Build.Tests/WasmBuildAppTest.cs new file mode 100644 index 0000000000000..c6ff9aa45d312 --- /dev/null +++ b/src/tests/BuildWasmApps/Wasm.Build.Tests/WasmBuildAppTest.cs @@ -0,0 +1,663 @@ +// 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; +using System.IO; +using System.Linq; +using System.Text; +using Xunit; +using Xunit.Abstractions; + +#nullable enable + +namespace Wasm.Build.Tests +{ + public class WasmBuildAppTest : IDisposable + { + private const string TestLogPathEnvVar = "TEST_LOG_PATH"; + private const string SkipProjectCleanupEnvVar = "SKIP_PROJECT_CLEANUP"; + private const string XHarnessRunnerCommandEnvVar = "XHARNESS_CLI_PATH"; + + 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)); + } + + [Theory] + [MemberData(nameof(ConfigWithAOTData), parameters: /*aot*/ true)] + public void TopLevelMain(string config, bool aot) + => TestMain("top_level", + @"System.Console.WriteLine(""Hello, World!""); return await System.Threading.Tasks.Task.FromResult(42);", + config, aot); + + [Theory] + [MemberData(nameof(ConfigWithAOTData), parameters: /*aot*/ true)] + public void AsyncMain(string config, bool aot) + => TestMain("async_main", @" + using System; + using System.Threading.Tasks; + + public class TestClass { + public static async Task Main() + { + Console.WriteLine(""Hello, World!""); + return await Task.FromResult(42); + } + }", config, aot); + + [Theory] + [MemberData(nameof(ConfigWithAOTData), parameters: /*aot*/ true)] + public void NonAsyncMain(string config, bool aot) + => TestMain("non_async_main", @" + using System; + using System.Threading.Tasks; + + public class TestClass { + 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"); + } + } + + private (int, string) RunProcess(string path, + string args = "", + IDictionary? envVars = null, + string? workingDir = null, + string? label = null, + bool logToXUnit = true) + { + _testOutput.WriteLine($"Running: {path} {args}"); + StringBuilder outputBuilder = new (); + var processStartInfo = new ProcessStartInfo + { + FileName = path, + UseShellExecute = false, + CreateNoWindow = true, + RedirectStandardError = true, + RedirectStandardOutput = true, + Arguments = args, + }; + + 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 = 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}"); + } + } + + 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 = @" + + + + + + + + + + + + + + + + + + +"; + + } + + } diff --git a/src/tests/Common/dirs.proj b/src/tests/Common/dirs.proj index e12d882d1dc67..53a064e6e0e0c 100644 --- a/src/tests/Common/dirs.proj +++ b/src/tests/Common/dirs.proj @@ -10,6 +10,7 @@ +