diff --git a/src/Microsoft.DotNet.XHarness.CLI/CommandArguments/Arguments/WebServerUploadResults.cs b/src/Microsoft.DotNet.XHarness.CLI/CommandArguments/Arguments/WebServerUploadResults.cs new file mode 100644 index 000000000..1cbef9d41 --- /dev/null +++ b/src/Microsoft.DotNet.XHarness.CLI/CommandArguments/Arguments/WebServerUploadResults.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.DotNet.XHarness.CLI.CommandArguments; + +internal class WebServerUploadResults : SwitchArgument +{ + public WebServerUploadResults() + : base("web-server-upload-results", "Enable uploading XML test results", true) + { + } +} diff --git a/src/Microsoft.DotNet.XHarness.CLI/CommandArguments/WASI/WasiTestCommandArguments.cs b/src/Microsoft.DotNet.XHarness.CLI/CommandArguments/WASI/WasiTestCommandArguments.cs index 441eb954d..8ae98786d 100644 --- a/src/Microsoft.DotNet.XHarness.CLI/CommandArguments/WASI/WasiTestCommandArguments.cs +++ b/src/Microsoft.DotNet.XHarness.CLI/CommandArguments/WASI/WasiTestCommandArguments.cs @@ -25,6 +25,7 @@ internal class WasiTestCommandArguments : XHarnessCommandArguments, IWebServerAr public WebServerUseCrossOriginPolicyArguments WebServerUseCrossOriginPolicy { get; } = new(); public WebServerUseDefaultFilesArguments WebServerUseDefaultFiles { get; } = new(); public bool IsWebServerEnabled => WebServerMiddlewarePathsAndTypes.Value.Count > 0; + public WebServerUploadResults WebServerUploadResults { get; } = new(); protected override IEnumerable GetArguments() => new Argument[] { diff --git a/src/Microsoft.DotNet.XHarness.CLI/CommandArguments/WASM/IWebServerArguments.cs b/src/Microsoft.DotNet.XHarness.CLI/CommandArguments/WASM/IWebServerArguments.cs index b4c672157..c10c49e24 100644 --- a/src/Microsoft.DotNet.XHarness.CLI/CommandArguments/WASM/IWebServerArguments.cs +++ b/src/Microsoft.DotNet.XHarness.CLI/CommandArguments/WASM/IWebServerArguments.cs @@ -14,4 +14,6 @@ internal interface IWebServerArguments WebServerUseCorsArguments WebServerUseCors { get; } WebServerUseCrossOriginPolicyArguments WebServerUseCrossOriginPolicy { get; } WebServerUseDefaultFilesArguments WebServerUseDefaultFiles { get; } + WebServerUploadResults WebServerUploadResults { get; } + OutputDirectoryArgument OutputDirectory { get; } } diff --git a/src/Microsoft.DotNet.XHarness.CLI/CommandArguments/WASM/WasmTestBrowserCommandArguments.cs b/src/Microsoft.DotNet.XHarness.CLI/CommandArguments/WASM/WasmTestBrowserCommandArguments.cs index 25678458e..1d980c118 100644 --- a/src/Microsoft.DotNet.XHarness.CLI/CommandArguments/WASM/WasmTestBrowserCommandArguments.cs +++ b/src/Microsoft.DotNet.XHarness.CLI/CommandArguments/WASM/WasmTestBrowserCommandArguments.cs @@ -38,6 +38,7 @@ internal class WasmTestBrowserCommandArguments : XHarnessCommandArguments, IWebS public WebServerUseCorsArguments WebServerUseCors { get; } = new(); public WebServerUseCrossOriginPolicyArguments WebServerUseCrossOriginPolicy { get; } = new(); public WebServerUseDefaultFilesArguments WebServerUseDefaultFiles { get; } = new(); + public WebServerUploadResults WebServerUploadResults { get; } = new(); public bool IsWebServerEnabled => WebServerMiddlewarePathsAndTypes.Value.Count > 0; protected override IEnumerable GetArguments() => new Argument[] diff --git a/src/Microsoft.DotNet.XHarness.CLI/CommandArguments/WASM/WasmTestCommandArguments.cs b/src/Microsoft.DotNet.XHarness.CLI/CommandArguments/WASM/WasmTestCommandArguments.cs index 44fd3b2b6..20675e172 100644 --- a/src/Microsoft.DotNet.XHarness.CLI/CommandArguments/WASM/WasmTestCommandArguments.cs +++ b/src/Microsoft.DotNet.XHarness.CLI/CommandArguments/WASM/WasmTestCommandArguments.cs @@ -30,6 +30,7 @@ internal class WasmTestCommandArguments : XHarnessCommandArguments, IWebServerAr public WebServerUseCorsArguments WebServerUseCors { get; } = new(); public WebServerUseCrossOriginPolicyArguments WebServerUseCrossOriginPolicy { get; } = new(); public WebServerUseDefaultFilesArguments WebServerUseDefaultFiles { get; } = new(); + public WebServerUploadResults WebServerUploadResults { get; } = new(); public bool IsWebServerEnabled => WebServerMiddlewarePathsAndTypes.Value.Count > 0; protected override IEnumerable GetArguments() => new Argument[] diff --git a/src/Microsoft.DotNet.XHarness.CLI/CommandArguments/WASM/WebServerCommandArguments.cs b/src/Microsoft.DotNet.XHarness.CLI/CommandArguments/WASM/WebServerCommandArguments.cs index d4d1d3c18..3b708a07c 100644 --- a/src/Microsoft.DotNet.XHarness.CLI/CommandArguments/WASM/WebServerCommandArguments.cs +++ b/src/Microsoft.DotNet.XHarness.CLI/CommandArguments/WASM/WebServerCommandArguments.cs @@ -18,6 +18,9 @@ internal class WebServerCommandArguments : XHarnessCommandArguments, IWebServerA public WebServerUseCorsArguments WebServerUseCors { get; } = new(); public WebServerUseCrossOriginPolicyArguments WebServerUseCrossOriginPolicy { get; } = new(); public WebServerUseDefaultFilesArguments WebServerUseDefaultFiles { get; } = new(); + public WebServerUploadResults WebServerUploadResults { get; } = new(); + public OutputDirectoryArgument OutputDirectory { get; } = new(); + public bool IsWebServerEnabled => WebServerMiddlewarePathsAndTypes.Value.Count > 0; public TimeoutArgument Timeout { get; } = new(TimeSpan.FromMinutes(15)); diff --git a/src/Microsoft.DotNet.XHarness.CLI/Commands/WebServer.cs b/src/Microsoft.DotNet.XHarness.CLI/Commands/WebServer.cs index 3a42365f9..8985f25bd 100644 --- a/src/Microsoft.DotNet.XHarness.CLI/Commands/WebServer.cs +++ b/src/Microsoft.DotNet.XHarness.CLI/Commands/WebServer.cs @@ -21,6 +21,7 @@ using LogLevel = Microsoft.Extensions.Logging.LogLevel; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Hosting; +using System.IO; namespace Microsoft.DotNet.XHarness.CLI.Commands; @@ -175,6 +176,20 @@ public void Configure(IApplicationBuilder app, IOptionsMonitor + { + router.MapPost("/test-results", async context => + { + var xmlResultsFilePath = Path.Combine(options.OutputDirectory!, "testResults.xml"); + + await using var fileStream = File.Create(xmlResultsFilePath); + await context.Request.BodyReader.CopyToAsync(fileStream); + _logger.LogInformation($"Stored {xmlResultsFilePath} results {fileStream.Position} bytes"); + }); + }); + } foreach (var middleware in options.EchoServerMiddlewares) { @@ -192,6 +207,8 @@ internal class TestWebServerOptions public bool UseHttps { get; set; } public bool UseCrossOriginPolicy { get; set; } public bool UseDefaultFiles { get; set; } + public bool WebServerUploadResults { get; set; } + public string? OutputDirectory { get; set; } public string? ContentRoot { get; set; } public void CopyTo(TestWebServerOptions otherOptions) @@ -202,6 +219,8 @@ public void CopyTo(TestWebServerOptions otherOptions) otherOptions.UseHttps = UseHttps; otherOptions.UseCrossOriginPolicy = UseCrossOriginPolicy; otherOptions.UseDefaultFiles = UseDefaultFiles; + otherOptions.WebServerUploadResults = WebServerUploadResults; + otherOptions.OutputDirectory = OutputDirectory; otherOptions.ContentRoot = ContentRoot; } @@ -212,6 +231,8 @@ public static TestWebServerOptions FromArguments(IWebServerArguments arguments) options.UseHttps = arguments.WebServerUseHttps; options.UseCrossOriginPolicy = arguments.WebServerUseCrossOriginPolicy; options.UseDefaultFiles = arguments.WebServerUseDefaultFiles; + options.WebServerUploadResults = arguments.WebServerUploadResults; + options.OutputDirectory = arguments.OutputDirectory; foreach (var middlewareType in arguments.WebServerMiddlewarePathsAndTypes.GetLoadedTypes()) { options.EchoServerMiddlewares.Add(middlewareType); diff --git a/src/Microsoft.DotNet.XHarness.TestRunners.Common/ApplicationEntryPoint.cs b/src/Microsoft.DotNet.XHarness.TestRunners.Common/ApplicationEntryPoint.cs index 5b36565ef..ca6ca5fbd 100644 --- a/src/Microsoft.DotNet.XHarness.TestRunners.Common/ApplicationEntryPoint.cs +++ b/src/Microsoft.DotNet.XHarness.TestRunners.Common/ApplicationEntryPoint.cs @@ -173,7 +173,7 @@ internal static void ConfigureRunnerFilters(TestRunner runner, ApplicationOption } } - private static void WriteResults(TestRunner runner, ApplicationOptions options, LogWriter logger, TextWriter writer) + private static async Task WriteResults(TestRunner runner, ApplicationOptions options, LogWriter logger, TextWriter writer) { if (options.EnableXml && writer == null) { @@ -182,12 +182,12 @@ private static void WriteResults(TestRunner runner, ApplicationOptions options, if (options.EnableXml) { - runner.WriteResultsToFile(writer, options.XmlVersion); + await runner.WriteResultsToFile(writer, options.XmlVersion); logger.Info("Xml file was written to the provided writer."); } else { - string resultsFilePath = runner.WriteResultsToFile(options.XmlVersion); + string resultsFilePath = await runner.WriteResultsToFile(options.XmlVersion); logger.Info($"XML results can be found in '{resultsFilePath}'"); } } @@ -227,7 +227,7 @@ protected async Task InternalRunAsync(ApplicationOptions options, Te logger.MinimumLogLevel = MinimumLogLevel.Info; var runner = await InternalRunAsync(logger); - WriteResults(runner, options, logger, resultsFile ?? Console.Out); + await WriteResults(runner, options, logger, resultsFile ?? Console.Out); logger.Info($"{Environment.NewLine}=== TEST EXECUTION SUMMARY ==={Environment.NewLine}Tests run: {runner.TotalTests} Passed: {runner.PassedTests} Inconclusive: {runner.InconclusiveTests} Failed: {runner.FailedTests} Ignored: {runner.FilteredTests} Skipped: {runner.SkippedTests}{Environment.NewLine}"); diff --git a/src/Microsoft.DotNet.XHarness.TestRunners.Common/TestRunner.cs b/src/Microsoft.DotNet.XHarness.TestRunners.Common/TestRunner.cs index 7d4ee0ff2..7fbcc8768 100644 --- a/src/Microsoft.DotNet.XHarness.TestRunners.Common/TestRunner.cs +++ b/src/Microsoft.DotNet.XHarness.TestRunners.Common/TestRunner.cs @@ -112,8 +112,8 @@ protected TestRunner(LogWriter logger) } public abstract Task Run(IEnumerable testAssemblies); - public abstract string WriteResultsToFile(XmlResultJargon xmlResultJargon); - public abstract void WriteResultsToFile(TextWriter writer, XmlResultJargon jargon); + public abstract Task WriteResultsToFile(XmlResultJargon xmlResultJargon); + public abstract Task WriteResultsToFile(TextWriter writer, XmlResultJargon jargon); public abstract void SkipTests(IEnumerable tests); public abstract void SkipCategories(IEnumerable categories); public abstract void SkipMethod(string method, bool isExcluded); diff --git a/src/Microsoft.DotNet.XHarness.TestRunners.NUnit/NUnitTestRunner.cs b/src/Microsoft.DotNet.XHarness.TestRunners.NUnit/NUnitTestRunner.cs index b9d8d1920..c79d68b67 100644 --- a/src/Microsoft.DotNet.XHarness.TestRunners.NUnit/NUnitTestRunner.cs +++ b/src/Microsoft.DotNet.XHarness.TestRunners.NUnit/NUnitTestRunner.cs @@ -158,32 +158,34 @@ private bool ReportFilteredAssembly(TestAssemblyInfo assemblyInfo, bool include) return include; } - public override string WriteResultsToFile(XmlResultJargon jargon) + public override Task WriteResultsToFile(XmlResultJargon jargon) { if (_results == null) { - return string.Empty; + return Task.FromResult(string.Empty); } string ret = GetResultsFilePath(); if (string.IsNullOrEmpty(ret)) { - return string.Empty; + return Task.FromResult(string.Empty); } jargon.GetWriter().WriteResultFile(_results, ret); - return ret; + return Task.FromResult(ret); } - public override void WriteResultsToFile(TextWriter writer, XmlResultJargon jargon) + public override Task WriteResultsToFile(TextWriter writer, XmlResultJargon jargon) { if (_results == null) { - return; + return Task.CompletedTask; } jargon.GetWriter().WriteResultFile(_results, writer); + + return Task.CompletedTask; } public override void SkipTests(IEnumerable tests) diff --git a/src/Microsoft.DotNet.XHarness.TestRunners.Xunit/ThreadlessXunitTestRunner.cs b/src/Microsoft.DotNet.XHarness.TestRunners.Xunit/ThreadlessXunitTestRunner.cs index dd9cf08e1..02227905f 100644 --- a/src/Microsoft.DotNet.XHarness.TestRunners.Xunit/ThreadlessXunitTestRunner.cs +++ b/src/Microsoft.DotNet.XHarness.TestRunners.Xunit/ThreadlessXunitTestRunner.cs @@ -19,16 +19,14 @@ namespace Microsoft.DotNet.XHarness.TestRunners.Xunit; internal class ThreadlessXunitTestRunner : XunitTestRunnerBase { - public ThreadlessXunitTestRunner(LogWriter logger, bool oneLineResults = false) : base(logger) + public ThreadlessXunitTestRunner(LogWriter logger) : base(logger) { - _oneLineResults = oneLineResults; ShowFailureInfos = false; } protected override string ResultsFileName { get => string.Empty; set => throw new InvalidOperationException("This runner outputs its results to stdout."); } private readonly XElement _assembliesElement = new XElement("assemblies"); - private readonly bool _oneLineResults; public override async Task Run(IEnumerable testAssemblies) { @@ -110,26 +108,16 @@ private ExecutionSummary Combine(ExecutionSummary aggregateSummary, ExecutionSum }; } - public override string WriteResultsToFile(XmlResultJargon xmlResultJargon) + public override async Task WriteResultsToFile(XmlResultJargon xmlResultJargon) { Debug.Assert(xmlResultJargon == XmlResultJargon.xUnit); - WriteResultsToFile(Console.Out, xmlResultJargon); + await WriteResultsToFile(Console.Out, xmlResultJargon); return ""; } - public override void WriteResultsToFile(TextWriter writer, XmlResultJargon jargon) + public override async Task WriteResultsToFile(TextWriter writer, XmlResultJargon jargon) { - if (_oneLineResults) - { - WasmXmlResultWriter.WriteOnSingleLine(_assembliesElement); - } - else - { - writer.WriteLine($"STARTRESULTXML"); - _assembliesElement.Save(writer); - writer.WriteLine(); - writer.WriteLine($"ENDRESULTXML"); - } + await WasmXmlResultWriter.WriteResultsToFile(_assembliesElement); } } diff --git a/src/Microsoft.DotNet.XHarness.TestRunners.Xunit/WasmApplicationEntryPoint.cs b/src/Microsoft.DotNet.XHarness.TestRunners.Xunit/WasmApplicationEntryPoint.cs index aa28cf2af..31751b807 100644 --- a/src/Microsoft.DotNet.XHarness.TestRunners.Xunit/WasmApplicationEntryPoint.cs +++ b/src/Microsoft.DotNet.XHarness.TestRunners.Xunit/WasmApplicationEntryPoint.cs @@ -28,7 +28,7 @@ public abstract class WasmApplicationEntryPoint : WasmApplicationEntryPointBase protected override TestRunner GetTestRunner(LogWriter logWriter) { XunitTestRunnerBase runner = IsThreadless - ? new ThreadlessXunitTestRunner(logWriter, true) + ? new ThreadlessXunitTestRunner(logWriter) : new WasmThreadedTestRunner(logWriter) { MaxParallelThreads = MaxParallelThreads }; ConfigureRunnerFilters(runner, ApplicationOptions.Current); diff --git a/src/Microsoft.DotNet.XHarness.TestRunners.Xunit/WasmThreadedTestRunner.cs b/src/Microsoft.DotNet.XHarness.TestRunners.Xunit/WasmThreadedTestRunner.cs index 77ac96453..cfddb05fb 100644 --- a/src/Microsoft.DotNet.XHarness.TestRunners.Xunit/WasmThreadedTestRunner.cs +++ b/src/Microsoft.DotNet.XHarness.TestRunners.Xunit/WasmThreadedTestRunner.cs @@ -31,6 +31,6 @@ public override Task Run(IEnumerable testAssemblies) return base.Run(testAssemblies); } - public override void WriteResultsToFile(TextWriter writer, XmlResultJargon jargon) - => WasmXmlResultWriter.WriteOnSingleLine(AssembliesElement); + public override Task WriteResultsToFile(TextWriter writer, XmlResultJargon jargon) + => WasmXmlResultWriter.WriteResultsToFile(AssembliesElement); } diff --git a/src/Microsoft.DotNet.XHarness.TestRunners.Xunit/WasmXmlResultWriter.cs b/src/Microsoft.DotNet.XHarness.TestRunners.Xunit/WasmXmlResultWriter.cs index cfef8d454..4c4ec793c 100644 --- a/src/Microsoft.DotNet.XHarness.TestRunners.Xunit/WasmXmlResultWriter.cs +++ b/src/Microsoft.DotNet.XHarness.TestRunners.Xunit/WasmXmlResultWriter.cs @@ -4,6 +4,10 @@ using System; using System.IO; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Runtime.InteropServices.JavaScript; +using System.Threading.Tasks; using System.Xml.Linq; #nullable enable @@ -12,10 +16,45 @@ namespace Microsoft.DotNet.XHarness.TestRunners.Xunit; internal class WasmXmlResultWriter { - public static void WriteOnSingleLine(XElement assembliesElement) + public async static Task WriteResultsToFile(XElement assembliesElement) { using var ms = new MemoryStream(); assembliesElement.Save(ms); + + if (OperatingSystem.IsBrowser()) + { + try + { + using JSObject globalThis = JSHost.GlobalThis; + if (globalThis.HasProperty("fetch") && globalThis.HasProperty("location") && globalThis.HasProperty("document")) + { + ms.Position = 0; + + // globalThis.location.origin + using JSObject location = globalThis.GetPropertyAsJSObject("location")!; + var originURL = location.GetPropertyAsString("origin"); + + using var req = new HttpRequestMessage(HttpMethod.Post, originURL + "/test-results"); + req.Content = new StreamContent(ms); + req.Content.Headers.ContentType = new MediaTypeHeaderValue("application/xml"); + req.Content.Headers.ContentLength = ms.Length; + + using var httpClient = new HttpClient(); + using var response = await httpClient.SendAsync(req); + if (response.IsSuccessStatusCode) + { + Console.WriteLine($"Finished uploading {ms.Length} bytes of RESULTXML"); + return; + } + // otherwise fall back to the console output + } + } + catch (Exception) + { + // fall back to the console output + } + } + ms.TryGetBuffer(out var bytes); var base64 = Convert.ToBase64String(bytes, Base64FormattingOptions.None); Console.WriteLine($"STARTRESULTXML {bytes.Count} {base64} ENDRESULTXML"); diff --git a/src/Microsoft.DotNet.XHarness.TestRunners.Xunit/XUnitTestRunner.cs b/src/Microsoft.DotNet.XHarness.TestRunners.Xunit/XUnitTestRunner.cs index 187e62671..84a3ec07b 100644 --- a/src/Microsoft.DotNet.XHarness.TestRunners.Xunit/XUnitTestRunner.cs +++ b/src/Microsoft.DotNet.XHarness.TestRunners.Xunit/XUnitTestRunner.cs @@ -901,11 +901,11 @@ public override async Task Run(IEnumerable testAssemblies) TotalTests += FilteredTests; // ensure that we do have in the total run the excluded ones. } - public override string WriteResultsToFile(XmlResultJargon jargon) + public override Task WriteResultsToFile(XmlResultJargon jargon) { if (_assembliesElement == null) { - return string.Empty; + return Task.FromResult(string.Empty); } // remove all the empty nodes _assembliesElement.Descendants().Where(e => e.Name == "collection" && !e.Descendants().Any()).Remove(); @@ -926,15 +926,15 @@ public override string WriteResultsToFile(XmlResultJargon jargon) _assembliesElement.Save(xmlWriter); break; } - } - - return outputFilePath; + } + + return Task.FromResult(outputFilePath); } - public override void WriteResultsToFile(TextWriter writer, XmlResultJargon jargon) + public override Task WriteResultsToFile(TextWriter writer, XmlResultJargon jargon) { if (_assembliesElement == null) { - return; + return Task.CompletedTask; } // remove all the empty nodes _assembliesElement.Descendants().Where(e => e.Name == "collection" && !e.Descendants().Any()).Remove(); @@ -969,6 +969,7 @@ public override void WriteResultsToFile(TextWriter writer, XmlResultJargon jargo break; } } + return Task.CompletedTask; } private void Transform_Results(string xsltResourceName, XElement element, XmlWriter writer)