From c0312872fd5f21779f70ebe9b43c1614572de39b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Jare=C5=A1?= Date: Wed, 4 Sep 2024 12:05:17 +0200 Subject: [PATCH 01/20] fixes --- Directory.Packages.props | 23 +- NuGet.config | 1 + eng/Versions.props | 2 +- src/Cli/dotnet/Class1.cs | 334 ++++++++++++++++++ src/Cli/dotnet/Properties/launchSettings.json | 11 +- .../commands/dotnet-test/CustomEventArgs.cs | 2 +- .../dotnet-test/MSBuildConnectionHandler.cs | 1 + .../commands/dotnet-test/TestApplication.cs | 2 +- .../dotnet-test/TestingPlatformCommand.cs | 85 ++++- src/Cli/dotnet/dotnet.csproj | 1 + 10 files changed, 442 insertions(+), 20 deletions(-) create mode 100644 src/Cli/dotnet/Class1.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index bc6204e13b19..976ef0a8aa5f 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,10 +1,9 @@ - + $(NoWarn);NU1507 - @@ -12,7 +11,7 @@ - + @@ -43,7 +42,7 @@ - + @@ -63,6 +62,7 @@ + @@ -95,11 +95,11 @@ - - - - - + + + + + @@ -119,9 +119,8 @@ - + - + diff --git a/eng/Versions.props b/eng/Versions.props index 18c89b5250f7..0c312f8703f8 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -77,7 +77,7 @@ 2.0.3 13.0.3 4.8.6 - 1.2.0-beta.435 + 1.2.0-beta.507 4.0.5 2.0.0-beta4.24324.3 0.4.0-alpha.24324.3 diff --git a/src/Cli/dotnet/Class1.cs b/src/Cli/dotnet/Class1.cs new file mode 100644 index 000000000000..3497a9c31b9b --- /dev/null +++ b/src/Cli/dotnet/Class1.cs @@ -0,0 +1,334 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#pragma warning disable CA1837 // Use 'Environment.ProcessId' +#pragma warning disable CA1416 // Validate platform compatibility + +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.InteropServices.ComTypes; + +namespace Microsoft.Testing.TestInfrastructure; + +public class DebuggerUtility +{ + public static bool AttachCurrentProcessToParentVSProcess(bool enableLog = false) => AttachVSToProcess(Process.GetCurrentProcess().Id, null, enableLog); + + public static bool AttachCurrentProcessToVSProcessPID(int vsProcessPid, bool enableLog = false) => AttachVSToProcess(Process.GetCurrentProcess().Id, vsProcessPid, enableLog); + + private static bool AttachVSToProcess(int? pid, int? vsPid, bool enableLog = false) + { + try + { + if (pid == null) + { + Trace($"FAIL: Pid is null.", enabled: enableLog); + return false; + } + + var process = Process.GetProcessById(pid.Value); + Trace($"Starting with pid '{pid}({process.ProcessName})', and vsPid '{vsPid}'", enabled: enableLog); + Trace($"Using pid: {pid} to get parent VS.", enabled: enableLog); + Process vs = GetVsFromPid(Process.GetProcessById(vsPid ?? process.Id)); + + if (vs != null) + { + Trace($"Parent VS is {vs.ProcessName} ({vs.Id}).", enabled: enableLog); + AttachTo(process, vs); + return true; + } + + Trace($"Parent VS not found, finding the first VS that started.", enabled: enableLog); + var firstVs = Process.GetProcesses() + .Where(p => p.ProcessName == "devenv") + .Select(p => + { + try + { + return new { Process = p, p.StartTime, p.HasExited }; + } + catch + { + return null; + } + }) + .Where(p => p != null && !p.HasExited) + .OrderBy(p => p!.StartTime) + .FirstOrDefault(); + + if (firstVs != null) + { + Trace($"Found VS {firstVs.Process.Id}", enabled: enableLog); + AttachTo(process, firstVs.Process); + return true; + } + + Trace("Could not find any started VS.", enabled: enableLog); + } + catch (Exception ex) + { + Trace($"ERROR: {ex}, {ex.StackTrace}", enabled: enableLog); + } + + return false; + } + + private static void AttachTo(Process process, Process vs, bool enableLog = false) + { + bool attached = AttachVs(vs, process.Id); + if (attached) + { + // You won't see this in DebugView++ because at this point VS is already attached and all the output goes into Debug window in VS. + Trace($"SUCCESS: Attached process: {process.ProcessName} ({process.Id})", enabled: enableLog); + } + else + { + Trace($"FAIL: Could not attach process: {process.ProcessName} ({process.Id})", enabled: enableLog); + } + } + + private static bool AttachVs(Process vs, int pid, bool enableLog = false) + { + IBindCtx bindCtx = null; + IRunningObjectTable runningObjectTable = null; + IEnumMoniker enumMoniker = null; + try + { +#pragma warning disable IL2050 + int r = CreateBindCtx(0, out bindCtx); +#pragma warning restore IL2050 + Marshal.ThrowExceptionForHR(r); + if (bindCtx == null) + { + Trace($"BindCtx is null. Cannot attach VS.", enabled: enableLog); + return false; + } + + bindCtx.GetRunningObjectTable(out runningObjectTable); + if (runningObjectTable == null) + { + Trace($"RunningObjectTable is null. Cannot attach VS.", enabled: enableLog); + return false; + } + + runningObjectTable.EnumRunning(out enumMoniker); + if (enumMoniker == null) + { + Trace($"EnumMoniker is null. Cannot attach VS.", enabled: enableLog); + return false; + } + + string dteSuffix = ":" + vs.Id; + + var moniker = new IMoniker[1]; + while (enumMoniker.Next(1, moniker, IntPtr.Zero) == 0 && moniker[0] != null) + { + moniker[0].GetDisplayName(bindCtx, null, out string dn); + + if (dn.StartsWith("!VisualStudio.DTE.", StringComparison.Ordinal) && dn.EndsWith(dteSuffix, StringComparison.Ordinal)) + { + object dbg, lps; + runningObjectTable.GetObject(moniker[0], out object dte); + + // The COM object can be busy, we retry few times, hoping that it won't be busy next time. + for (int i = 0; i < 10; i++) + { + try + { + dbg = dte.GetType().InvokeMember("Debugger", BindingFlags.GetProperty, null, dte, null, CultureInfo.InvariantCulture)!; + lps = dbg.GetType().InvokeMember("LocalProcesses", BindingFlags.GetProperty, null, dbg, null, CultureInfo.InvariantCulture)!; + var lpn = (System.Collections.IEnumerator)lps.GetType().InvokeMember("GetEnumerator", BindingFlags.InvokeMethod, null, lps, null, CultureInfo.InvariantCulture)!; + + while (lpn.MoveNext()) + { + int pn = Convert.ToInt32(lpn.Current.GetType().InvokeMember("ProcessID", BindingFlags.GetProperty, null, lpn.Current, null, CultureInfo.InvariantCulture), CultureInfo.InvariantCulture); + + if (pn == pid) + { + lpn.Current.GetType().InvokeMember("Attach", BindingFlags.InvokeMethod, null, lpn.Current, null, CultureInfo.InvariantCulture); + return true; + } + } + } + + // Catch the exception if it is COMException coming directly, or coming from methodInvocation, otherwise just let it be. + catch (Exception ex) when (ex is COMException or TargetInvocationException { InnerException: COMException }) + { + Trace($"ComException: Retrying in 250ms.\n{ex}", enabled: enableLog); + Thread.Sleep(250); + } + } + + Marshal.ReleaseComObject(moniker[0]); + + break; + } + + Marshal.ReleaseComObject(moniker[0]); + } + + return false; + } + finally + { + if (enumMoniker != null) + { + try + { + Marshal.ReleaseComObject(enumMoniker); + } + catch + { + } + } + + if (runningObjectTable != null) + { + try + { + Marshal.ReleaseComObject(runningObjectTable); + } + catch + { + } + } + + if (bindCtx != null) + { + try + { + Marshal.ReleaseComObject(bindCtx); + } + catch + { + } + } + } + } + + private static Process GetVsFromPid(Process process) + { + Process parent = process; + while (!IsVsOrNull(parent)) + { + parent = GetParentProcess(parent); + } + + return parent; + } + + private static bool IsVsOrNull([NotNullWhen(false)] Process process, bool enableLog = false) + { + if (process == null) + { + Trace("Parent process is null..", enabled: enableLog); + return true; + } + + bool isVs = process.ProcessName.Equals("devenv", StringComparison.OrdinalIgnoreCase); + if (isVs) + { + Trace($"Process {process.ProcessName} ({process.Id}) is VS.", enabled: enableLog); + } + else + { + Trace($"Process {process.ProcessName} ({process.Id}) is not VS.", enabled: enableLog); + } + + return isVs; + } + + private static bool IsCorrectParent(Process currentProcess, Process parent, bool enableLog = false) + { + try + { + // Parent needs to start before the child, otherwise it might be a different process + // that is just reusing the same PID. + if (parent.StartTime <= currentProcess.StartTime) + { + return true; + } + + Trace($"Process {parent.ProcessName} ({parent.Id}) is not a valid parent because it started after the current process.", enabled: enableLog); + } + catch + { + // Access denied or process exited while we were holding the Process object. + } + + return false; + } + + private static Process GetParentProcess(Process process) + { + int id = GetParentProcessId(process); + if (id != -1) + { + try + { + var parent = Process.GetProcessById(id); + if (IsCorrectParent(process, parent)) + { + return parent; + } + } + catch + { + // throws when parent no longer runs + } + } + + return null; + + static int GetParentProcessId(Process process) + { + try + { + IntPtr handle = process.Handle; + int res = NtQueryInformationProcess(handle, 0, out PROCESS_BASIC_INFORMATION pbi, Marshal.SizeOf(), out int size); + + int p = res != 0 ? -1 : pbi.InheritedFromUniqueProcessId.ToInt32(); + + return p; + } + catch + { + return -1; + } + } + } + + private static void Trace(string message, [CallerMemberName] string methodName = null, bool enabled = false) + { + if (enabled) + { + Console.WriteLine($"[AttachVS]{methodName}: {message}"); + } + } + + [StructLayout(LayoutKind.Sequential)] + private struct PROCESS_BASIC_INFORMATION + { + public readonly IntPtr ExitStatus; + public readonly IntPtr PebBaseAddress; + public readonly IntPtr AffinityMask; + public readonly IntPtr BasePriority; + public readonly IntPtr UniqueProcessId; + public IntPtr InheritedFromUniqueProcessId; + } + + [DllImport("ntdll.dll", SetLastError = true)] + private static extern int NtQueryInformationProcess( + IntPtr processHandle, + int processInformationClass, + out PROCESS_BASIC_INFORMATION processInformation, + int processInformationLength, + out int returnLength); + + [DllImport("ole32.dll")] + private static extern int CreateBindCtx(uint reserved, out IBindCtx ppbc); +} diff --git a/src/Cli/dotnet/Properties/launchSettings.json b/src/Cli/dotnet/Properties/launchSettings.json index 8d0ebf35dc6f..ccb2fad63201 100644 --- a/src/Cli/dotnet/Properties/launchSettings.json +++ b/src/Cli/dotnet/Properties/launchSettings.json @@ -1,7 +1,14 @@ { "profiles": { "dotnet": { - "commandName": "Project" + "commandName": "Project", + "commandLineArgs": "test -bl:S:\\t\\mstest184\\log.binlog", + "workingDirectory": "S:\\t\\mstest184", + "environmentVariables": { + "DOTNET_CLI_TESTINGPLATFORM_ENABLE": "1", + "DOTNET_CLI_VSTEST_TRACE": "1", + "DOTNET_ROOT": "S:\\p\\dotnet-sdk\\artifacts\\bin\\redist\\Debug\\dotnet", + } } } -} \ No newline at end of file +} diff --git a/src/Cli/dotnet/commands/dotnet-test/CustomEventArgs.cs b/src/Cli/dotnet/commands/dotnet-test/CustomEventArgs.cs index ec2c5fa5a63f..8914a2031c42 100644 --- a/src/Cli/dotnet/commands/dotnet-test/CustomEventArgs.cs +++ b/src/Cli/dotnet/commands/dotnet-test/CustomEventArgs.cs @@ -7,7 +7,7 @@ namespace Microsoft.DotNet.Cli { internal class HandshakeInfoArgs : EventArgs { - public HandshakeInfo handshakeInfo { get; set; } + public HandshakeInfo HandshakeInfo { get; set; } } internal class HelpEventArgs : EventArgs diff --git a/src/Cli/dotnet/commands/dotnet-test/MSBuildConnectionHandler.cs b/src/Cli/dotnet/commands/dotnet-test/MSBuildConnectionHandler.cs index d57e75c7e83b..a253b670451a 100644 --- a/src/Cli/dotnet/commands/dotnet-test/MSBuildConnectionHandler.cs +++ b/src/Cli/dotnet/commands/dotnet-test/MSBuildConnectionHandler.cs @@ -65,6 +65,7 @@ private Task OnRequest(IRequest request) var testApp = new TestApplication(module.DLLPath, _args); // Write the test application to the channel _actionQueue.Enqueue(testApp); + Thread.Sleep(100); testApp.OnCreated(); } catch (Exception ex) diff --git a/src/Cli/dotnet/commands/dotnet-test/TestApplication.cs b/src/Cli/dotnet/commands/dotnet-test/TestApplication.cs index 061cf446b1ec..8fac559a390d 100644 --- a/src/Cli/dotnet/commands/dotnet-test/TestApplication.cs +++ b/src/Cli/dotnet/commands/dotnet-test/TestApplication.cs @@ -269,7 +269,7 @@ public void OnHandshakeInfo(HandshakeInfo handshakeInfo) { ExecutionIdReceived?.Invoke(this, new ExecutionEventArgs { ModulePath = _modulePath, ExecutionId = executionId }); } - HandshakeInfoReceived?.Invoke(this, new HandshakeInfoArgs { handshakeInfo = handshakeInfo }); + HandshakeInfoReceived?.Invoke(this, new HandshakeInfoArgs { HandshakeInfo = handshakeInfo }); } public void OnCommandLineOptionMessages(CommandLineOptionMessages commandLineOptionMessages) diff --git a/src/Cli/dotnet/commands/dotnet-test/TestingPlatformCommand.cs b/src/Cli/dotnet/commands/dotnet-test/TestingPlatformCommand.cs index 8096d6f69c44..73f3c4cdd20d 100644 --- a/src/Cli/dotnet/commands/dotnet-test/TestingPlatformCommand.cs +++ b/src/Cli/dotnet/commands/dotnet-test/TestingPlatformCommand.cs @@ -3,8 +3,12 @@ using System.Collections.Concurrent; using System.CommandLine; +using Microsoft.DotNet.Cli.Utils; using Microsoft.DotNet.Tools.Test; using Microsoft.TemplateEngine.Cli.Commands; +using Microsoft.Testing.Platform.Helpers; +using Microsoft.Testing.Platform.OutputDevice.Terminal; +using Microsoft.Testing.TestInfrastructure; namespace Microsoft.DotNet.Cli { @@ -15,9 +19,11 @@ internal partial class TestingPlatformCommand : CliCommand, ICustomHelp private MSBuildConnectionHandler _msBuildConnectionHandler; private TestModulesFilterHandler _testModulesFilterHandler; + private TerminalTestReporter _output; private TestApplicationActionQueue _actionQueue; private Task _namedPipeConnectionLoop; private string[] _args; + private Dictionary _executions = new(); public TestingPlatformCommand(string name, string description = null) : base(name, description) { @@ -26,6 +32,11 @@ public TestingPlatformCommand(string name, string description = null) : base(nam public int Run(ParseResult parseResult) { + if (Environment.GetEnvironmentVariable("Debug") == "1") + { + DebuggerUtility.AttachCurrentProcessToParentVSProcess(); + } + if (parseResult.HasOption(TestingPlatformOptions.ArchitectureOption)) { VSTestTrace.SafeWriteTrace(() => $"The --arch option is not yet supported."); @@ -37,6 +48,16 @@ public int Run(ParseResult parseResult) if (!int.TryParse(parseResult.GetValue(TestingPlatformOptions.MaxParallelTestModulesOption), out int degreeOfParallelism)) degreeOfParallelism = Environment.ProcessorCount; + var console = new SystemConsole(); + var output = new TerminalTestReporter(console, new TerminalTestReporterOptions() + { + UseAnsi = true, + ShowAssembly = true, + ShowAssemblyStartAndComplete = true, + }); + _output = output; + _output.TestExecutionStarted(DateTimeOffset.Now, degreeOfParallelism); + if (ContainsHelpOption(parseResult.GetArguments())) { _actionQueue = new(degreeOfParallelism, async (TestApplication testApp) => @@ -47,7 +68,9 @@ public int Run(ParseResult parseResult) testApp.Created += OnTestApplicationCreated; testApp.ExecutionIdReceived += OnExecutionIdReceived; - return await testApp.RunAsync(enableHelp: true); + var result = await testApp.RunAsync(enableHelp: true); + _output.TestExecutionCompleted(DateTimeOffset.Now); + return result; }); } else @@ -65,7 +88,8 @@ public int Run(ParseResult parseResult) testApp.Created += OnTestApplicationCreated; testApp.ExecutionIdReceived += OnExecutionIdReceived; - return await testApp.RunAsync(enableHelp: false); + var result = await testApp.RunAsync(enableHelp: false); + return result; }); } @@ -78,6 +102,7 @@ public int Run(ParseResult parseResult) { if (!_testModulesFilterHandler.RunWithTestModulesFilter(parseResult)) { + _output.TestExecutionCompleted(DateTimeOffset.Now); return ExitCodes.GenericFailure; } } @@ -88,6 +113,7 @@ public int Run(ParseResult parseResult) if (msbuildResult != 0) { VSTestTrace.SafeWriteTrace(() => $"MSBuild task _GetTestsProject didn't execute properly with exit code: {msbuildResult}."); + _output.TestExecutionCompleted(DateTimeOffset.Now); return ExitCodes.GenericFailure; } } @@ -102,6 +128,7 @@ public int Run(ParseResult parseResult) // Clean up everything CleanUp(); + _output.TestExecutionCompleted(DateTimeOffset.Now); return hasFailed ? ExitCodes.GenericFailure : ExitCodes.Success; } @@ -116,12 +143,20 @@ private void CleanUp() private void OnHandshakeInfoReceived(object sender, HandshakeInfoArgs args) { + var testApplication = (TestApplication)sender; + var executionId = args.HandshakeInfo.Properties[HandshakeInfoPropertyNames.ExecutionId]; + var arch = args.HandshakeInfo.Properties[HandshakeInfoPropertyNames.Architecture]; + var tfm = args.HandshakeInfo.Properties[HandshakeInfoPropertyNames.Framework]; + (string ModulePath, string TargetFramework, string Architecture, string ExecutionId) appInfo = new(testApplication.ModulePath, tfm, arch, executionId); + _executions[testApplication] = appInfo; + _output.AssemblyRunStarted(appInfo.ModulePath, appInfo.TargetFramework, appInfo.Architecture, appInfo.ExecutionId); + if (!VSTestTrace.TraceEnabled) { return; } - var handshakeInfo = args.handshakeInfo; + var handshakeInfo = args.HandshakeInfo; foreach (var property in handshakeInfo.Properties) { @@ -142,6 +177,37 @@ private void OnDiscoveredTestReceived(object sender, DiscoveredTestEventArgs arg private void OnTestResultReceived(object sender, EventArgs args) { + if (args is SuccessfulTestResultEventArgs success) + { + var testApp = (TestApplication)sender; + var appInfo = _executions[testApp]; + // TODO: timespan for duration + _output.TestCompleted(appInfo.ModulePath, appInfo.TargetFramework, appInfo.Architecture, appInfo.ExecutionId, + success.SuccessfulTestResultMessage.DisplayName, + TestOutcome.Passed, + TimeSpan.FromSeconds(1), + errorMessage: null, + errorStackTrace: null, + expected: null, + actual: null); + } + else if (args is FailedTestResultEventArgs failed) + { + var testApp = (TestApplication)sender; + // TODO: timespan for duration + // TODO: expected + // TODO: actual + var appInfo = _executions[testApp]; + _output.TestCompleted(appInfo.ModulePath, appInfo.TargetFramework, appInfo.Architecture, appInfo.ExecutionId, + failed.FailedTestResultMessage.DisplayName, + TestOutcome.Fail, + TimeSpan.FromSeconds(1), + errorMessage: failed.FailedTestResultMessage.ErrorMessage, + errorStackTrace: failed.FailedTestResultMessage.ErrorStackTrace, + expected: null, actual: null); + } + + if (!VSTestTrace.TraceEnabled) { return; @@ -198,6 +264,11 @@ private void OnErrorReceived(object sender, ErrorEventArgs args) private void OnTestProcessExited(object sender, TestProcessExitEventArgs args) { + var testApplication = (TestApplication)sender; + + var appInfo = _executions[testApplication]; + _output.AssemblyRunCompleted(appInfo.ModulePath, appInfo.TargetFramework, appInfo.Architecture, appInfo.ExecutionId); + if (!VSTestTrace.TraceEnabled) { return; @@ -223,12 +294,18 @@ private void OnTestApplicationCreated(object sender, EventArgs args) { TestApplication testApp = sender as TestApplication; _testApplications[testApp.ModulePath] = testApp; + + VSTestTrace.SafeWriteTrace(() => $"Created {testApp.ModulePath}"); + + } private void OnExecutionIdReceived(object sender, ExecutionEventArgs args) { if (_testApplications.TryGetValue(args.ModulePath, out var testApp)) { + VSTestTrace.SafeWriteTrace(() => $"id {args.ModulePath}"); + testApp.AddExecutionId(args.ExecutionId); } } @@ -236,3 +313,5 @@ private void OnExecutionIdReceived(object sender, ExecutionEventArgs args) private static bool ContainsHelpOption(IEnumerable args) => args.Contains(CliConstants.HelpOptionKey) || args.Contains(CliConstants.HelpOptionKey.Substring(0, 2)); } } + + diff --git a/src/Cli/dotnet/dotnet.csproj b/src/Cli/dotnet/dotnet.csproj index 9e98bade78e4..6e479523d2c0 100644 --- a/src/Cli/dotnet/dotnet.csproj +++ b/src/Cli/dotnet/dotnet.csproj @@ -96,6 +96,7 @@ + From 990139637d9ce72fd3577bd46339dfa37a5683bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Jare=C5=A1?= Date: Wed, 4 Sep 2024 16:57:36 +0200 Subject: [PATCH 02/20] make testing easier --- .../commands/dotnet-test/TestingPlatformCommand.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Cli/dotnet/commands/dotnet-test/TestingPlatformCommand.cs b/src/Cli/dotnet/commands/dotnet-test/TestingPlatformCommand.cs index 00b69447a8ac..65f1887bf9b6 100644 --- a/src/Cli/dotnet/commands/dotnet-test/TestingPlatformCommand.cs +++ b/src/Cli/dotnet/commands/dotnet-test/TestingPlatformCommand.cs @@ -7,6 +7,7 @@ using Microsoft.DotNet.Tools.Test; using Microsoft.TemplateEngine.Cli.Commands; using Microsoft.Testing.Platform.Helpers; +using Microsoft.Testing.Platform.OutputDevice; using Microsoft.Testing.Platform.OutputDevice.Terminal; using Microsoft.Testing.TestInfrastructure; @@ -58,7 +59,9 @@ public int Run(ParseResult parseResult) var console = new SystemConsole(); var output = new TerminalTestReporter(console, new TerminalTestReporterOptions() { - UseAnsi = true, + ShowPassedTests = Environment.GetEnvironmentVariable("SHOW_PASSED") == "1", + ShowProgress = () => Environment.GetEnvironmentVariable("NO_PROGRESS") != "1", + UseAnsi = Environment.GetEnvironmentVariable("NO_ANSI") != "1", ShowAssembly = true, ShowAssemblyStartAndComplete = true, }); @@ -151,8 +154,8 @@ private void OnHandshakeInfoReceived(object sender, HandshakeInfoArgs args) { var testApplication = (TestApplication)sender; var executionId = args.HandshakeInfo.Properties[HandshakeInfoPropertyNames.ExecutionId]; - var arch = args.HandshakeInfo.Properties[HandshakeInfoPropertyNames.Architecture]; - var tfm = args.HandshakeInfo.Properties[HandshakeInfoPropertyNames.Framework]; + var arch = args.HandshakeInfo.Properties[HandshakeInfoPropertyNames.Architecture]?.ToLower(); + var tfm = TargetFrameworkParser.GetShortTargetFramework(args.HandshakeInfo.Properties[HandshakeInfoPropertyNames.Framework]); (string ModulePath, string TargetFramework, string Architecture, string ExecutionId) appInfo = new(testApplication.Module.DLLPath, tfm, arch, executionId); _executions[testApplication] = appInfo; _output.AssemblyRunStarted(appInfo.ModulePath, appInfo.TargetFramework, appInfo.Architecture, appInfo.ExecutionId); From 8549dc2f8bbca321fc8a971d4d38c51144a2f8e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Jare=C5=A1?= Date: Wed, 18 Sep 2024 15:09:39 +0200 Subject: [PATCH 03/20] up --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 976ef0a8aa5f..4cec76f6e40a 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -62,7 +62,7 @@ - + From 39cbf436eee52f34356f24a2243c620dd2a28fe3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Jare=C5=A1?= Date: Wed, 18 Sep 2024 15:32:36 +0200 Subject: [PATCH 04/20] update --- .../dotnet-test/TestingPlatformCommand.cs | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/Cli/dotnet/commands/dotnet-test/TestingPlatformCommand.cs b/src/Cli/dotnet/commands/dotnet-test/TestingPlatformCommand.cs index 0da84b379015..80b2e25b30bc 100644 --- a/src/Cli/dotnet/commands/dotnet-test/TestingPlatformCommand.cs +++ b/src/Cli/dotnet/commands/dotnet-test/TestingPlatformCommand.cs @@ -158,9 +158,9 @@ private void CleanUp() private void OnHandshakeReceived(object sender, HandshakeArgs args) { var testApplication = (TestApplication)sender; - var executionId = args.HandshakeInfo.Properties[HandshakeInfoPropertyNames.ExecutionId]; - var arch = args.HandshakeInfo.Properties[HandshakeInfoPropertyNames.Architecture]?.ToLower(); - var tfm = TargetFrameworkParser.GetShortTargetFramework(args.HandshakeInfo.Properties[HandshakeInfoPropertyNames.Framework]); + var executionId = args.Handshake.Properties[HandshakeMessagePropertyNames.ExecutionId]; + var arch = args.Handshake.Properties[HandshakeMessagePropertyNames.Architecture]?.ToLower(); + var tfm = TargetFrameworkParser.GetShortTargetFramework(args.Handshake.Properties[HandshakeMessagePropertyNames.Framework]); (string ModulePath, string TargetFramework, string Architecture, string ExecutionId) appInfo = new(testApplication.Module.DLLPath, tfm, arch, executionId); _executions[testApplication] = appInfo; _output.AssemblyRunStarted(appInfo.ModulePath, appInfo.TargetFramework, appInfo.Architecture, appInfo.ExecutionId); @@ -196,13 +196,14 @@ private void OnDiscoveredTestsReceived(object sender, DiscoveredTestEventArgs ar private void OnTestResultsReceived(object sender, TestResultEventArgs args) { - if (args is SuccessfulTestResultEventArgs success) + foreach (var testResult in args.SuccessfulTestResults) { + var testApp = (TestApplication)sender; var appInfo = _executions[testApp]; // TODO: timespan for duration _output.TestCompleted(appInfo.ModulePath, appInfo.TargetFramework, appInfo.Architecture, appInfo.ExecutionId, - success.SuccessfulTestResultMessage.DisplayName, + testResult.DisplayName, TestOutcome.Passed, TimeSpan.FromSeconds(1), errorMessage: null, @@ -210,19 +211,21 @@ private void OnTestResultsReceived(object sender, TestResultEventArgs args) expected: null, actual: null); } - else if (args is FailedTestResultEventArgs failed) + + foreach (var testResult in args.FailedTestResults) { + var testApp = (TestApplication)sender; // TODO: timespan for duration // TODO: expected // TODO: actual var appInfo = _executions[testApp]; _output.TestCompleted(appInfo.ModulePath, appInfo.TargetFramework, appInfo.Architecture, appInfo.ExecutionId, - failed.FailedTestResultMessage.DisplayName, + testResult.DisplayName, TestOutcome.Fail, TimeSpan.FromSeconds(1), - errorMessage: failed.FailedTestResultMessage.ErrorMessage, - errorStackTrace: failed.FailedTestResultMessage.ErrorStackTrace, + errorMessage: testResult.ErrorMessage, + errorStackTrace: testResult.ErrorStackTrace, expected: null, actual: null); } From a35faa3d156090ba6e229ddb0d27d13ee5a2c77e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Jare=C5=A1?= Date: Tue, 8 Oct 2024 09:26:52 +0200 Subject: [PATCH 05/20] Fix --- src/Cli/dotnet/commands/dotnet-test/TestingPlatformCommand.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Cli/dotnet/commands/dotnet-test/TestingPlatformCommand.cs b/src/Cli/dotnet/commands/dotnet-test/TestingPlatformCommand.cs index 80b2e25b30bc..20e6666091e2 100644 --- a/src/Cli/dotnet/commands/dotnet-test/TestingPlatformCommand.cs +++ b/src/Cli/dotnet/commands/dotnet-test/TestingPlatformCommand.cs @@ -161,7 +161,7 @@ private void OnHandshakeReceived(object sender, HandshakeArgs args) var executionId = args.Handshake.Properties[HandshakeMessagePropertyNames.ExecutionId]; var arch = args.Handshake.Properties[HandshakeMessagePropertyNames.Architecture]?.ToLower(); var tfm = TargetFrameworkParser.GetShortTargetFramework(args.Handshake.Properties[HandshakeMessagePropertyNames.Framework]); - (string ModulePath, string TargetFramework, string Architecture, string ExecutionId) appInfo = new(testApplication.Module.DLLPath, tfm, arch, executionId); + (string ModulePath, string TargetFramework, string Architecture, string ExecutionId) appInfo = new(testApplication.Module.DLLOrExe, tfm, arch, executionId); _executions[testApplication] = appInfo; _output.AssemblyRunStarted(appInfo.ModulePath, appInfo.TargetFramework, appInfo.Architecture, appInfo.ExecutionId); From 137031807193230a7dcb711e11946652ebae6a34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Jare=C5=A1?= Date: Fri, 11 Oct 2024 20:44:34 +0200 Subject: [PATCH 06/20] Convert to correct state --- .../dotnet-test/TestingPlatformCommand.cs | 30 +++++++++++++++---- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/src/Cli/dotnet/commands/dotnet-test/TestingPlatformCommand.cs b/src/Cli/dotnet/commands/dotnet-test/TestingPlatformCommand.cs index 20e6666091e2..686606c97700 100644 --- a/src/Cli/dotnet/commands/dotnet-test/TestingPlatformCommand.cs +++ b/src/Cli/dotnet/commands/dotnet-test/TestingPlatformCommand.cs @@ -7,6 +7,7 @@ using Microsoft.DotNet.Tools.Test; using Microsoft.TemplateEngine.Cli.Commands; using Microsoft.Testing.Platform.Helpers; +using Microsoft.Testing.Platform.IPC; using Microsoft.Testing.Platform.OutputDevice; using Microsoft.Testing.Platform.OutputDevice.Terminal; using Microsoft.Testing.TestInfrastructure; @@ -204,12 +205,14 @@ private void OnTestResultsReceived(object sender, TestResultEventArgs args) // TODO: timespan for duration _output.TestCompleted(appInfo.ModulePath, appInfo.TargetFramework, appInfo.Architecture, appInfo.ExecutionId, testResult.DisplayName, - TestOutcome.Passed, + ToOutcome(testResult.State), TimeSpan.FromSeconds(1), errorMessage: null, errorStackTrace: null, expected: null, - actual: null); + actual: null, + standardOutput: null, + errorOutput: null); } foreach (var testResult in args.FailedTestResults) @@ -222,11 +225,14 @@ private void OnTestResultsReceived(object sender, TestResultEventArgs args) var appInfo = _executions[testApp]; _output.TestCompleted(appInfo.ModulePath, appInfo.TargetFramework, appInfo.Architecture, appInfo.ExecutionId, testResult.DisplayName, - TestOutcome.Fail, + ToOutcome(testResult.State), TimeSpan.FromSeconds(1), errorMessage: testResult.ErrorMessage, errorStackTrace: testResult.ErrorStackTrace, - expected: null, actual: null); + expected: null, + actual: null, + standardOutput: null, + errorOutput: null); } @@ -251,6 +257,20 @@ private void OnTestResultsReceived(object sender, TestResultEventArgs args) } } + public static TestOutcome ToOutcome(byte? testState) + { + return testState switch + { + TestStates.Passed => TestOutcome.Passed, + TestStates.Skipped => TestOutcome.Skipped, + TestStates.Failed => TestOutcome.Fail, + TestStates.Error => TestOutcome.Error, + TestStates.Timeout => TestOutcome.Timeout, + TestStates.Cancelled => TestOutcome.Canceled, + _ => throw new ArgumentOutOfRangeException(nameof(testState), $"Invalid test state value {testState}") + }; + } + private void OnFileArtifactsReceived(object sender, FileArtifactEventArgs args) { if (!VSTestTrace.TraceEnabled) @@ -262,7 +282,7 @@ private void OnFileArtifactsReceived(object sender, FileArtifactEventArgs args) foreach (FileArtifact fileArtifactMessage in args.FileArtifacts) { - VSTestTrace.SafeWriteTrace(() => $"FileArtifacr: {fileArtifactMessage.FullPath}, {fileArtifactMessage.DisplayName}, " + + VSTestTrace.SafeWriteTrace(() => $"FileArtifact: {fileArtifactMessage.FullPath}, {fileArtifactMessage.DisplayName}, " + $"{fileArtifactMessage.Description}, {fileArtifactMessage.TestUid}, {fileArtifactMessage.TestDisplayName}, " + $"{fileArtifactMessage.SessionUid}"); } From 38cb652ffd532e53d9ba4d64199fa11af9e25a3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Jare=C5=A1?= Date: Mon, 14 Oct 2024 15:08:39 +0200 Subject: [PATCH 07/20] Fix cancellation --- .../dotnet-test/TestingPlatformCommand.cs | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/src/Cli/dotnet/commands/dotnet-test/TestingPlatformCommand.cs b/src/Cli/dotnet/commands/dotnet-test/TestingPlatformCommand.cs index 686606c97700..0a4ce0a9c902 100644 --- a/src/Cli/dotnet/commands/dotnet-test/TestingPlatformCommand.cs +++ b/src/Cli/dotnet/commands/dotnet-test/TestingPlatformCommand.cs @@ -26,6 +26,7 @@ internal partial class TestingPlatformCommand : CliCommand, ICustomHelp private Task _namedPipeConnectionLoop; private List _args; private Dictionary _executions = new(); + private byte _cancelled; public TestingPlatformCommand(string name, string description = null) : base(name, description) { @@ -34,6 +35,12 @@ public TestingPlatformCommand(string name, string description = null) : base(nam public int Run(ParseResult parseResult) { + Console.CancelKeyPress += (s, e) => + { + _output?.StartCancelling(); + CompleteRun(); + }; + if (Environment.GetEnvironmentVariable("Debug") == "1") { DebuggerUtility.AttachCurrentProcessToParentVSProcess(); @@ -86,7 +93,7 @@ public int Run(ParseResult parseResult) testApp.ExecutionIdReceived += OnExecutionIdReceived; var result = await testApp.RunAsync(filterModeEnabled, enableHelp: true, builtInOptions); - _output.TestExecutionCompleted(DateTimeOffset.Now); + CompleteRun(); return result; }); } @@ -117,7 +124,7 @@ public int Run(ParseResult parseResult) { if (!_testModulesFilterHandler.RunWithTestModulesFilter(parseResult)) { - _output.TestExecutionCompleted(DateTimeOffset.Now); + CompleteRun(); return ExitCodes.GenericFailure; } } @@ -128,7 +135,7 @@ public int Run(ParseResult parseResult) if (msbuildResult != 0) { VSTestTrace.SafeWriteTrace(() => $"MSBuild task _GetTestsProject didn't execute properly with exit code: {msbuildResult}."); - _output.TestExecutionCompleted(DateTimeOffset.Now); + CompleteRun(); return ExitCodes.GenericFailure; } } @@ -143,10 +150,18 @@ public int Run(ParseResult parseResult) // Clean up everything CleanUp(); - _output.TestExecutionCompleted(DateTimeOffset.Now); + CompleteRun(); return hasFailed ? ExitCodes.GenericFailure : ExitCodes.Success; } + private void CompleteRun() + { + if (Interlocked.CompareExchange(ref _cancelled, 1, 0) == 0) + { + _output?.TestExecutionCompleted(DateTimeOffset.Now); + } + } + private void CleanUp() { _msBuildConnectionHandler.Dispose(); From 721ccd455f6724091d7b5c230c56d484a1baac39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Jare=C5=A1?= Date: Thu, 17 Oct 2024 13:14:57 +0200 Subject: [PATCH 08/20] Update package --- Directory.Packages.props | 24 ++++++++++--------- NuGet.config | 2 -- eng/Version.Details.xml | 4 ++++ eng/Versions.props | 4 ++++ src/Cli/dotnet/Properties/launchSettings.json | 11 ++------- 5 files changed, 23 insertions(+), 22 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 802a73ebbd0c..9c9f79ec1e95 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,9 +1,10 @@ - + $(NoWarn);NU1507 + @@ -11,7 +12,7 @@ - + @@ -42,7 +43,7 @@ - + @@ -62,9 +63,9 @@ - + @@ -95,11 +96,11 @@ - - - - - + + + + + @@ -120,8 +121,9 @@ - + + - - diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 2f8cc88d3902..a0afaf9d4764 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -228,6 +228,10 @@ https://github.com/microsoft/vstest bc9161306b23641b0364b8f93d546da4d48da1eb + + https://github.com/microsoft/testfx + bc9161306b23641b0364b8f93d546da4d48da1eb + https://github.com/microsoft/vstest diff --git a/eng/Versions.props b/eng/Versions.props index 86bea0acb32d..995f1b06a553 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -199,6 +199,10 @@ 17.12.0-release-24508-01 17.12.0-release-24508-01 + + + 1.5.0-preview.24516.3 + 10.0.0-preview.24508.1 diff --git a/src/Cli/dotnet/Properties/launchSettings.json b/src/Cli/dotnet/Properties/launchSettings.json index ccb2fad63201..8d0ebf35dc6f 100644 --- a/src/Cli/dotnet/Properties/launchSettings.json +++ b/src/Cli/dotnet/Properties/launchSettings.json @@ -1,14 +1,7 @@ { "profiles": { "dotnet": { - "commandName": "Project", - "commandLineArgs": "test -bl:S:\\t\\mstest184\\log.binlog", - "workingDirectory": "S:\\t\\mstest184", - "environmentVariables": { - "DOTNET_CLI_TESTINGPLATFORM_ENABLE": "1", - "DOTNET_CLI_VSTEST_TRACE": "1", - "DOTNET_ROOT": "S:\\p\\dotnet-sdk\\artifacts\\bin\\redist\\Debug\\dotnet", - } + "commandName": "Project" } } -} +} \ No newline at end of file From 2622d9852064fe9ba1e8cf906e348090e1835ecb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Jare=C5=A1?= Date: Thu, 17 Oct 2024 13:19:28 +0200 Subject: [PATCH 09/20] Revert style cop version --- eng/Versions.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eng/Versions.props b/eng/Versions.props index 995f1b06a553..d0dfa574a265 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -77,7 +77,7 @@ 2.0.3 13.0.3 4.8.6 - 1.2.0-beta.507 + 1.2.0-beta.435 4.0.5 2.0.0-beta4.24324.3 0.4.0-alpha.24324.3 From d3b3f9198343ded39d72ad780566800bde400ca6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Jare=C5=A1?= Date: Thu, 17 Oct 2024 14:14:38 +0200 Subject: [PATCH 10/20] Remove debugger utility --- src/Cli/dotnet/Class1.cs | 334 ------------------ .../dotnet-test/TestingPlatformCommand.cs | 5 - 2 files changed, 339 deletions(-) delete mode 100644 src/Cli/dotnet/Class1.cs diff --git a/src/Cli/dotnet/Class1.cs b/src/Cli/dotnet/Class1.cs deleted file mode 100644 index 3497a9c31b9b..000000000000 --- a/src/Cli/dotnet/Class1.cs +++ /dev/null @@ -1,334 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -#pragma warning disable CA1837 // Use 'Environment.ProcessId' -#pragma warning disable CA1416 // Validate platform compatibility - -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Globalization; -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using System.Runtime.InteropServices.ComTypes; - -namespace Microsoft.Testing.TestInfrastructure; - -public class DebuggerUtility -{ - public static bool AttachCurrentProcessToParentVSProcess(bool enableLog = false) => AttachVSToProcess(Process.GetCurrentProcess().Id, null, enableLog); - - public static bool AttachCurrentProcessToVSProcessPID(int vsProcessPid, bool enableLog = false) => AttachVSToProcess(Process.GetCurrentProcess().Id, vsProcessPid, enableLog); - - private static bool AttachVSToProcess(int? pid, int? vsPid, bool enableLog = false) - { - try - { - if (pid == null) - { - Trace($"FAIL: Pid is null.", enabled: enableLog); - return false; - } - - var process = Process.GetProcessById(pid.Value); - Trace($"Starting with pid '{pid}({process.ProcessName})', and vsPid '{vsPid}'", enabled: enableLog); - Trace($"Using pid: {pid} to get parent VS.", enabled: enableLog); - Process vs = GetVsFromPid(Process.GetProcessById(vsPid ?? process.Id)); - - if (vs != null) - { - Trace($"Parent VS is {vs.ProcessName} ({vs.Id}).", enabled: enableLog); - AttachTo(process, vs); - return true; - } - - Trace($"Parent VS not found, finding the first VS that started.", enabled: enableLog); - var firstVs = Process.GetProcesses() - .Where(p => p.ProcessName == "devenv") - .Select(p => - { - try - { - return new { Process = p, p.StartTime, p.HasExited }; - } - catch - { - return null; - } - }) - .Where(p => p != null && !p.HasExited) - .OrderBy(p => p!.StartTime) - .FirstOrDefault(); - - if (firstVs != null) - { - Trace($"Found VS {firstVs.Process.Id}", enabled: enableLog); - AttachTo(process, firstVs.Process); - return true; - } - - Trace("Could not find any started VS.", enabled: enableLog); - } - catch (Exception ex) - { - Trace($"ERROR: {ex}, {ex.StackTrace}", enabled: enableLog); - } - - return false; - } - - private static void AttachTo(Process process, Process vs, bool enableLog = false) - { - bool attached = AttachVs(vs, process.Id); - if (attached) - { - // You won't see this in DebugView++ because at this point VS is already attached and all the output goes into Debug window in VS. - Trace($"SUCCESS: Attached process: {process.ProcessName} ({process.Id})", enabled: enableLog); - } - else - { - Trace($"FAIL: Could not attach process: {process.ProcessName} ({process.Id})", enabled: enableLog); - } - } - - private static bool AttachVs(Process vs, int pid, bool enableLog = false) - { - IBindCtx bindCtx = null; - IRunningObjectTable runningObjectTable = null; - IEnumMoniker enumMoniker = null; - try - { -#pragma warning disable IL2050 - int r = CreateBindCtx(0, out bindCtx); -#pragma warning restore IL2050 - Marshal.ThrowExceptionForHR(r); - if (bindCtx == null) - { - Trace($"BindCtx is null. Cannot attach VS.", enabled: enableLog); - return false; - } - - bindCtx.GetRunningObjectTable(out runningObjectTable); - if (runningObjectTable == null) - { - Trace($"RunningObjectTable is null. Cannot attach VS.", enabled: enableLog); - return false; - } - - runningObjectTable.EnumRunning(out enumMoniker); - if (enumMoniker == null) - { - Trace($"EnumMoniker is null. Cannot attach VS.", enabled: enableLog); - return false; - } - - string dteSuffix = ":" + vs.Id; - - var moniker = new IMoniker[1]; - while (enumMoniker.Next(1, moniker, IntPtr.Zero) == 0 && moniker[0] != null) - { - moniker[0].GetDisplayName(bindCtx, null, out string dn); - - if (dn.StartsWith("!VisualStudio.DTE.", StringComparison.Ordinal) && dn.EndsWith(dteSuffix, StringComparison.Ordinal)) - { - object dbg, lps; - runningObjectTable.GetObject(moniker[0], out object dte); - - // The COM object can be busy, we retry few times, hoping that it won't be busy next time. - for (int i = 0; i < 10; i++) - { - try - { - dbg = dte.GetType().InvokeMember("Debugger", BindingFlags.GetProperty, null, dte, null, CultureInfo.InvariantCulture)!; - lps = dbg.GetType().InvokeMember("LocalProcesses", BindingFlags.GetProperty, null, dbg, null, CultureInfo.InvariantCulture)!; - var lpn = (System.Collections.IEnumerator)lps.GetType().InvokeMember("GetEnumerator", BindingFlags.InvokeMethod, null, lps, null, CultureInfo.InvariantCulture)!; - - while (lpn.MoveNext()) - { - int pn = Convert.ToInt32(lpn.Current.GetType().InvokeMember("ProcessID", BindingFlags.GetProperty, null, lpn.Current, null, CultureInfo.InvariantCulture), CultureInfo.InvariantCulture); - - if (pn == pid) - { - lpn.Current.GetType().InvokeMember("Attach", BindingFlags.InvokeMethod, null, lpn.Current, null, CultureInfo.InvariantCulture); - return true; - } - } - } - - // Catch the exception if it is COMException coming directly, or coming from methodInvocation, otherwise just let it be. - catch (Exception ex) when (ex is COMException or TargetInvocationException { InnerException: COMException }) - { - Trace($"ComException: Retrying in 250ms.\n{ex}", enabled: enableLog); - Thread.Sleep(250); - } - } - - Marshal.ReleaseComObject(moniker[0]); - - break; - } - - Marshal.ReleaseComObject(moniker[0]); - } - - return false; - } - finally - { - if (enumMoniker != null) - { - try - { - Marshal.ReleaseComObject(enumMoniker); - } - catch - { - } - } - - if (runningObjectTable != null) - { - try - { - Marshal.ReleaseComObject(runningObjectTable); - } - catch - { - } - } - - if (bindCtx != null) - { - try - { - Marshal.ReleaseComObject(bindCtx); - } - catch - { - } - } - } - } - - private static Process GetVsFromPid(Process process) - { - Process parent = process; - while (!IsVsOrNull(parent)) - { - parent = GetParentProcess(parent); - } - - return parent; - } - - private static bool IsVsOrNull([NotNullWhen(false)] Process process, bool enableLog = false) - { - if (process == null) - { - Trace("Parent process is null..", enabled: enableLog); - return true; - } - - bool isVs = process.ProcessName.Equals("devenv", StringComparison.OrdinalIgnoreCase); - if (isVs) - { - Trace($"Process {process.ProcessName} ({process.Id}) is VS.", enabled: enableLog); - } - else - { - Trace($"Process {process.ProcessName} ({process.Id}) is not VS.", enabled: enableLog); - } - - return isVs; - } - - private static bool IsCorrectParent(Process currentProcess, Process parent, bool enableLog = false) - { - try - { - // Parent needs to start before the child, otherwise it might be a different process - // that is just reusing the same PID. - if (parent.StartTime <= currentProcess.StartTime) - { - return true; - } - - Trace($"Process {parent.ProcessName} ({parent.Id}) is not a valid parent because it started after the current process.", enabled: enableLog); - } - catch - { - // Access denied or process exited while we were holding the Process object. - } - - return false; - } - - private static Process GetParentProcess(Process process) - { - int id = GetParentProcessId(process); - if (id != -1) - { - try - { - var parent = Process.GetProcessById(id); - if (IsCorrectParent(process, parent)) - { - return parent; - } - } - catch - { - // throws when parent no longer runs - } - } - - return null; - - static int GetParentProcessId(Process process) - { - try - { - IntPtr handle = process.Handle; - int res = NtQueryInformationProcess(handle, 0, out PROCESS_BASIC_INFORMATION pbi, Marshal.SizeOf(), out int size); - - int p = res != 0 ? -1 : pbi.InheritedFromUniqueProcessId.ToInt32(); - - return p; - } - catch - { - return -1; - } - } - } - - private static void Trace(string message, [CallerMemberName] string methodName = null, bool enabled = false) - { - if (enabled) - { - Console.WriteLine($"[AttachVS]{methodName}: {message}"); - } - } - - [StructLayout(LayoutKind.Sequential)] - private struct PROCESS_BASIC_INFORMATION - { - public readonly IntPtr ExitStatus; - public readonly IntPtr PebBaseAddress; - public readonly IntPtr AffinityMask; - public readonly IntPtr BasePriority; - public readonly IntPtr UniqueProcessId; - public IntPtr InheritedFromUniqueProcessId; - } - - [DllImport("ntdll.dll", SetLastError = true)] - private static extern int NtQueryInformationProcess( - IntPtr processHandle, - int processInformationClass, - out PROCESS_BASIC_INFORMATION processInformation, - int processInformationLength, - out int returnLength); - - [DllImport("ole32.dll")] - private static extern int CreateBindCtx(uint reserved, out IBindCtx ppbc); -} diff --git a/src/Cli/dotnet/commands/dotnet-test/TestingPlatformCommand.cs b/src/Cli/dotnet/commands/dotnet-test/TestingPlatformCommand.cs index 0a4ce0a9c902..987b846a66d8 100644 --- a/src/Cli/dotnet/commands/dotnet-test/TestingPlatformCommand.cs +++ b/src/Cli/dotnet/commands/dotnet-test/TestingPlatformCommand.cs @@ -41,11 +41,6 @@ public int Run(ParseResult parseResult) CompleteRun(); }; - if (Environment.GetEnvironmentVariable("Debug") == "1") - { - DebuggerUtility.AttachCurrentProcessToParentVSProcess(); - } - if (parseResult.HasOption(TestingPlatformOptions.ArchitectureOption)) { VSTestTrace.SafeWriteTrace(() => $"The --arch option is not yet supported."); From 890337ed376c23568248206da979adf8f93a89b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Jare=C5=A1?= Date: Tue, 22 Oct 2024 13:30:12 +0200 Subject: [PATCH 11/20] Fixes from review --- .../commands/dotnet-test/TestingPlatformCommand.cs | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/src/Cli/dotnet/commands/dotnet-test/TestingPlatformCommand.cs b/src/Cli/dotnet/commands/dotnet-test/TestingPlatformCommand.cs index 987b846a66d8..f850720936d4 100644 --- a/src/Cli/dotnet/commands/dotnet-test/TestingPlatformCommand.cs +++ b/src/Cli/dotnet/commands/dotnet-test/TestingPlatformCommand.cs @@ -3,14 +3,11 @@ using System.Collections.Concurrent; using System.CommandLine; -using Microsoft.DotNet.Cli.Utils; using Microsoft.DotNet.Tools.Test; using Microsoft.TemplateEngine.Cli.Commands; using Microsoft.Testing.Platform.Helpers; -using Microsoft.Testing.Platform.IPC; using Microsoft.Testing.Platform.OutputDevice; using Microsoft.Testing.Platform.OutputDevice.Terminal; -using Microsoft.Testing.TestInfrastructure; namespace Microsoft.DotNet.Cli { @@ -41,12 +38,6 @@ public int Run(ParseResult parseResult) CompleteRun(); }; - if (parseResult.HasOption(TestingPlatformOptions.ArchitectureOption)) - { - VSTestTrace.SafeWriteTrace(() => $"The --arch option is not yet supported."); - return ExitCodes.GenericFailure; - } - // User can decide what the degree of parallelism should be // If not specified, we will default to the number of processors if (!int.TryParse(parseResult.GetValue(TestingPlatformOptions.MaxParallelTestModulesOption), out int degreeOfParallelism)) @@ -57,6 +48,7 @@ public int Run(ParseResult parseResult) if (filterModeEnabled && parseResult.HasOption(TestingPlatformOptions.ArchitectureOption)) { VSTestTrace.SafeWriteTrace(() => $"The --arch option is not supported yet."); + return ExitCodes.GenericFailure; } BuiltInOptions builtInOptions = new( @@ -209,7 +201,6 @@ private void OnTestResultsReceived(object sender, TestResultEventArgs args) { foreach (var testResult in args.SuccessfulTestResults) { - var testApp = (TestApplication)sender; var appInfo = _executions[testApp]; // TODO: timespan for duration @@ -227,7 +218,6 @@ private void OnTestResultsReceived(object sender, TestResultEventArgs args) foreach (var testResult in args.FailedTestResults) { - var testApp = (TestApplication)sender; // TODO: timespan for duration // TODO: expected @@ -245,7 +235,6 @@ private void OnTestResultsReceived(object sender, TestResultEventArgs args) errorOutput: null); } - if (!VSTestTrace.TraceEnabled) { return; From 768d5dabb39f2fa5932e17d68500c66ca8bc52a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Jare=C5=A1?= Date: Tue, 12 Nov 2024 16:26:11 +0100 Subject: [PATCH 12/20] Fix ipc --- NuGet.config | 1 + eng/Versions.props | 2 +- .../commands/dotnet-test/BuiltInOptions.cs | 2 +- .../IPC/Models/TestResultMessages.cs | 4 +- .../dotnet-test/IPC/ObjectFieldIds.cs | 10 ++- .../TestResultMessagesSerializer.cs | 89 ++++++++++++++++--- src/Cli/dotnet/commands/dotnet-test/Models.cs | 4 +- .../commands/dotnet-test/TestApplication.cs | 7 +- .../commands/dotnet-test/TestCommandParser.cs | 1 + .../dotnet-test/TestingPlatformCommand.cs | 59 ++++++++---- .../dotnet-test/TestingPlatformOptions.cs | 6 ++ 11 files changed, 150 insertions(+), 35 deletions(-) diff --git a/NuGet.config b/NuGet.config index ecf2c176d5ad..8cb6c0138d0f 100644 --- a/NuGet.config +++ b/NuGet.config @@ -8,6 +8,7 @@ + diff --git a/eng/Versions.props b/eng/Versions.props index 37e3fcb44c12..48e62e876da2 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -184,7 +184,7 @@ - 1.5.0-preview.24516.3 + 1.7.0-dev diff --git a/src/Cli/dotnet/commands/dotnet-test/BuiltInOptions.cs b/src/Cli/dotnet/commands/dotnet-test/BuiltInOptions.cs index f3bc4c1419c5..0462c6c7486d 100644 --- a/src/Cli/dotnet/commands/dotnet-test/BuiltInOptions.cs +++ b/src/Cli/dotnet/commands/dotnet-test/BuiltInOptions.cs @@ -3,5 +3,5 @@ namespace Microsoft.DotNet.Cli { - internal record BuiltInOptions(bool HasNoRestore, bool HasNoBuild, string Configuration, string Architecture); + internal record BuiltInOptions(bool HasNoRestore, bool HasNoBuild, bool HasListTests, string Configuration, string Architecture); } diff --git a/src/Cli/dotnet/commands/dotnet-test/IPC/Models/TestResultMessages.cs b/src/Cli/dotnet/commands/dotnet-test/IPC/Models/TestResultMessages.cs index e4d8d7ff1458..2b0aa7422a50 100644 --- a/src/Cli/dotnet/commands/dotnet-test/IPC/Models/TestResultMessages.cs +++ b/src/Cli/dotnet/commands/dotnet-test/IPC/Models/TestResultMessages.cs @@ -7,7 +7,9 @@ namespace Microsoft.DotNet.Tools.Test { internal sealed record SuccessfulTestResultMessage(string? Uid, string? DisplayName, byte? State, long? Duration, string? Reason, string? StandardOutput, string? ErrorOutput, string? SessionUid); - internal sealed record FailedTestResultMessage(string? Uid, string? DisplayName, byte? State, long? Duration, string? Reason, string? ErrorMessage, string? ErrorStackTrace, string? StandardOutput, string? ErrorOutput, string? SessionUid); + internal sealed record FailedTestResultMessage(string? Uid, string? DisplayName, byte? State, long? Duration, string? Reason, ExceptionMessage[]? Exceptions, string? StandardOutput, string? ErrorOutput, string? SessionUid); + + internal sealed record ExceptionMessage(string? ErrorMessage, string? ErrorType, string? StackTrace); internal sealed record TestResultMessages(string? ExecutionId, SuccessfulTestResultMessage[] SuccessfulTestMessages, FailedTestResultMessage[] FailedTestMessages) : IRequest; } diff --git a/src/Cli/dotnet/commands/dotnet-test/IPC/ObjectFieldIds.cs b/src/Cli/dotnet/commands/dotnet-test/IPC/ObjectFieldIds.cs index e7f720c96fbb..01d38f3d9ec2 100644 --- a/src/Cli/dotnet/commands/dotnet-test/IPC/ObjectFieldIds.cs +++ b/src/Cli/dotnet/commands/dotnet-test/IPC/ObjectFieldIds.cs @@ -85,13 +85,19 @@ internal static class FailedTestResultMessageFieldsId public const ushort State = 3; public const ushort Duration = 4; public const ushort Reason = 5; - public const ushort ErrorMessage = 6; - public const ushort ErrorStackTrace = 7; + public const ushort ExceptionMessageList = 6; public const ushort StandardOutput = 8; public const ushort ErrorOutput = 9; public const ushort SessionUid = 10; } + internal static class ExceptionMessageFieldsId + { + public const ushort ErrorMessage = 1; + public const ushort ErrorType = 2; + public const ushort StackTrace = 3; + } + internal static class FileArtifactMessagesFieldsId { public const int MessagesSerializerId = 7; diff --git a/src/Cli/dotnet/commands/dotnet-test/IPC/Serializers/TestResultMessagesSerializer.cs b/src/Cli/dotnet/commands/dotnet-test/IPC/Serializers/TestResultMessagesSerializer.cs index aebd6e86fe04..9577e81d2bb1 100644 --- a/src/Cli/dotnet/commands/dotnet-test/IPC/Serializers/TestResultMessagesSerializer.cs +++ b/src/Cli/dotnet/commands/dotnet-test/IPC/Serializers/TestResultMessagesSerializer.cs @@ -218,8 +218,8 @@ private static List ReadFailedTestMessagesPayload(Strea int length = ReadInt(stream); for (int i = 0; i < length; i++) { - string? uid = null, displayName = null, reason = null, sessionUid = null, - errorMessage = null, errorStackTrace = null, standardOutput = null, errorOutput = null; + string? uid = null, displayName = null, reason = null, sessionUid = null, standardOutput = null, errorOutput = null; + List exceptionMessages = []; byte? state = null; long? duration = null; @@ -252,13 +252,44 @@ private static List ReadFailedTestMessagesPayload(Strea reason = ReadStringValue(stream, fieldSize); break; - case FailedTestResultMessageFieldsId.ErrorMessage: - errorMessage = ReadStringValue(stream, fieldSize); - break; + case FailedTestResultMessageFieldsId.ExceptionMessageList: + { + int length2 = ReadInt(stream); + for (int k = 0; k < length2; k++) + { - case FailedTestResultMessageFieldsId.ErrorStackTrace: - errorStackTrace = ReadStringValue(stream, fieldSize); - break; + int fieldCount2 = ReadShort(stream); + + string? errorMessage = null; + string? errorType = null; + string? stackTrace = null; + + for (int l = 0; l < fieldCount2; l++) + { + int fieldId2 = ReadShort(stream); + int fieldSize2 = ReadInt(stream); + + switch (fieldId2) + { + case ExceptionMessageFieldsId.ErrorMessage: + errorMessage = ReadStringValue(stream, fieldSize2); + break; + + case ExceptionMessageFieldsId.ErrorType: + errorType = ReadStringValue(stream, fieldSize2); + break; + + case ExceptionMessageFieldsId.StackTrace: + stackTrace = ReadStringValue(stream, fieldSize2); + break; + } + } + + exceptionMessages.Add(new ExceptionMessage(errorMessage, errorType, stackTrace)); + } + + break; + } case FailedTestResultMessageFieldsId.StandardOutput: standardOutput = ReadStringValue(stream, fieldSize); @@ -278,7 +309,7 @@ private static List ReadFailedTestMessagesPayload(Strea } } - failedTestResultMessages.Add(new FailedTestResultMessage(uid, displayName, state, duration, reason, errorMessage, errorStackTrace, standardOutput, errorOutput, sessionUid)); + failedTestResultMessages.Add(new FailedTestResultMessage(uid, displayName, state, duration, reason, exceptionMessages.ToArray(), standardOutput, errorOutput, sessionUid)); } return failedTestResultMessages; @@ -355,8 +386,7 @@ private static void WriteFailedTestMessagesPayload(Stream stream, FailedTestResu WriteField(stream, FailedTestResultMessageFieldsId.State, failedTestResultMessage.State); WriteField(stream, FailedTestResultMessageFieldsId.Duration, failedTestResultMessage.Duration); WriteField(stream, FailedTestResultMessageFieldsId.Reason, failedTestResultMessage.Reason); - WriteField(stream, FailedTestResultMessageFieldsId.ErrorMessage, failedTestResultMessage.ErrorMessage); - WriteField(stream, FailedTestResultMessageFieldsId.ErrorStackTrace, failedTestResultMessage.ErrorStackTrace); + WriteExceptionMessagesPayload(stream, failedTestResultMessage.Exceptions); WriteField(stream, FailedTestResultMessageFieldsId.StandardOutput, failedTestResultMessage.StandardOutput); WriteField(stream, FailedTestResultMessageFieldsId.ErrorOutput, failedTestResultMessage.ErrorOutput); WriteField(stream, FailedTestResultMessageFieldsId.SessionUid, failedTestResultMessage.SessionUid); @@ -367,6 +397,35 @@ private static void WriteFailedTestMessagesPayload(Stream stream, FailedTestResu WriteAtPosition(stream, (int)(stream.Position - before), before - sizeof(int)); } + private static void WriteExceptionMessagesPayload(Stream stream, ExceptionMessage[]? exceptionMessages) + { + if (exceptionMessages is null || exceptionMessages.Length == 0) + { + return; + } + + WriteShort(stream, FailedTestResultMessageFieldsId.ExceptionMessageList); + + // We will reserve an int (4 bytes) + // so that we fill the size later, once we write the payload + WriteInt(stream, 0); + + long before = stream.Position; + WriteInt(stream, exceptionMessages.Length); + foreach (ExceptionMessage exceptionMessage in exceptionMessages) + { + WriteShort(stream, GetFieldCount(exceptionMessage)); + + WriteField(stream, ExceptionMessageFieldsId.ErrorMessage, exceptionMessage.ErrorMessage); + WriteField(stream, ExceptionMessageFieldsId.ErrorType, exceptionMessage.ErrorType); + WriteField(stream, ExceptionMessageFieldsId.StackTrace, exceptionMessage.StackTrace); + } + + // NOTE: We are able to seek only if we are using a MemoryStream + // thus, the seek operation is fast as we are only changing the value of a property + WriteAtPosition(stream, (int)(stream.Position - before), before - sizeof(int)); + } + private static ushort GetFieldCount(TestResultMessages testResultMessages) => (ushort)((testResultMessages.ExecutionId is null ? 0 : 1) + (IsNullOrEmpty(testResultMessages.SuccessfulTestMessages) ? 0 : 1) + @@ -388,10 +447,14 @@ private static ushort GetFieldCount(FailedTestResultMessage failedTestResultMess (failedTestResultMessage.State is null ? 0 : 1) + (failedTestResultMessage.Duration is null ? 0 : 1) + (failedTestResultMessage.Reason is null ? 0 : 1) + - (failedTestResultMessage.ErrorMessage is null ? 0 : 1) + - (failedTestResultMessage.ErrorStackTrace is null ? 0 : 1) + + (IsNullOrEmpty(failedTestResultMessage.Exceptions) ? 0 : 1) + (failedTestResultMessage.StandardOutput is null ? 0 : 1) + (failedTestResultMessage.ErrorOutput is null ? 0 : 1) + (failedTestResultMessage.SessionUid is null ? 0 : 1)); + + private static ushort GetFieldCount(ExceptionMessage exceptionMessage) => + (ushort)((exceptionMessage.ErrorMessage is null ? 0 : 1) + + (exceptionMessage.ErrorType is null ? 0 : 1) + + (exceptionMessage.StackTrace is null ? 0 : 1)); } } diff --git a/src/Cli/dotnet/commands/dotnet-test/Models.cs b/src/Cli/dotnet/commands/dotnet-test/Models.cs index 4f4edf811e6d..e9bd91e79482 100644 --- a/src/Cli/dotnet/commands/dotnet-test/Models.cs +++ b/src/Cli/dotnet/commands/dotnet-test/Models.cs @@ -15,7 +15,9 @@ internal sealed record DiscoveredTest(string? Uid, string? DisplayName); internal sealed record SuccessfulTestResult(string? Uid, string? DisplayName, byte? State, long? Duration, string? Reason, string? StandardOutput, string? ErrorOutput, string? SessionUid); - internal sealed record FailedTestResult(string? Uid, string? DisplayName, byte? State, long? Duration, string? Reason, string? ErrorMessage, string? ErrorStackTrace, string? StandardOutput, string? ErrorOutput, string? SessionUid); + internal sealed record FailedTestResult(string? Uid, string? DisplayName, byte? State, long? Duration, string? Reason, FlatException[]? Exceptions, string? StandardOutput, string? ErrorOutput, string? SessionUid); + + internal sealed record FlatException(string? ErrorMessage, string? ErrorType, string? StackTrace); internal sealed record FileArtifact(string? FullPath, string? DisplayName, string? Description, string? TestUid, string? TestDisplayName, string? SessionUid); diff --git a/src/Cli/dotnet/commands/dotnet-test/TestApplication.cs b/src/Cli/dotnet/commands/dotnet-test/TestApplication.cs index 0ad9e9e038af..59fadf17ee04 100644 --- a/src/Cli/dotnet/commands/dotnet-test/TestApplication.cs +++ b/src/Cli/dotnet/commands/dotnet-test/TestApplication.cs @@ -248,6 +248,11 @@ private string BuildArgsWithDotnetRun(BuiltInOptions builtInOptions) builder.Append($" {TestingPlatformOptions.NoBuildOption.Name}"); } + if (builtInOptions.HasListTests) + { + builder.Append($" {TestingPlatformOptions.ListTestsOption.Name}"); + } + if (!string.IsNullOrEmpty(builtInOptions.Architecture)) { builder.Append($" {TestingPlatformOptions.ArchitectureOption.Name} {builtInOptions.Architecture}"); @@ -336,7 +341,7 @@ internal void OnTestResultMessages(TestResultMessages testResultMessage) { ExecutionId = testResultMessage.ExecutionId, SuccessfulTestResults = testResultMessage.SuccessfulTestMessages.Select(message => new SuccessfulTestResult(message.Uid, message.DisplayName, message.State, message.Duration, message.Reason, message.StandardOutput, message.ErrorOutput, message.SessionUid)).ToArray(), - FailedTestResults = testResultMessage.FailedTestMessages.Select(message => new FailedTestResult(message.Uid, message.DisplayName, message.State, message.Duration, message.Reason, message.ErrorMessage, message.ErrorStackTrace, message.StandardOutput, message.ErrorOutput, message.SessionUid)).ToArray() + FailedTestResults = testResultMessage.FailedTestMessages.Select(message => new FailedTestResult(message.Uid, message.DisplayName, message.State, message.Duration, message.Reason, message.Exceptions.Select(e => new FlatException(e.ErrorMessage, e.ErrorType, e.StackTrace)).ToArray(), message.StandardOutput, message.ErrorOutput, message.SessionUid)).ToArray() }); } diff --git a/src/Cli/dotnet/commands/dotnet-test/TestCommandParser.cs b/src/Cli/dotnet/commands/dotnet-test/TestCommandParser.cs index 5196981af9ae..c308cd2185fe 100644 --- a/src/Cli/dotnet/commands/dotnet-test/TestCommandParser.cs +++ b/src/Cli/dotnet/commands/dotnet-test/TestCommandParser.cs @@ -200,6 +200,7 @@ private static CliCommand GetTestingPlatformCliCommand() command.Options.Add(TestingPlatformOptions.ArchitectureOption); command.Options.Add(TestingPlatformOptions.ConfigurationOption); command.Options.Add(TestingPlatformOptions.ProjectOption); + command.Options.Add(TestingPlatformOptions.ListTestsOption); return command; } diff --git a/src/Cli/dotnet/commands/dotnet-test/TestingPlatformCommand.cs b/src/Cli/dotnet/commands/dotnet-test/TestingPlatformCommand.cs index 529d7a2692f3..fd2173c1008e 100644 --- a/src/Cli/dotnet/commands/dotnet-test/TestingPlatformCommand.cs +++ b/src/Cli/dotnet/commands/dotnet-test/TestingPlatformCommand.cs @@ -5,6 +5,7 @@ using System.CommandLine; using Microsoft.DotNet.Tools.Test; using Microsoft.TemplateEngine.Cli.Commands; +using Microsoft.Testing.Platform.Builder; using Microsoft.Testing.Platform.Helpers; using Microsoft.Testing.Platform.OutputDevice; using Microsoft.Testing.Platform.OutputDevice.Terminal; @@ -24,6 +25,7 @@ internal partial class TestingPlatformCommand : CliCommand, ICustomHelp private List _args; private Dictionary _executions = new(); private byte _cancelled; + private bool _isDiscovery; public TestingPlatformCommand(string name, string description = null) : base(name, description) { @@ -51,23 +53,29 @@ public int Run(ParseResult parseResult) return ExitCodes.GenericFailure; } + if (parseResult.HasOption(TestingPlatformOptions.ListTestsOption)) + { + _isDiscovery = true; + } + BuiltInOptions builtInOptions = new( parseResult.HasOption(TestingPlatformOptions.NoRestoreOption), parseResult.HasOption(TestingPlatformOptions.NoBuildOption), + parseResult.HasOption(TestingPlatformOptions.ListTestsOption), parseResult.GetValue(TestingPlatformOptions.ConfigurationOption), parseResult.GetValue(TestingPlatformOptions.ArchitectureOption)); var console = new SystemConsole(); var output = new TerminalTestReporter(console, new TerminalTestReporterOptions() { - ShowPassedTests = Environment.GetEnvironmentVariable("SHOW_PASSED") == "1", + ShowPassedTests = Environment.GetEnvironmentVariable("SHOW_PASSED") == "1" ? () => true : () => false, ShowProgress = () => Environment.GetEnvironmentVariable("NO_PROGRESS") != "1", UseAnsi = Environment.GetEnvironmentVariable("NO_ANSI") != "1", ShowAssembly = true, ShowAssemblyStartAndComplete = true, }); _output = output; - _output.TestExecutionStarted(DateTimeOffset.Now, degreeOfParallelism); + _output.TestExecutionStarted(DateTimeOffset.Now, degreeOfParallelism, _isDiscovery); if (ContainsHelpOption(parseResult.GetArguments())) { @@ -183,13 +191,23 @@ private void OnHandshakeReceived(object sender, HandshakeArgs args) private void OnDiscoveredTestsReceived(object sender, DiscoveredTestEventArgs args) { + var testApp = (TestApplication)sender; + var appInfo = _executions[testApp]; + _executions[testApp] = appInfo; + + foreach (var test in args.DiscoveredTests) + { + _output.TestDiscovered(appInfo.ModulePath, appInfo.TargetFramework, appInfo.Architecture, appInfo.ExecutionId, + test.DisplayName, + test.Uid); + } + if (!VSTestTrace.TraceEnabled) { return; } var discoveredTestMessages = args.DiscoveredTests; - VSTestTrace.SafeWriteTrace(() => $"DiscoveredTests Execution Id: {args.ExecutionId}"); foreach (DiscoveredTest discoveredTestMessage in discoveredTestMessages) { @@ -203,13 +221,11 @@ private void OnTestResultsReceived(object sender, TestResultEventArgs args) { var testApp = (TestApplication)sender; var appInfo = _executions[testApp]; - // TODO: timespan for duration _output.TestCompleted(appInfo.ModulePath, appInfo.TargetFramework, appInfo.Architecture, appInfo.ExecutionId, testResult.DisplayName, ToOutcome(testResult.State), - TimeSpan.FromSeconds(1), - errorMessage: null, - errorStackTrace: null, + TimeSpan.FromTicks(testResult.Duration ?? 0), + exceptions: null, expected: null, actual: null, standardOutput: null, @@ -219,16 +235,14 @@ private void OnTestResultsReceived(object sender, TestResultEventArgs args) foreach (var testResult in args.FailedTestResults) { var testApp = (TestApplication)sender; - // TODO: timespan for duration // TODO: expected // TODO: actual var appInfo = _executions[testApp]; _output.TestCompleted(appInfo.ModulePath, appInfo.TargetFramework, appInfo.Architecture, appInfo.ExecutionId, testResult.DisplayName, ToOutcome(testResult.State), - TimeSpan.FromSeconds(1), - errorMessage: testResult.ErrorMessage, - errorStackTrace: testResult.ErrorStackTrace, + TimeSpan.FromTicks(testResult.Duration ?? 0), + exceptions: testResult.Exceptions.Select(fe => new Microsoft.Testing.Platform.OutputDevice.Terminal.FlatException(fe.ErrorMessage, fe.ErrorType, fe.StackTrace)).ToArray(), expected: null, actual: null, standardOutput: null, @@ -252,8 +266,8 @@ private void OnTestResultsReceived(object sender, TestResultEventArgs args) foreach (FailedTestResult failedTestResult in args.FailedTestResults) { VSTestTrace.SafeWriteTrace(() => $"FailedTestResult: {failedTestResult.Uid}, {failedTestResult.DisplayName}, " + - $"{failedTestResult.State}, {failedTestResult.Duration}, {failedTestResult.Reason}, {failedTestResult.ErrorMessage}," + - $"{failedTestResult.ErrorStackTrace}, {failedTestResult.StandardOutput}, {failedTestResult.ErrorOutput}, {failedTestResult.SessionUid}"); + $"{failedTestResult.State}, {failedTestResult.Duration}, {failedTestResult.Reason}, {string.Join(", ", failedTestResult.Exceptions?.Select(e => $"{e.ErrorMessage}, {e.ErrorType}, {e.StackTrace}"))}" + + $"{failedTestResult.StandardOutput}, {failedTestResult.ErrorOutput}, {failedTestResult.SessionUid}"); } } @@ -273,6 +287,17 @@ public static TestOutcome ToOutcome(byte? testState) private void OnFileArtifactsReceived(object sender, FileArtifactEventArgs args) { + var testApp = (TestApplication)sender; + var appInfo = _executions[testApp]; + + foreach (var artifact in args.FileArtifacts) { + // TODO: Is artifact out of process + _output.ArtifactAdded( + outOfProcess: false, + appInfo.ModulePath, appInfo.TargetFramework, appInfo.Architecture, appInfo.ExecutionId, + artifact.TestDisplayName, artifact.FullPath); + } + if (!VSTestTrace.TraceEnabled) { return; @@ -313,8 +338,12 @@ private void OnTestProcessExited(object sender, TestProcessExitEventArgs args) { var testApplication = (TestApplication)sender; - var appInfo = _executions[testApplication]; - _output.AssemblyRunCompleted(appInfo.ModulePath, appInfo.TargetFramework, appInfo.Architecture, appInfo.ExecutionId); + // If the application exits too early we might not start the execution, + // e.g. if the parameter is incorrect. + if (_executions.TryGetValue(testApplication, out var appInfo)) + { + _output.AssemblyRunCompleted(appInfo.ModulePath, appInfo.TargetFramework, appInfo.Architecture, appInfo.ExecutionId, args.ExitCode, string.Join(Environment.NewLine, args.OutputData), string.Join(Environment.NewLine, args.ErrorData)); + } if (!VSTestTrace.TraceEnabled) { diff --git a/src/Cli/dotnet/commands/dotnet-test/TestingPlatformOptions.cs b/src/Cli/dotnet/commands/dotnet-test/TestingPlatformOptions.cs index f894c4cab66f..cd0a128146ce 100644 --- a/src/Cli/dotnet/commands/dotnet-test/TestingPlatformOptions.cs +++ b/src/Cli/dotnet/commands/dotnet-test/TestingPlatformOptions.cs @@ -57,5 +57,11 @@ internal static class TestingPlatformOptions Description = LocalizableStrings.CmdProjectDescription, Arity = ArgumentArity.ExactlyOne }; + + public static readonly CliOption ListTestsOption = new("--list-tests") + { + Description = LocalizableStrings.CmdListTestsDescription, + Arity = ArgumentArity.Zero + }; } } From ac326d938b240484479f225113e08c4c40b853f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Jare=C5=A1?= Date: Tue, 3 Dec 2024 17:11:37 +0100 Subject: [PATCH 13/20] remove local --- NuGet.config | 1 - 1 file changed, 1 deletion(-) diff --git a/NuGet.config b/NuGet.config index 8cb6c0138d0f..ecf2c176d5ad 100644 --- a/NuGet.config +++ b/NuGet.config @@ -8,7 +8,6 @@ - From c28a02cb95ba621c21262cdebc5c81342a0bbeb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Jare=C5=A1?= Date: Wed, 4 Dec 2024 10:13:53 +0100 Subject: [PATCH 14/20] Update eng/Version.Details.xml --- eng/Version.Details.xml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 6cb416984087..d3d6a41c6710 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -224,10 +224,6 @@ https://github.com/microsoft/vstest 7d34b30433259fb914aaaf276fde663a47b6ef2f - - https://github.com/microsoft/testfx - bc9161306b23641b0364b8f93d546da4d48da1eb - https://github.com/microsoft/vstest From a2e4d899cf6b3fce803aedd3c49dcbd3421349c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Jare=C5=A1?= Date: Tue, 10 Dec 2024 15:46:36 +0100 Subject: [PATCH 15/20] concurrent dict --- src/Cli/dotnet/commands/dotnet-test/TestingPlatformCommand.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Cli/dotnet/commands/dotnet-test/TestingPlatformCommand.cs b/src/Cli/dotnet/commands/dotnet-test/TestingPlatformCommand.cs index 828dbe77b627..96a2bb262afb 100644 --- a/src/Cli/dotnet/commands/dotnet-test/TestingPlatformCommand.cs +++ b/src/Cli/dotnet/commands/dotnet-test/TestingPlatformCommand.cs @@ -6,7 +6,6 @@ using System.CommandLine; using Microsoft.DotNet.Tools.Test; using Microsoft.TemplateEngine.Cli.Commands; -using Microsoft.Testing.Platform.Builder; using Microsoft.Testing.Platform.Helpers; using Microsoft.Testing.Platform.OutputDevice; using Microsoft.Testing.Platform.OutputDevice.Terminal; @@ -24,7 +23,7 @@ internal partial class TestingPlatformCommand : CliCommand, ICustomHelp private TestApplicationActionQueue _actionQueue; private Task _namedPipeConnectionLoop; private List _args; - private Dictionary _executions = new(); + private ConcurrentDictionary _executions = new(); private byte _cancelled; private bool _isDiscovery; @@ -212,7 +211,6 @@ private void OnDiscoveredTestsReceived(object sender, DiscoveredTestEventArgs ar { var testApp = (TestApplication)sender; var appInfo = _executions[testApp]; - _executions[testApp] = appInfo; foreach (var test in args.DiscoveredTests) { From 879effa66b93f3745834873b5072c509b5a89601 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Jare=C5=A1?= Date: Thu, 12 Dec 2024 15:18:53 +0100 Subject: [PATCH 16/20] Update eng/Versions.props --- eng/Versions.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eng/Versions.props b/eng/Versions.props index 7ebbf9ee813b..2b24069d2a01 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -179,7 +179,7 @@ - 1.5.0-preview.24602.2 + 1.5.0-preview.24604.7 From b6dea5c80cd3127152d3acbce73bc5754ad25de5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Jare=C5=A1?= Date: Wed, 8 Jan 2025 13:09:27 +0100 Subject: [PATCH 17/20] uuf --- Directory.Packages.props | 2 +- NuGet.config | 1 + src/Cli/dotnet/commands/dotnet-test/TestingPlatformCommand.cs | 2 ++ src/Cli/dotnet/dotnet.csproj | 2 +- 4 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 37f12c935a34..56cc3862dd6e 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -63,7 +63,7 @@ - + diff --git a/NuGet.config b/NuGet.config index ecf2c176d5ad..5963117ff7ee 100644 --- a/NuGet.config +++ b/NuGet.config @@ -8,6 +8,7 @@ + diff --git a/src/Cli/dotnet/commands/dotnet-test/TestingPlatformCommand.cs b/src/Cli/dotnet/commands/dotnet-test/TestingPlatformCommand.cs index 96a2bb262afb..026900ec1e12 100644 --- a/src/Cli/dotnet/commands/dotnet-test/TestingPlatformCommand.cs +++ b/src/Cli/dotnet/commands/dotnet-test/TestingPlatformCommand.cs @@ -239,6 +239,7 @@ private void OnTestResultsReceived(object sender, TestResultEventArgs args) var testApp = (TestApplication)sender; var appInfo = _executions[testApp]; _output.TestCompleted(appInfo.ModulePath, appInfo.TargetFramework, appInfo.Architecture, appInfo.ExecutionId, + testResult.Uid, testResult.DisplayName, ToOutcome(testResult.State), TimeSpan.FromTicks(testResult.Duration ?? 0), @@ -256,6 +257,7 @@ private void OnTestResultsReceived(object sender, TestResultEventArgs args) // TODO: actual var appInfo = _executions[testApp]; _output.TestCompleted(appInfo.ModulePath, appInfo.TargetFramework, appInfo.Architecture, appInfo.ExecutionId, + testResult.Uid, testResult.DisplayName, ToOutcome(testResult.State), TimeSpan.FromTicks(testResult.Duration ?? 0), diff --git a/src/Cli/dotnet/dotnet.csproj b/src/Cli/dotnet/dotnet.csproj index 3fecec01f539..f19ef88ebd53 100644 --- a/src/Cli/dotnet/dotnet.csproj +++ b/src/Cli/dotnet/dotnet.csproj @@ -100,7 +100,7 @@ - + From d25e5280928821e84f091be7a91302da0f5e36bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Jare=C5=A1?= Date: Thu, 9 Jan 2025 16:30:52 +0100 Subject: [PATCH 18/20] Use client package --- Directory.Packages.props | 4 ++-- eng/Version.Details.xml | 2 +- eng/Versions.props | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 56cc3862dd6e..93ff1f7a4954 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -63,8 +63,8 @@ - - + + diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 14492c27c40b..55eb95caeeb5 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -634,7 +634,7 @@ https://github.com/dotnet/runtime e77011b31a3e5c47d931248a64b47f9b2d47853d - + https://github.com/microsoft/testfx f7ddde6db81476dc031fb5c327abb2eef7f38969 diff --git a/eng/Versions.props b/eng/Versions.props index d550850f2213..52627b39f38a 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -162,7 +162,7 @@ - 1.5.0-preview.24604.7 + 1.6.0-preview.25059.11 From fbf87a220fd330b83b02e48c64e263483d3aa94c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Jare=C5=A1?= Date: Thu, 9 Jan 2025 18:35:39 +0100 Subject: [PATCH 19/20] Add terminal as code --- Directory.Packages.props | 2 - eng/Version.Details.xml | 4 - .../dotnet-test/LocalizableStrings.resx | 114 +- .../dotnet-test/Terminal/AnsiCodes.cs | 144 +++ .../dotnet-test/Terminal/AnsiDetector.cs | 41 + .../dotnet-test/Terminal/AnsiTerminal.cs | 311 +++++ .../Terminal/AnsiTerminalTestProgressFrame.cs | 352 ++++++ .../dotnet-test/Terminal/ErrorMessage.cs | 9 + .../Terminal/ExceptionFlattener.cs | 52 + .../dotnet-test/Terminal/FileUtilities.cs | 37 + .../HumanReadableDurationFormatter.cs | 112 ++ .../commands/dotnet-test/Terminal/IColor.cs | 9 + .../commands/dotnet-test/Terminal/IConsole.cs | 48 + .../dotnet-test/Terminal/IProgressMessage.cs | 9 + .../dotnet-test/Terminal/IStopwatch.cs | 13 + .../dotnet-test/Terminal/ITerminal.cs | 44 + .../dotnet-test/Terminal/NativeMethods.cs | 122 ++ .../dotnet-test/Terminal/NonAnsiTerminal.cs | 254 ++++ .../dotnet-test/Terminal/SystemConsole.cs | 212 ++++ .../Terminal/SystemConsoleColor.cs | 15 + .../dotnet-test/Terminal/SystemStopwatch.cs | 25 + .../Terminal/TargetFrameworkParser.cs | 74 ++ .../dotnet-test/Terminal/TerminalColor.cs | 95 ++ .../Terminal/TerminalTestReporter.cs | 1019 +++++++++++++++++ .../Terminal/TerminalTestReporterOptions.cs | 54 + .../dotnet-test/Terminal/TestDetailState.cs | 37 + .../Terminal/TestNodeResultsState.cs | 63 + .../dotnet-test/Terminal/TestOutcome.cs | 40 + .../dotnet-test/Terminal/TestProgressState.cs | 57 + .../TestProgressStateAwareTerminal.cs | 205 ++++ .../dotnet-test/Terminal/TestRunArtifact.cs | 9 + .../dotnet-test/Terminal/WarningMessage.cs | 9 + .../dotnet-test/xlf/LocalizableStrings.cs.xlf | 165 +++ .../dotnet-test/xlf/LocalizableStrings.de.xlf | 165 +++ .../dotnet-test/xlf/LocalizableStrings.es.xlf | 165 +++ .../dotnet-test/xlf/LocalizableStrings.fr.xlf | 165 +++ .../dotnet-test/xlf/LocalizableStrings.it.xlf | 165 +++ .../dotnet-test/xlf/LocalizableStrings.ja.xlf | 165 +++ .../dotnet-test/xlf/LocalizableStrings.ko.xlf | 165 +++ .../dotnet-test/xlf/LocalizableStrings.pl.xlf | 165 +++ .../xlf/LocalizableStrings.pt-BR.xlf | 165 +++ .../dotnet-test/xlf/LocalizableStrings.ru.xlf | 165 +++ .../dotnet-test/xlf/LocalizableStrings.tr.xlf | 165 +++ .../xlf/LocalizableStrings.zh-Hans.xlf | 165 +++ .../xlf/LocalizableStrings.zh-Hant.xlf | 165 +++ src/Cli/dotnet/dotnet.csproj | 1 - 46 files changed, 5727 insertions(+), 10 deletions(-) create mode 100644 src/Cli/dotnet/commands/dotnet-test/Terminal/AnsiCodes.cs create mode 100644 src/Cli/dotnet/commands/dotnet-test/Terminal/AnsiDetector.cs create mode 100644 src/Cli/dotnet/commands/dotnet-test/Terminal/AnsiTerminal.cs create mode 100644 src/Cli/dotnet/commands/dotnet-test/Terminal/AnsiTerminalTestProgressFrame.cs create mode 100644 src/Cli/dotnet/commands/dotnet-test/Terminal/ErrorMessage.cs create mode 100644 src/Cli/dotnet/commands/dotnet-test/Terminal/ExceptionFlattener.cs create mode 100644 src/Cli/dotnet/commands/dotnet-test/Terminal/FileUtilities.cs create mode 100644 src/Cli/dotnet/commands/dotnet-test/Terminal/HumanReadableDurationFormatter.cs create mode 100644 src/Cli/dotnet/commands/dotnet-test/Terminal/IColor.cs create mode 100644 src/Cli/dotnet/commands/dotnet-test/Terminal/IConsole.cs create mode 100644 src/Cli/dotnet/commands/dotnet-test/Terminal/IProgressMessage.cs create mode 100644 src/Cli/dotnet/commands/dotnet-test/Terminal/IStopwatch.cs create mode 100644 src/Cli/dotnet/commands/dotnet-test/Terminal/ITerminal.cs create mode 100644 src/Cli/dotnet/commands/dotnet-test/Terminal/NativeMethods.cs create mode 100644 src/Cli/dotnet/commands/dotnet-test/Terminal/NonAnsiTerminal.cs create mode 100644 src/Cli/dotnet/commands/dotnet-test/Terminal/SystemConsole.cs create mode 100644 src/Cli/dotnet/commands/dotnet-test/Terminal/SystemConsoleColor.cs create mode 100644 src/Cli/dotnet/commands/dotnet-test/Terminal/SystemStopwatch.cs create mode 100644 src/Cli/dotnet/commands/dotnet-test/Terminal/TargetFrameworkParser.cs create mode 100644 src/Cli/dotnet/commands/dotnet-test/Terminal/TerminalColor.cs create mode 100644 src/Cli/dotnet/commands/dotnet-test/Terminal/TerminalTestReporter.cs create mode 100644 src/Cli/dotnet/commands/dotnet-test/Terminal/TerminalTestReporterOptions.cs create mode 100644 src/Cli/dotnet/commands/dotnet-test/Terminal/TestDetailState.cs create mode 100644 src/Cli/dotnet/commands/dotnet-test/Terminal/TestNodeResultsState.cs create mode 100644 src/Cli/dotnet/commands/dotnet-test/Terminal/TestOutcome.cs create mode 100644 src/Cli/dotnet/commands/dotnet-test/Terminal/TestProgressState.cs create mode 100644 src/Cli/dotnet/commands/dotnet-test/Terminal/TestProgressStateAwareTerminal.cs create mode 100644 src/Cli/dotnet/commands/dotnet-test/Terminal/TestRunArtifact.cs create mode 100644 src/Cli/dotnet/commands/dotnet-test/Terminal/WarningMessage.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 93ff1f7a4954..2e3b4ae1f740 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -63,8 +63,6 @@ - - diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 8f0e3694f845..30817f0e174a 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -634,10 +634,6 @@ https://github.com/dotnet/runtime e77011b31a3e5c47d931248a64b47f9b2d47853d - - https://github.com/microsoft/testfx - e8edc352e84880a7bc9d3b23fe1eb2d14fa8f229 - https://github.com/microsoft/testfx e8edc352e84880a7bc9d3b23fe1eb2d14fa8f229 diff --git a/src/Cli/dotnet/commands/dotnet-test/LocalizableStrings.resx b/src/Cli/dotnet/commands/dotnet-test/LocalizableStrings.resx index 2a6adfc6f413..ad7b9ae7f9ee 100644 --- a/src/Cli/dotnet/commands/dotnet-test/LocalizableStrings.resx +++ b/src/Cli/dotnet/commands/dotnet-test/LocalizableStrings.resx @@ -298,10 +298,10 @@ Examples: NAME="VALUE" - No serializer registered with ID '{0}' + No serializer registered with ID '{0}' - No serializer registered with type '{0}' + No serializer registered with type '{0}' The max number of test modules that can run in parallel. @@ -326,4 +326,112 @@ Examples: Test application(s) that support VSTest are not supported. - + + Aborted + + + {0} tests running + + + and {0} more + + + Actual + + + canceled + + + Canceling the test session... + + + Console is already in batching mode. + Exception that is thrown when console is already collecting input into a batch (into a string builder), and code asks to enable batching mode again. + + + Discovered {0} tests in assembly + 0 is count, the sentence is followed by the path of the assebly + + + Discovering tests from + + + Exit code + + + Expected + + + Failed + + + failed + + + failed with {0} error(s) + + + failed with {0} error(s) and {1} warning(s) + + + failed with {0} warning(s) + + + For test + is followed by test name + + + from + from followed by a file name to point to the file from which test is originating + + + In process file artifacts produced: + + + Minimum expected tests policy violation, tests ran {0}, minimum expected {1} + {0}, {1} number of tests + + + Out of process file artifacts produced: + + + Passed + + + passed + + + Running tests from + + + skipped + + + at + at that is used for a stack frame location in a stack trace, is followed by a class and method name + + + in + in that is used in stack frame it is followed by file name + + + Error output + + + Standard output + + + Discovered {0} tests in {1} assemblies. + 0 is number of tests, 1 is count of assemblies + + + Discovered {0} tests. + 0 is number of tests + + + Test run summary: + + + Zero tests ran + + \ No newline at end of file diff --git a/src/Cli/dotnet/commands/dotnet-test/Terminal/AnsiCodes.cs b/src/Cli/dotnet/commands/dotnet-test/Terminal/AnsiCodes.cs new file mode 100644 index 000000000000..8e9543c9dbe3 --- /dev/null +++ b/src/Cli/dotnet/commands/dotnet-test/Terminal/AnsiCodes.cs @@ -0,0 +1,144 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.Testing.Platform.OutputDevice.Terminal; + +/// +/// A collection of standard ANSI/VT100 control codes. +/// +internal static class AnsiCodes +{ + /// + /// Escape character. + /// + public const string Esc = "\x1b"; + + /// + /// The control sequence introducer. + /// + public const string CSI = $"{Esc}["; + + /// + /// Select graphic rendition. + /// + /// + /// Print color-code to change text color. + /// + public const string SetColor = "m"; + + /// + /// Select graphic rendition - set bold mode. + /// + /// + /// Print to change text to bold. + /// + public const string SetBold = "1m"; + + /// + /// A shortcut to reset color back to normal. + /// + public const string SetDefaultColor = CSI + "m"; + + /// + /// Non-xterm extension to render a hyperlink. + /// + /// + /// Print urltext to render a hyperlink. + /// + public const string LinkPrefix = $"{Esc}]8;;"; + + /// + /// . + /// + public const string LinkInfix = $"{Esc}\\"; + + /// + /// . + /// + public const string LinkSuffix = $"{Esc}]8;;{Esc}\\"; + + /// + /// Moves up the specified number of lines and puts cursor at the beginning of the line. + /// + /// + /// Print N to move N lines up. + /// + public const string MoveUpToLineStart = "F"; + + /// + /// Moves forward (to the right) the specified number of characters. + /// + /// + /// Print N to move N characters forward. + /// + public const string MoveForward = "C"; + + /// + /// Moves backward (to the left) the specified number of characters. + /// + /// + /// Print N to move N characters backward. + /// + public const string MoveBackward = "D"; + + /// + /// Clears everything from cursor to end of screen. + /// + /// + /// Print to clear. + /// + public const string EraseInDisplay = "J"; + + /// + /// Clears everything from cursor to the end of the current line. + /// + /// + /// Print to clear. + /// + public const string EraseInLine = "K"; + + /// + /// Hides the cursor. + /// + public const string HideCursor = $"{Esc}[?25l"; + + /// + /// Shows/restores the cursor. + /// + public const string ShowCursor = $"{Esc}[?25h"; + + /// + /// Set progress state to a busy spinner.
+ /// Note: this code works only on ConEmu terminals, and conflicts with push a notification code on iTerm2. + ///
+ /// + /// ConEmu specific OSC codes.
+ /// iTerm2 proprietary escape codes. + ///
+ public const string SetBusySpinner = $"{Esc}]9;4;3;{Esc}\\"; + + /// + /// Remove progress state, restoring taskbar status to normal.
+ /// Note: this code works only on ConEmu terminals, and conflicts with push a notification code on iTerm2. + ///
+ /// + /// ConEmu specific OSC codes.
+ /// iTerm2 proprietary escape codes. + ///
+ public const string RemoveBusySpinner = $"{Esc}]9;4;0;{Esc}\\"; + + public static string Colorize(string? s, TerminalColor color) + => String.IsNullOrWhiteSpace(s) ? s ?? string.Empty : $"{CSI}{(int)color}{SetColor}{s}{SetDefaultColor}"; + + public static string MakeBold(string? s) + => String.IsNullOrWhiteSpace(s) ? s ?? string.Empty : $"{CSI}{SetBold}{s}{SetDefaultColor}"; + + public static string MoveCursorBackward(int count) => $"{CSI}{count}{MoveBackward}"; + + /// + /// Moves cursor to the specified column, or the rightmost column if is greater than the width of the terminal. + /// + /// Column index. + /// Control codes to set the desired position. + public static string SetCursorHorizontal(int column) => $"{CSI}{column}G"; +} diff --git a/src/Cli/dotnet/commands/dotnet-test/Terminal/AnsiDetector.cs b/src/Cli/dotnet/commands/dotnet-test/Terminal/AnsiDetector.cs new file mode 100644 index 000000000000..3cb6c139e71a --- /dev/null +++ b/src/Cli/dotnet/commands/dotnet-test/Terminal/AnsiDetector.cs @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +// Portions of the code in this file were ported from the spectre.console by Patrik Svensson, Phil Scott, Nils Andresen +// https://github.com/spectreconsole/spectre.console/blob/main/src/Spectre.Console/Internal/Backends/Ansi/AnsiDetector.cs +// and from the supports-ansi project by Qingrong Ke +// https://github.com/keqingrong/supports-ansi/blob/master/index.js + +using System.Text.RegularExpressions; + +namespace Microsoft.Testing.Platform.OutputDevice.Terminal; + +/// +/// Works together with the to figure out if the current console is capable of using ANSI output codes. +/// +internal static class AnsiDetector +{ + private static readonly Regex[] TerminalsRegexes = + { + new("^xterm"), // xterm, PuTTY, Mintty + new("^rxvt"), // RXVT + new("^(?!eterm-color).*eterm.*"), // Accepts eterm, but not eterm-color, which does not support moving the cursor, see #9950. + new("^screen"), // GNU screen, tmux + new("tmux"), // tmux + new("^vt100"), // DEC VT series + new("^vt102"), // DEC VT series + new("^vt220"), // DEC VT series + new("^vt320"), // DEC VT series + new("ansi"), // ANSI + new("scoansi"), // SCO ANSI + new("cygwin"), // Cygwin, MinGW + new("linux"), // Linux console + new("konsole"), // Konsole + new("bvterm"), // Bitvise SSH Client + new("^st-256color"), // Suckless Simple Terminal, st + new("alacritty"), // Alacritty + }; + + public static bool IsAnsiSupported(string? termType) + => !String.IsNullOrEmpty(termType) && TerminalsRegexes.Any(regex => regex.IsMatch(termType)); +} diff --git a/src/Cli/dotnet/commands/dotnet-test/Terminal/AnsiTerminal.cs b/src/Cli/dotnet/commands/dotnet-test/Terminal/AnsiTerminal.cs new file mode 100644 index 000000000000..a52a8b951d17 --- /dev/null +++ b/src/Cli/dotnet/commands/dotnet-test/Terminal/AnsiTerminal.cs @@ -0,0 +1,311 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Globalization; +using Microsoft.Testing.Platform.Helpers; +using LocalizableStrings = Microsoft.DotNet.Tools.Test.LocalizableStrings; + +namespace Microsoft.Testing.Platform.OutputDevice.Terminal; + +/// +/// Terminal writer that is used when writing ANSI is allowed. It is capable of batching as many updates as possible and writing them at the end, +/// because the terminal is responsible for rendering the colors and control codes. +/// +internal sealed class AnsiTerminal : ITerminal +{ + /// + /// File extensions that we will link to directly, all other files + /// are linked to their directory, to avoid opening dlls, or executables. + /// + private static readonly string[] KnownFileExtensions = new string[] + { + // code files + ".cs", + ".vb", + ".fs", + // logs + ".log", + ".txt", + // reports + ".coverage", + ".ctrf", + ".html", + ".junit", + ".nunit", + ".trx", + ".xml", + ".xunit", + }; + + private readonly IConsole _console; + private readonly string? _baseDirectory; + private readonly bool _useBusyIndicator; + private readonly StringBuilder _stringBuilder = new(); + private bool _isBatching; + private AnsiTerminalTestProgressFrame _currentFrame = new(0, 0); + + public AnsiTerminal(IConsole console, string? baseDirectory) + { + _console = console; + _baseDirectory = baseDirectory ?? Directory.GetCurrentDirectory(); + + // Output ansi code to get spinner on top of a terminal, to indicate in-progress task. + // https://github.com/dotnet/msbuild/issues/8958: iTerm2 treats ;9 code to post a notification instead, so disable progress reporting on Mac. + _useBusyIndicator = !RuntimeInformation.IsOSPlatform(OSPlatform.OSX); + } + + public int Width + => _console.IsOutputRedirected ? int.MaxValue : _console.BufferWidth; + + public int Height + => _console.IsOutputRedirected ? int.MaxValue : _console.BufferHeight; + + public void Append(char value) + { + if (_isBatching) + { + _stringBuilder.Append(value); + } + else + { + _console.Write(value); + } + } + + public void Append(string value) + { + if (_isBatching) + { + _stringBuilder.Append(value); + } + else + { + _console.Write(value); + } + } + + public void AppendLine() + { + if (_isBatching) + { + _stringBuilder.AppendLine(); + } + else + { + _console.WriteLine(); + } + } + + public void AppendLine(string value) + { + if (_isBatching) + { + _stringBuilder.AppendLine(value); + } + else + { + _console.WriteLine(value); + } + } + + public void SetColor(TerminalColor color) + { + string setColor = $"{AnsiCodes.CSI}{(int)color}{AnsiCodes.SetColor}"; + if (_isBatching) + { + _stringBuilder.Append(setColor); + } + else + { + _console.Write(setColor); + } + } + + public void ResetColor() + { + string resetColor = AnsiCodes.SetDefaultColor; + if (_isBatching) + { + _stringBuilder.Append(resetColor); + } + else + { + _console.Write(resetColor); + } + } + + public void ShowCursor() + { + if (_isBatching) + { + _stringBuilder.Append(AnsiCodes.ShowCursor); + } + else + { + _console.Write(AnsiCodes.ShowCursor); + } + } + + public void HideCursor() + { + if (_isBatching) + { + _stringBuilder.Append(AnsiCodes.HideCursor); + } + else + { + _console.Write(AnsiCodes.HideCursor); + } + } + + public void StartUpdate() + { + if (_isBatching) + { + throw new InvalidOperationException(LocalizableStrings.ConsoleIsAlreadyInBatchingMode); + } + + _stringBuilder.Clear(); + _isBatching = true; + } + + public void StopUpdate() + { + _console.Write(_stringBuilder.ToString()); + _isBatching = false; + } + + public void AppendLink(string? path, int? lineNumber) + { + if (String.IsNullOrWhiteSpace(path)) + { + return; + } + + // For non code files, point to the directory, so we don't end up running the + // exe by clicking at the link. + string? extension = Path.GetExtension(path); + bool linkToFile = !String.IsNullOrWhiteSpace(extension) && KnownFileExtensions.Contains(extension); + + bool knownNonExistingFile = path.StartsWith("/_/", ignoreCase: false, CultureInfo.CurrentCulture); + + string linkPath = path; + if (!linkToFile) + { + try + { + linkPath = Path.GetDirectoryName(linkPath) ?? linkPath; + } + catch + { + // Ignore all GetDirectoryName errors. + } + } + + // If the output path is under the initial working directory, make the console output relative to that to save space. + if (_baseDirectory != null && path.StartsWith(_baseDirectory, FileUtilities.PathComparison)) + { + if (path.Length > _baseDirectory.Length + && (path[_baseDirectory.Length] == Path.DirectorySeparatorChar + || path[_baseDirectory.Length] == Path.AltDirectorySeparatorChar)) + { + path = path[(_baseDirectory.Length + 1)..]; + } + } + + if (lineNumber != null) + { + path += $":{lineNumber}"; + } + + if (knownNonExistingFile) + { + Append(path); + return; + } + + // Generates file:// schema url string which is better handled by various Terminal clients than raw folder name. + if (Uri.TryCreate(linkPath, UriKind.Absolute, out Uri? uri)) + { + // url.ToString() un-escapes the URL which is needed for our case file:// + linkPath = uri.ToString(); + } + + SetColor(TerminalColor.DarkGray); + Append(AnsiCodes.LinkPrefix); + Append(linkPath); + Append(AnsiCodes.LinkInfix); + Append(path); + Append(AnsiCodes.LinkSuffix); + ResetColor(); + } + + public void MoveCursorUp(int lineCount) + { + string moveCursor = $"{AnsiCodes.CSI}{lineCount}{AnsiCodes.MoveUpToLineStart}"; + if (_isBatching) + { + _stringBuilder.AppendLine(moveCursor); + } + else + { + _console.WriteLine(moveCursor); + } + } + + public void SetCursorHorizontal(int position) + { + string setCursor = AnsiCodes.SetCursorHorizontal(position); + if (_isBatching) + { + _stringBuilder.Append(setCursor); + } + else + { + _console.Write(setCursor); + } + } + + /// + /// Erases the previously printed live node output. + /// + public void EraseProgress() + { + if (_currentFrame.RenderedLines == null || _currentFrame.RenderedLines.Count == 0) + { + return; + } + + AppendLine($"{AnsiCodes.CSI}{_currentFrame.RenderedLines.Count + 2}{AnsiCodes.MoveUpToLineStart}"); + Append($"{AnsiCodes.CSI}{AnsiCodes.EraseInDisplay}"); + _currentFrame.Clear(); + } + + public void RenderProgress(TestProgressState?[] progress) + { + AnsiTerminalTestProgressFrame newFrame = new(Width, Height); + newFrame.Render(_currentFrame, progress, terminal: this); + + _currentFrame = newFrame; + } + + public void StartBusyIndicator() + { + if (_useBusyIndicator) + { + Append(AnsiCodes.SetBusySpinner); + } + + HideCursor(); + } + + public void StopBusyIndicator() + { + if (_useBusyIndicator) + { + Append(AnsiCodes.RemoveBusySpinner); + } + + ShowCursor(); + } +} diff --git a/src/Cli/dotnet/commands/dotnet-test/Terminal/AnsiTerminalTestProgressFrame.cs b/src/Cli/dotnet/commands/dotnet-test/Terminal/AnsiTerminalTestProgressFrame.cs new file mode 100644 index 000000000000..35cd1f7e3813 --- /dev/null +++ b/src/Cli/dotnet/commands/dotnet-test/Terminal/AnsiTerminalTestProgressFrame.cs @@ -0,0 +1,352 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Globalization; + +namespace Microsoft.Testing.Platform.OutputDevice.Terminal; + +/// +/// Captures that was rendered to screen, so we can only partially update the screen on next update. +/// +internal sealed class AnsiTerminalTestProgressFrame +{ + private const int MaxColumn = 250; + + public int Width { get; } + + public int Height { get; } + + public List? RenderedLines { get; set; } + + public AnsiTerminalTestProgressFrame(int width, int height) + { + Width = Math.Min(width, MaxColumn); + Height = height; + } + + public void AppendTestWorkerProgress(TestProgressState progress, RenderedProgressItem currentLine, AnsiTerminal terminal) + { + string durationString = HumanReadableDurationFormatter.Render(progress.Stopwatch.Elapsed); + + currentLine.RenderedDurationLength = durationString.Length; + + int nonReservedWidth = Width - (durationString.Length + 2); + + int passed = progress.PassedTests; + int failed = progress.FailedTests; + int skipped = progress.SkippedTests; + int charsTaken = 0; + + terminal.Append('['); + charsTaken++; + terminal.SetColor(TerminalColor.Green); + terminal.Append('✓'); + charsTaken++; + string passedText = passed.ToString(CultureInfo.CurrentCulture); + terminal.Append(passedText); + charsTaken += passedText.Length; + terminal.ResetColor(); + + terminal.Append('/'); + charsTaken++; + + terminal.SetColor(TerminalColor.Red); + terminal.Append('x'); + charsTaken++; + string failedText = failed.ToString(CultureInfo.CurrentCulture); + terminal.Append(failedText); + charsTaken += failedText.Length; + terminal.ResetColor(); + + terminal.Append('/'); + charsTaken++; + + terminal.SetColor(TerminalColor.Yellow); + terminal.Append('↓'); + charsTaken++; + string skippedText = skipped.ToString(CultureInfo.CurrentCulture); + terminal.Append(skippedText); + charsTaken += skippedText.Length; + terminal.ResetColor(); + terminal.Append(']'); + charsTaken++; + + terminal.Append(' '); + charsTaken++; + AppendToWidth(terminal, progress.AssemblyName, nonReservedWidth, ref charsTaken); + + if (charsTaken < nonReservedWidth && (progress.TargetFramework != null || progress.Architecture != null)) + { + int lengthNeeded = 0; + + lengthNeeded++; // for '(' + if (progress.TargetFramework != null) + { + lengthNeeded += progress.TargetFramework.Length; + if (progress.Architecture != null) + { + lengthNeeded++; // for '|' + } + } + + if (progress.Architecture != null) + { + lengthNeeded += progress.Architecture.Length; + } + + lengthNeeded++; // for ')' + + if ((charsTaken + lengthNeeded) < nonReservedWidth) + { + terminal.Append(" ("); + if (progress.TargetFramework != null) + { + terminal.Append(progress.TargetFramework); + if (progress.Architecture != null) + { + terminal.Append('|'); + } + } + + if (progress.Architecture != null) + { + terminal.Append(progress.Architecture); + } + + terminal.Append(')'); + } + } + + terminal.SetCursorHorizontal(Width - durationString.Length); + terminal.Append(durationString); + } + + public void AppendTestWorkerDetail(TestDetailState detail, RenderedProgressItem currentLine, AnsiTerminal terminal) + { + string durationString = HumanReadableDurationFormatter.Render(detail.Stopwatch?.Elapsed); + + currentLine.RenderedDurationLength = durationString.Length; + + int nonReservedWidth = Width - (durationString.Length + 2); + int charsTaken = 0; + + terminal.Append(" "); + charsTaken += 2; + + AppendToWidth(terminal, detail.Text, nonReservedWidth, ref charsTaken); + + terminal.SetCursorHorizontal(Width - durationString.Length); + terminal.Append(durationString); + } + + private static void AppendToWidth(AnsiTerminal terminal, string text, int width, ref int charsTaken) + { + if (charsTaken + text.Length < width) + { + terminal.Append(text); + charsTaken += text.Length; + } + else + { + terminal.Append("..."); + charsTaken += 3; + if (charsTaken < width) + { + int charsToTake = width - charsTaken; + string cutText = text[^charsToTake..]; + terminal.Append(cutText); + charsTaken += charsToTake; + } + } + } + + /// + /// Render VT100 string to update from current to next frame. + /// + public void Render(AnsiTerminalTestProgressFrame previousFrame, TestProgressState?[] progress, AnsiTerminal terminal) + { + // Clear everything if Terminal width or height have changed. + if (Width != previousFrame.Width || Height != previousFrame.Height) + { + terminal.EraseProgress(); + } + + // At the end of the terminal we're going to print the live progress. + // We re-render this progress by moving the cursor to the beginning of the previous progress + // and then overwriting the lines that have changed. + // The assumption we do here is that: + // - Each rendered line is a single line, i.e. a single detail cannot span multiple lines. + // - Each rendered detail can be tracked via a unique ID and version, so that we can + // quickly determine if the detail has changed since the last render. + + // Don't go up if we did not render any lines in previous frame or we already cleared them. + if (previousFrame.RenderedLines != null && previousFrame.RenderedLines.Count > 0) + { + // Move cursor back to 1st line of progress. + // + 2 because we output and empty line right below. + terminal.MoveCursorUp(previousFrame.RenderedLines.Count + 2); + } + + // When there is nothing to render, don't write empty lines, e.g. when we start the test run, and then we kick off build + // in dotnet test, there is a long pause where we have no assemblies and no test results (yet). + if (progress.Length > 0) + { + terminal.AppendLine(); + } + + int i = 0; + RenderedLines = new List(progress.Length * 2); + List progresses = GenerateLinesToRender(progress); + + foreach (object item in progresses) + { + if (previousFrame.RenderedLines != null && previousFrame.RenderedLines.Count > i) + { + if (item is TestProgressState progressItem) + { + var currentLine = new RenderedProgressItem(progressItem.Id, progressItem.Version); + RenderedLines.Add(currentLine); + + // We have a line that was rendered previously, compare it and decide how to render. + RenderedProgressItem previouslyRenderedLine = previousFrame.RenderedLines[i]; + if (previouslyRenderedLine.ProgressId == progressItem.Id && false) + { + // This is the same progress item and it was not updated since we rendered it, only update the timestamp if possible to avoid flicker. + string durationString = HumanReadableDurationFormatter.Render(progressItem.Stopwatch.Elapsed); + + if (previouslyRenderedLine.RenderedDurationLength == durationString.Length) + { + // Duration is the same length rewrite just it. + terminal.SetCursorHorizontal(MaxColumn); + terminal.Append($"{AnsiCodes.SetCursorHorizontal(MaxColumn)}{AnsiCodes.MoveCursorBackward(durationString.Length)}{durationString}"); + currentLine.RenderedDurationLength = durationString.Length; + } + else + { + // Duration is not the same length (it is longer because time moves only forward), we need to re-render the whole line + // to avoid writing the duration over the last portion of text: my.dll (1s) -> my.d (1m 1s) + terminal.Append($"{AnsiCodes.CSI}{AnsiCodes.EraseInLine}"); + AppendTestWorkerProgress(progressItem, currentLine, terminal); + } + } + else + { + // These lines are different or the line was updated. Render the whole line. + terminal.Append($"{AnsiCodes.CSI}{AnsiCodes.EraseInLine}"); + AppendTestWorkerProgress(progressItem, currentLine, terminal); + } + } + + if (item is TestDetailState detailItem) + { + var currentLine = new RenderedProgressItem(detailItem.Id, detailItem.Version); + RenderedLines.Add(currentLine); + + // We have a line that was rendered previously, compare it and decide how to render. + RenderedProgressItem previouslyRenderedLine = previousFrame.RenderedLines[i]; + if (previouslyRenderedLine.ProgressId == detailItem.Id && previouslyRenderedLine.ProgressVersion == detailItem.Version) + { + // This is the same progress item and it was not updated since we rendered it, only update the timestamp if possible to avoid flicker. + string durationString = HumanReadableDurationFormatter.Render(detailItem.Stopwatch?.Elapsed); + + if (previouslyRenderedLine.RenderedDurationLength == durationString.Length) + { + // Duration is the same length rewrite just it. + terminal.SetCursorHorizontal(MaxColumn); + terminal.Append($"{AnsiCodes.SetCursorHorizontal(MaxColumn)}{AnsiCodes.MoveCursorBackward(durationString.Length)}{durationString}"); + currentLine.RenderedDurationLength = durationString.Length; + } + else + { + // Duration is not the same length (it is longer because time moves only forward), we need to re-render the whole line + // to avoid writing the duration over the last portion of text: my.dll (1s) -> my.d (1m 1s) + terminal.Append($"{AnsiCodes.CSI}{AnsiCodes.EraseInLine}"); + AppendTestWorkerDetail(detailItem, currentLine, terminal); + } + } + else + { + // These lines are different or the line was updated. Render the whole line. + terminal.Append($"{AnsiCodes.CSI}{AnsiCodes.EraseInLine}"); + AppendTestWorkerDetail(detailItem, currentLine, terminal); + } + } + } + else + { + // We are rendering more lines than we rendered in previous frame + if (item is TestProgressState progressItem) + { + var currentLine = new RenderedProgressItem(progressItem.Id, progressItem.Version); + RenderedLines.Add(currentLine); + AppendTestWorkerProgress(progressItem, currentLine, terminal); + } + + if (item is TestDetailState detailItem) + { + var currentLine = new RenderedProgressItem(detailItem.Id, detailItem.Version); + RenderedLines.Add(currentLine); + AppendTestWorkerDetail(detailItem, currentLine, terminal); + } + } + + // This makes the progress not stick to the last line on the command line, which is + // not what I would prefer. But also if someone writes to console, the message will + // start at the beginning of the new line. Not after the progress bar that is kept on screen. + terminal.AppendLine(); + } + + // We rendered more lines in previous frame. Clear them. + if (previousFrame.RenderedLines != null && i < previousFrame.RenderedLines.Count) + { + terminal.Append($"{AnsiCodes.CSI}{AnsiCodes.EraseInDisplay}"); + } + } + + private List GenerateLinesToRender(TestProgressState?[] progress) + { + var linesToRender = new List(progress.Length); + + // Note: We want to render the list of active tests, but this can easily fill up the full screen. + // As such, we should balance the number of active tests shown per project. + // We do this by distributing the remaining lines for each projects. + TestProgressState[] progressItems = progress.OfType().ToArray(); + int linesToDistribute = (int)(Height * 0.7) - 1 - progressItems.Length; + var detailItems = new IEnumerable[progressItems.Length]; + IEnumerable sortedItemsIndices = Enumerable.Range(0, progressItems.Length).OrderBy(i => progressItems[i].TestNodeResultsState?.Count ?? 0); + + foreach (int sortedItemIndex in sortedItemsIndices) + { + detailItems[sortedItemIndex] = progressItems[sortedItemIndex].TestNodeResultsState?.GetRunningTasks( + linesToDistribute / progressItems.Length) + ?? Array.Empty(); + } + + for (int progressI = 0; progressI < progressItems.Length; progressI++) + { + linesToRender.Add(progressItems[progressI]); + linesToRender.AddRange(detailItems[progressI]); + } + + return linesToRender; + } + + public void Clear() => RenderedLines?.Clear(); + + internal sealed class RenderedProgressItem + { + public RenderedProgressItem(long id, long version) + { + ProgressId = id; + ProgressVersion = version; + } + + public long ProgressId { get; } + + public long ProgressVersion { get; } + + public int RenderedHeight { get; set; } + + public int RenderedDurationLength { get; set; } + } +} diff --git a/src/Cli/dotnet/commands/dotnet-test/Terminal/ErrorMessage.cs b/src/Cli/dotnet/commands/dotnet-test/Terminal/ErrorMessage.cs new file mode 100644 index 000000000000..f4e41aa43efa --- /dev/null +++ b/src/Cli/dotnet/commands/dotnet-test/Terminal/ErrorMessage.cs @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.Testing.Platform.OutputDevice.Terminal; + +/// +/// An error message that was sent to output during the build. +/// +internal sealed record ErrorMessage(string Text) : IProgressMessage; diff --git a/src/Cli/dotnet/commands/dotnet-test/Terminal/ExceptionFlattener.cs b/src/Cli/dotnet/commands/dotnet-test/Terminal/ExceptionFlattener.cs new file mode 100644 index 000000000000..2578e0bada68 --- /dev/null +++ b/src/Cli/dotnet/commands/dotnet-test/Terminal/ExceptionFlattener.cs @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.Testing.Platform.OutputDevice.Terminal; + +internal sealed class ExceptionFlattener +{ + internal static FlatException[] Flatten(string? errorMessage, Exception? exception) + { + if (errorMessage is null && exception is null) + { + return Array.Empty(); + } + + string? message = !String.IsNullOrWhiteSpace(errorMessage) ? errorMessage : exception?.Message; + string? type = exception?.GetType().FullName; + string? stackTrace = exception?.StackTrace; + var flatException = new FlatException(message, type, stackTrace); + + List flatExceptions = new() + { + flatException, + }; + + // Add all inner exceptions. This will flatten top level AggregateExceptions, + // and all AggregateExceptions that are directly in AggregateExceptions, but won't expand + // AggregateExceptions that are in non-aggregate exception inner exceptions. + IEnumerable aggregateExceptions = exception switch + { + AggregateException aggregate => aggregate.Flatten().InnerExceptions, + _ => [exception?.InnerException], + }; + + foreach (Exception? aggregate in aggregateExceptions) + { + Exception? currentException = aggregate; + while (currentException is not null) + { + flatExceptions.Add(new FlatException( + aggregate?.Message, + aggregate?.GetType().FullName, + aggregate?.StackTrace)); + + currentException = currentException.InnerException; + } + } + + return flatExceptions.ToArray(); + } +} + +internal sealed record FlatException(string? ErrorMessage, string? ErrorType, string? StackTrace); diff --git a/src/Cli/dotnet/commands/dotnet-test/Terminal/FileUtilities.cs b/src/Cli/dotnet/commands/dotnet-test/Terminal/FileUtilities.cs new file mode 100644 index 000000000000..41711363117b --- /dev/null +++ b/src/Cli/dotnet/commands/dotnet-test/Terminal/FileUtilities.cs @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Diagnostics; + +namespace Microsoft.Testing.Platform.OutputDevice.Terminal; + +internal static class FileUtilities +{ + internal static readonly StringComparison PathComparison = GetIsFileSystemCaseSensitive() ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase; + + internal static readonly StringComparer PathComparer = GetIsFileSystemCaseSensitive() ? StringComparer.Ordinal : StringComparer.OrdinalIgnoreCase; + + /// + /// Determines whether the file system is case sensitive. + /// Copied from https://github.com/dotnet/runtime/blob/73ba11f3015216b39cb866d9fb7d3d25e93489f2/src/libraries/Common/src/System/IO/PathInternal.CaseSensitivity.cs#L41-L59 . + /// + public static bool GetIsFileSystemCaseSensitive() + { + try + { + string pathWithUpperCase = Path.Combine(Path.GetTempPath(), "CASESENSITIVETEST" + Guid.NewGuid().ToString("N")); + using (new FileStream(pathWithUpperCase, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None, 0x1000, FileOptions.DeleteOnClose)) + { + string lowerCased = pathWithUpperCase.ToLowerInvariant(); + return !File.Exists(lowerCased); + } + } + catch (Exception exc) + { + // In case something goes terribly wrong, we don't want to fail just because + // of a casing test, so we assume case-insensitive-but-preserving. + Debug.Fail("Casing test failed: " + exc); + return false; + } + } +} diff --git a/src/Cli/dotnet/commands/dotnet-test/Terminal/HumanReadableDurationFormatter.cs b/src/Cli/dotnet/commands/dotnet-test/Terminal/HumanReadableDurationFormatter.cs new file mode 100644 index 000000000000..97a43b6a15fe --- /dev/null +++ b/src/Cli/dotnet/commands/dotnet-test/Terminal/HumanReadableDurationFormatter.cs @@ -0,0 +1,112 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Globalization; + +namespace Microsoft.Testing.Platform.OutputDevice.Terminal; + +internal static class HumanReadableDurationFormatter +{ + public static void Append(ITerminal terminal, TimeSpan duration, bool wrapInParentheses = true) + { + bool hasParentValue = false; + + if (wrapInParentheses) + { + terminal.Append('('); + } + + if (duration.Days > 0) + { + terminal.Append($"{duration.Days}d"); + hasParentValue = true; + } + + if (duration.Hours > 0 || hasParentValue) + { + terminal.Append(GetFormattedPart(duration.Hours, hasParentValue, "h")); + hasParentValue = true; + } + + if (duration.Minutes > 0 || hasParentValue) + { + terminal.Append(GetFormattedPart(duration.Minutes, hasParentValue, "m")); + hasParentValue = true; + } + + if (duration.Seconds > 0 || hasParentValue) + { + terminal.Append(GetFormattedPart(duration.Seconds, hasParentValue, "s")); + hasParentValue = true; + } + + if (duration.Milliseconds >= 0 || hasParentValue) + { + terminal.Append(GetFormattedPart(duration.Milliseconds, hasParentValue, "ms", paddingWitdh: 3)); + } + + if (wrapInParentheses) + { + terminal.Append(')'); + } + } + + private static string GetFormattedPart(int value, bool hasParentValue, string suffix, int paddingWitdh = 2) + => $"{(hasParentValue ? " " : string.Empty)}{(hasParentValue ? value.ToString(CultureInfo.InvariantCulture).PadLeft(paddingWitdh, '0') : value.ToString(CultureInfo.InvariantCulture))}{suffix}"; + + public static string Render(TimeSpan? duration, bool wrapInParentheses = true, bool showMilliseconds = false) + { + if (duration is null) + { + return string.Empty; + } + + bool hasParentValue = false; + + var stringBuilder = new StringBuilder(); + + if (wrapInParentheses) + { + stringBuilder.Append('('); + } + + if (duration.Value.Days > 0) + { + stringBuilder.Append(CultureInfo.CurrentCulture, $"{duration.Value.Days}d"); + hasParentValue = true; + } + + if (duration.Value.Hours > 0 || hasParentValue) + { + stringBuilder.Append(GetFormattedPart(duration.Value.Hours, hasParentValue, "h")); + hasParentValue = true; + } + + if (duration.Value.Minutes > 0 || hasParentValue) + { + stringBuilder.Append(GetFormattedPart(duration.Value.Minutes, hasParentValue, "m")); + hasParentValue = true; + } + + if (duration.Value.Seconds > 0 || hasParentValue || !showMilliseconds) + { + stringBuilder.Append(GetFormattedPart(duration.Value.Seconds, hasParentValue, "s")); + hasParentValue = true; + } + + if (showMilliseconds) + { + if (duration.Value.Milliseconds >= 0 || hasParentValue) + { + stringBuilder.Append(GetFormattedPart(duration.Value.Milliseconds, hasParentValue, "ms", paddingWitdh: 3)); + } + } + + if (wrapInParentheses) + { + stringBuilder.Append(')'); + } + + return stringBuilder.ToString(); + } +} diff --git a/src/Cli/dotnet/commands/dotnet-test/Terminal/IColor.cs b/src/Cli/dotnet/commands/dotnet-test/Terminal/IColor.cs new file mode 100644 index 000000000000..bfd6173af166 --- /dev/null +++ b/src/Cli/dotnet/commands/dotnet-test/Terminal/IColor.cs @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.Testing.Platform.OutputDevice; + +/// +/// Represents a color. +/// +public interface IColor; diff --git a/src/Cli/dotnet/commands/dotnet-test/Terminal/IConsole.cs b/src/Cli/dotnet/commands/dotnet-test/Terminal/IConsole.cs new file mode 100644 index 000000000000..742d16083972 --- /dev/null +++ b/src/Cli/dotnet/commands/dotnet-test/Terminal/IConsole.cs @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.Testing.Platform.Helpers; + +/// +/// Wraps the static System.Console to be isolatable in tests. +/// +internal interface IConsole +{ + event ConsoleCancelEventHandler? CancelKeyPress; + + public int BufferHeight { get; } + + public int BufferWidth { get; } + + public bool IsOutputRedirected { get; } + + void SetForegroundColor(ConsoleColor color); + + void SetBackgroundColor(ConsoleColor color); + + ConsoleColor GetForegroundColor(); + + ConsoleColor GetBackgroundColor(); + + void WriteLine(); + + void WriteLine(string? value); + + void WriteLine(object? value); + + void WriteLine(string format, object? arg0); + + void WriteLine(string format, object? arg0, object? arg1); + + void WriteLine(string format, object? arg0, object? arg1, object? arg2); + + void WriteLine(string format, object?[]? args); + + void Write(string format, object?[]? args); + + void Write(string? value); + + void Write(char value); + + void Clear(); +} diff --git a/src/Cli/dotnet/commands/dotnet-test/Terminal/IProgressMessage.cs b/src/Cli/dotnet/commands/dotnet-test/Terminal/IProgressMessage.cs new file mode 100644 index 000000000000..f8f331ab233c --- /dev/null +++ b/src/Cli/dotnet/commands/dotnet-test/Terminal/IProgressMessage.cs @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.Testing.Platform.OutputDevice.Terminal; + +/// +/// Error or warning message that was sent to screen during the test run. +/// +internal interface IProgressMessage; diff --git a/src/Cli/dotnet/commands/dotnet-test/Terminal/IStopwatch.cs b/src/Cli/dotnet/commands/dotnet-test/Terminal/IStopwatch.cs new file mode 100644 index 000000000000..e85a0a5bd627 --- /dev/null +++ b/src/Cli/dotnet/commands/dotnet-test/Terminal/IStopwatch.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.Testing.Platform.Helpers; + +internal interface IStopwatch +{ + void Start(); + + void Stop(); + + TimeSpan Elapsed { get; } +} diff --git a/src/Cli/dotnet/commands/dotnet-test/Terminal/ITerminal.cs b/src/Cli/dotnet/commands/dotnet-test/Terminal/ITerminal.cs new file mode 100644 index 000000000000..8fe335e2a83a --- /dev/null +++ b/src/Cli/dotnet/commands/dotnet-test/Terminal/ITerminal.cs @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.Testing.Platform.OutputDevice.Terminal; + +/// +/// An ANSI or non-ANSI terminal that is capable of rendering the messages from . +/// +internal interface ITerminal +{ + int Width { get; } + + int Height { get; } + + void Append(char value); + + void Append(string value); + + void AppendLine(); + + void AppendLine(string value); + + void AppendLink(string path, int? lineNumber); + + void SetColor(TerminalColor color); + + void ResetColor(); + + void ShowCursor(); + + void HideCursor(); + + void StartUpdate(); + + void StopUpdate(); + + void EraseProgress(); + + void RenderProgress(TestProgressState?[] progress); + + void StartBusyIndicator(); + + void StopBusyIndicator(); +} diff --git a/src/Cli/dotnet/commands/dotnet-test/Terminal/NativeMethods.cs b/src/Cli/dotnet/commands/dotnet-test/Terminal/NativeMethods.cs new file mode 100644 index 000000000000..0bd220fd5055 --- /dev/null +++ b/src/Cli/dotnet/commands/dotnet-test/Terminal/NativeMethods.cs @@ -0,0 +1,122 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.Testing.Platform.OutputDevice.Terminal; + +internal static class NativeMethods +{ + internal const uint FILE_TYPE_CHAR = 0x0002; + internal const int STD_OUTPUT_HANDLE = -11; + internal const int STD_ERROR_HANDLE = -12; + internal const uint ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004; + + private static bool? s_isWindows; + + /// + /// Gets a value indicating whether we are running under some version of Windows. + /// + // TODO: [SupportedOSPlatformGuard("windows")] + internal static bool IsWindows + { + get + { + s_isWindows ??= RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + return s_isWindows.Value; + } + } + + internal static (bool AcceptAnsiColorCodes, bool OutputIsScreen, uint? OriginalConsoleMode) QueryIsScreenAndTryEnableAnsiColorCodes(StreamHandleType handleType = StreamHandleType.StdOut) + { + if (Console.IsOutputRedirected) + { + // There's no ANSI terminal support if console output is redirected. + return (AcceptAnsiColorCodes: false, OutputIsScreen: false, OriginalConsoleMode: null); + } + + bool acceptAnsiColorCodes = false; + bool outputIsScreen = false; + uint? originalConsoleMode = null; + if (IsWindows) + { + try + { + nint outputStream = GetStdHandle((int)handleType); + if (GetConsoleMode(outputStream, out uint consoleMode)) + { + if ((consoleMode & ENABLE_VIRTUAL_TERMINAL_PROCESSING) == ENABLE_VIRTUAL_TERMINAL_PROCESSING) + { + // Console is already in required state. + acceptAnsiColorCodes = true; + } + else + { + originalConsoleMode = consoleMode; + consoleMode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING; + if (SetConsoleMode(outputStream, consoleMode) && GetConsoleMode(outputStream, out consoleMode)) + { + // We only know if vt100 is supported if the previous call actually set the new flag, older + // systems ignore the setting. + acceptAnsiColorCodes = (consoleMode & ENABLE_VIRTUAL_TERMINAL_PROCESSING) == ENABLE_VIRTUAL_TERMINAL_PROCESSING; + } + } + + uint fileType = GetFileType(outputStream); + // The std out is a char type (LPT or Console). + outputIsScreen = fileType == FILE_TYPE_CHAR; + acceptAnsiColorCodes &= outputIsScreen; + } + } + catch + { + // In the unlikely case that the above fails we just ignore and continue. + } + } + else + { + // On posix OSes detect whether the terminal supports VT100 from the value of the TERM environment variable. +#pragma warning disable RS0030 // Do not use banned APIs + acceptAnsiColorCodes = AnsiDetector.IsAnsiSupported(Environment.GetEnvironmentVariable("TERM")); +#pragma warning restore RS0030 // Do not use banned APIs + // It wasn't redirected as tested above so we assume output is screen/console + outputIsScreen = true; + } + + return (acceptAnsiColorCodes, outputIsScreen, originalConsoleMode); + } + + internal static void RestoreConsoleMode(uint? originalConsoleMode, StreamHandleType handleType = StreamHandleType.StdOut) + { + if (IsWindows && originalConsoleMode is not null) + { + nint stdOut = GetStdHandle((int)handleType); + _ = SetConsoleMode(stdOut, originalConsoleMode.Value); + } + } + + [DllImport("kernel32.dll")] + // TODO: [SupportedOSPlatform("windows")] + internal static extern nint GetStdHandle(int nStdHandle); + + [DllImport("kernel32.dll")] + // TODO: [SupportedOSPlatform("windows")] + internal static extern uint GetFileType(nint hFile); + + internal enum StreamHandleType + { + /// + /// StdOut. + /// + StdOut = STD_OUTPUT_HANDLE, + + /// + /// StdError. + /// + StdErr = STD_ERROR_HANDLE, + } + + [DllImport("kernel32.dll")] + internal static extern bool GetConsoleMode(nint hConsoleHandle, out uint lpMode); + + [DllImport("kernel32.dll")] + internal static extern bool SetConsoleMode(nint hConsoleHandle, uint dwMode); +} diff --git a/src/Cli/dotnet/commands/dotnet-test/Terminal/NonAnsiTerminal.cs b/src/Cli/dotnet/commands/dotnet-test/Terminal/NonAnsiTerminal.cs new file mode 100644 index 000000000000..798b95e40df7 --- /dev/null +++ b/src/Cli/dotnet/commands/dotnet-test/Terminal/NonAnsiTerminal.cs @@ -0,0 +1,254 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Globalization; +using Microsoft.Testing.Platform.Helpers; +using LocalizableStrings = Microsoft.DotNet.Tools.Test.LocalizableStrings; + +namespace Microsoft.Testing.Platform.OutputDevice.Terminal; + +/// +/// Non-ANSI terminal that writes text using the standard Console.Foreground color capabilities to stay compatible with +/// standard Windows command line, and other command lines that are not capable of ANSI, or when output is redirected. +/// +internal sealed class NonAnsiTerminal : ITerminal +{ + private readonly IConsole _console; + private readonly ConsoleColor _defaultForegroundColor; + private readonly StringBuilder _stringBuilder = new(); + private bool _isBatching; + + public NonAnsiTerminal(IConsole console) + { + _console = console; + _defaultForegroundColor = _console.GetForegroundColor(); + } + + public int Width => _console.IsOutputRedirected ? int.MaxValue : _console.BufferWidth; + + public int Height => _console.IsOutputRedirected ? int.MaxValue : _console.BufferHeight; + + public void Append(char value) + { + if (_isBatching) + { + _stringBuilder.Append(value); + } + else + { + _console.Write(value); + } + } + + public void Append(string value) + { + if (_isBatching) + { + _stringBuilder.Append(value); + } + else + { + _console.Write(value); + } + } + + public void AppendLine() + { + if (_isBatching) + { + _stringBuilder.AppendLine(); + } + else + { + _console.WriteLine(); + } + } + + public void AppendLine(string value) + { + if (_isBatching) + { + _stringBuilder.AppendLine(value); + } + else + { + _console.WriteLine(value); + } + } + + public void AppendLink(string path, int? lineNumber) + { + Append(path); + if (lineNumber.HasValue) + { + Append($":{lineNumber}"); + } + } + + public void SetColor(TerminalColor color) + { + if (_isBatching) + { + _console.Write(_stringBuilder.ToString()); + _stringBuilder.Clear(); + } + + _console.SetForegroundColor(ToConsoleColor(color)); + } + + public void ResetColor() + { + if (_isBatching) + { + _console.Write(_stringBuilder.ToString()); + _stringBuilder.Clear(); + } + + _console.SetForegroundColor(_defaultForegroundColor); + } + + public void ShowCursor() + { + // nop + } + + public void HideCursor() + { + // nop + } + + public void StartUpdate() + { + if (_isBatching) + { + throw new InvalidOperationException(LocalizableStrings.ConsoleIsAlreadyInBatchingMode); + } + + _stringBuilder.Clear(); + _isBatching = true; + } + + public void StopUpdate() + { + _console.Write(_stringBuilder.ToString()); + _isBatching = false; + } + + private ConsoleColor ToConsoleColor(TerminalColor color) => color switch + { + TerminalColor.Black => ConsoleColor.Black, + TerminalColor.DarkRed => ConsoleColor.DarkRed, + TerminalColor.DarkGreen => ConsoleColor.DarkGreen, + TerminalColor.DarkYellow => ConsoleColor.DarkYellow, + TerminalColor.DarkBlue => ConsoleColor.DarkBlue, + TerminalColor.DarkMagenta => ConsoleColor.DarkMagenta, + TerminalColor.DarkCyan => ConsoleColor.DarkCyan, + TerminalColor.Gray => ConsoleColor.White, + TerminalColor.Default => _defaultForegroundColor, + TerminalColor.DarkGray => ConsoleColor.Gray, + TerminalColor.Red => ConsoleColor.Red, + TerminalColor.Green => ConsoleColor.Green, + TerminalColor.Yellow => ConsoleColor.Yellow, + TerminalColor.Blue => ConsoleColor.Blue, + TerminalColor.Magenta => ConsoleColor.Magenta, + TerminalColor.Cyan => ConsoleColor.Cyan, + TerminalColor.White => ConsoleColor.White, + _ => _defaultForegroundColor, + }; + + public void EraseProgress() + { + // nop + } + + public void RenderProgress(TestProgressState?[] progress) + { + int count = 0; + foreach (TestProgressState? p in progress) + { + if (p == null) + { + continue; + } + + count++; + + string durationString = HumanReadableDurationFormatter.Render(p.Stopwatch.Elapsed); + + int passed = p.PassedTests; + int failed = p.FailedTests; + int skipped = p.SkippedTests; + + // Use just ascii here, so we don't put too many restrictions on fonts needing to + // properly show unicode, or logs being saved in particular encoding. + Append('['); + SetColor(TerminalColor.DarkGreen); + Append('+'); + Append(passed.ToString(CultureInfo.CurrentCulture)); + ResetColor(); + + Append('/'); + + SetColor(TerminalColor.DarkRed); + Append('x'); + Append(failed.ToString(CultureInfo.CurrentCulture)); + ResetColor(); + + Append('/'); + + SetColor(TerminalColor.DarkYellow); + Append('?'); + Append(skipped.ToString(CultureInfo.CurrentCulture)); + ResetColor(); + Append(']'); + + Append(' '); + Append(p.AssemblyName); + + if (p.TargetFramework != null || p.Architecture != null) + { + Append(" ("); + if (p.TargetFramework != null) + { + Append(p.TargetFramework); + Append('|'); + } + + if (p.Architecture != null) + { + Append(p.Architecture); + } + + Append(')'); + } + + TestDetailState? activeTest = p.TestNodeResultsState?.GetRunningTasks(1).FirstOrDefault(); + if (!String.IsNullOrWhiteSpace(activeTest?.Text)) + { + Append(" - "); + Append(activeTest.Text); + Append(' '); + } + + Append(durationString); + + AppendLine(); + } + + // Do not render empty lines when there is nothing to show. + if (count > 0) + { + AppendLine(); + } + } + + public void StartBusyIndicator() + { + // nop + } + + public void StopBusyIndicator() + { + // nop + } +} diff --git a/src/Cli/dotnet/commands/dotnet-test/Terminal/SystemConsole.cs b/src/Cli/dotnet/commands/dotnet-test/Terminal/SystemConsole.cs new file mode 100644 index 000000000000..996ac44b1142 --- /dev/null +++ b/src/Cli/dotnet/commands/dotnet-test/Terminal/SystemConsole.cs @@ -0,0 +1,212 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.Testing.Platform.Helpers; + +internal sealed class SystemConsole : IConsole +{ + private const int WriteBufferSize = 256; + private static readonly StreamWriter CaptureConsoleOutWriter; + + /// + /// Gets the height of the buffer area. + /// + public int BufferHeight => Console.BufferHeight; + + /// + /// Gets the width of the buffer area. + /// + public int BufferWidth => Console.BufferWidth; + + /// + /// Gets a value indicating whether output has been redirected from the standard output stream. + /// + public bool IsOutputRedirected => Console.IsOutputRedirected; + + private bool _suppressOutput; + + static SystemConsole() => + // From https://github.com/dotnet/runtime/blob/main/src/libraries/System.Console/src/System/Console.cs#L236 + CaptureConsoleOutWriter = new StreamWriter( + stream: Console.OpenStandardOutput(), + encoding: Console.Out.Encoding, + bufferSize: WriteBufferSize, + leaveOpen: true) + { + AutoFlush = true, + }; + + // the following event does not make sense in the mobile scenarios, user cannot ctrl+c + // but can just kill the app in the device via a gesture + public event ConsoleCancelEventHandler? CancelKeyPress + { + add + { +#if NET8_0_OR_GREATER + if (RuntimeInformation.RuntimeIdentifier.Contains("ios") || + RuntimeInformation.RuntimeIdentifier.Contains("android")) + { + return; + } +#endif + +#pragma warning disable IDE0027 // Use expression body for accessor + Console.CancelKeyPress += value; +#pragma warning restore IDE0027 // Use expression body for accessor + } + + remove + { +#if NET8_0_OR_GREATER + if (RuntimeInformation.RuntimeIdentifier.Contains("ios") || + RuntimeInformation.RuntimeIdentifier.Contains("android")) + { + return; + } +#endif +#pragma warning disable IDE0027 // Use expression body for accessor + Console.CancelKeyPress -= value; +#pragma warning restore IDE0027 // Use expression body for accessor + } + } + + public void SuppressOutput() => _suppressOutput = true; + + public void WriteLine() + { + if (!_suppressOutput) + { + CaptureConsoleOutWriter.WriteLine(); + } + } + + public void WriteLine(string? value) + { + if (!_suppressOutput) + { + CaptureConsoleOutWriter.WriteLine(value); + } + } + + public void WriteLine(object? value) + { + if (!_suppressOutput) + { + CaptureConsoleOutWriter.WriteLine(value); + } + } + + public void WriteLine(string format, object? arg0) + { + if (!_suppressOutput) + { + CaptureConsoleOutWriter.WriteLine(format, arg0); + } + } + + public void WriteLine(string format, object? arg0, object? arg1) + { + if (!_suppressOutput) + { + CaptureConsoleOutWriter.WriteLine(format, arg0, arg1); + } + } + + public void WriteLine(string format, object? arg0, object? arg1, object? arg2) + { + if (!_suppressOutput) + { + CaptureConsoleOutWriter.WriteLine(format, arg0, arg1, arg2); + } + } + + public void WriteLine(string format, object?[]? args) + { + if (!_suppressOutput) + { + CaptureConsoleOutWriter.WriteLine(format, args!); + } + } + + public void Write(string format, object?[]? args) + { + if (!_suppressOutput) + { + CaptureConsoleOutWriter.Write(format, args!); + } + } + + public void Write(string? value) + { + if (!_suppressOutput) + { + CaptureConsoleOutWriter.Write(value); + } + } + + public void Write(char value) + { + if (!_suppressOutput) + { + CaptureConsoleOutWriter.Write(value); + } + } + + public void SetForegroundColor(ConsoleColor color) + { +#if NET8_0_OR_GREATER + if (RuntimeInformation.RuntimeIdentifier.Contains("ios") || + RuntimeInformation.RuntimeIdentifier.Contains("android")) + { + return; + } +#endif +#pragma warning disable IDE0022 // Use expression body for method + Console.ForegroundColor = color; +#pragma warning restore IDE0022 // Use expression body for method + } + + public void SetBackgroundColor(ConsoleColor color) + { +#if NET8_0_OR_GREATER + if (RuntimeInformation.RuntimeIdentifier.Contains("ios") || + RuntimeInformation.RuntimeIdentifier.Contains("android")) + { + return; + } +#endif +#pragma warning disable IDE0022 // Use expression body for method + Console.BackgroundColor = color; +#pragma warning restore IDE0022 // Use expression body for method + } + + public ConsoleColor GetForegroundColor() + { +#if NET8_0_OR_GREATER + if (RuntimeInformation.RuntimeIdentifier.Contains("ios") || + RuntimeInformation.RuntimeIdentifier.Contains("android")) + { + return ConsoleColor.Black; + } +#endif +#pragma warning disable IDE0022 // Use expression body for method + return Console.ForegroundColor; +#pragma warning restore IDE0022 // Use expression body for method + } + + public ConsoleColor GetBackgroundColor() + { +#if NET8_0_OR_GREATER + if (RuntimeInformation.RuntimeIdentifier.Contains("ios") || + RuntimeInformation.RuntimeIdentifier.Contains("android")) + { + return ConsoleColor.Black; + } +#endif +#pragma warning disable IDE0022 // Use expression body for method + return Console.BackgroundColor; +#pragma warning restore IDE0022 // Use expression body for method + } + + public void Clear() => Console.Clear(); +} diff --git a/src/Cli/dotnet/commands/dotnet-test/Terminal/SystemConsoleColor.cs b/src/Cli/dotnet/commands/dotnet-test/Terminal/SystemConsoleColor.cs new file mode 100644 index 000000000000..e59363aa915e --- /dev/null +++ b/src/Cli/dotnet/commands/dotnet-test/Terminal/SystemConsoleColor.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.Testing.Platform.OutputDevice; + +/// +/// Represents a system console color. +/// +public sealed class SystemConsoleColor : IColor +{ + /// + /// Gets or inits the console color. + /// + public ConsoleColor ConsoleColor { get; init; } +} diff --git a/src/Cli/dotnet/commands/dotnet-test/Terminal/SystemStopwatch.cs b/src/Cli/dotnet/commands/dotnet-test/Terminal/SystemStopwatch.cs new file mode 100644 index 000000000000..210513d6093c --- /dev/null +++ b/src/Cli/dotnet/commands/dotnet-test/Terminal/SystemStopwatch.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Diagnostics; + +namespace Microsoft.Testing.Platform.Helpers; + +internal sealed class SystemStopwatch : IStopwatch +{ + private readonly Stopwatch _stopwatch = new(); + + public TimeSpan Elapsed => _stopwatch.Elapsed; + + public void Start() => _stopwatch.Start(); + + public void Stop() => _stopwatch.Stop(); + + public static IStopwatch StartNew() + { + SystemStopwatch wallClockStopwatch = new(); + wallClockStopwatch.Start(); + + return wallClockStopwatch; + } +} diff --git a/src/Cli/dotnet/commands/dotnet-test/Terminal/TargetFrameworkParser.cs b/src/Cli/dotnet/commands/dotnet-test/Terminal/TargetFrameworkParser.cs new file mode 100644 index 000000000000..1e8e4ec7a5bb --- /dev/null +++ b/src/Cli/dotnet/commands/dotnet-test/Terminal/TargetFrameworkParser.cs @@ -0,0 +1,74 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Globalization; + +namespace Microsoft.Testing.Platform.OutputDevice; + +internal static class TargetFrameworkParser +{ + public static string? GetShortTargetFramework(string? frameworkDescription) + { + if (frameworkDescription == null) + { + return null; + } + + // https://learn.microsoft.com/dotnet/api/system.runtime.interopservices.runtimeinformation.frameworkdescription + string netFramework = ".NET Framework"; + if (frameworkDescription.StartsWith(netFramework, ignoreCase: false, CultureInfo.InvariantCulture)) + { + // .NET Framework 4.7.2 + if (frameworkDescription.Length < (netFramework.Length + 6)) + { + return frameworkDescription; + } + + char major = frameworkDescription[netFramework.Length + 1]; + char minor = frameworkDescription[netFramework.Length + 3]; + char patch = frameworkDescription[netFramework.Length + 5]; + + if (major == '4' && minor == '6' && patch == '2') + { + return "net462"; + } + else if (major == '4' && minor == '7' && patch == '1') + { + return "net471"; + } + else if (major == '4' && minor == '7' && patch == '2') + { + return "net472"; + } + else if (major == '4' && minor == '8' && patch == '1') + { + return "net481"; + } + else + { + // Just return the first 2 numbers. + return $"net{major}{minor}"; + } + } + + string netCore = ".NET Core"; + if (frameworkDescription.StartsWith(netCore, ignoreCase: false, CultureInfo.InvariantCulture)) + { + // .NET Core 3.1 + return frameworkDescription.Length >= (netCore.Length + 4) + ? $"netcoreapp{frameworkDescription[netCore.Length + 1]}.{frameworkDescription[netCore.Length + 3]}" + : frameworkDescription; + } + + string net = ".NET"; + if (frameworkDescription.StartsWith(net, ignoreCase: false, CultureInfo.InvariantCulture)) + { + int firstDotInVersion = frameworkDescription.IndexOf('.', net.Length + 1); + return firstDotInVersion < 1 + ? frameworkDescription + : $"net{frameworkDescription.Substring(net.Length + 1, firstDotInVersion - net.Length - 1)}.{frameworkDescription[firstDotInVersion + 1]}"; + } + + return frameworkDescription; + } +} diff --git a/src/Cli/dotnet/commands/dotnet-test/Terminal/TerminalColor.cs b/src/Cli/dotnet/commands/dotnet-test/Terminal/TerminalColor.cs new file mode 100644 index 000000000000..1120f47962b4 --- /dev/null +++ b/src/Cli/dotnet/commands/dotnet-test/Terminal/TerminalColor.cs @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.Testing.Platform.OutputDevice.Terminal; + +/// +/// Enumerates the text colors supported by VT100 terminal. +/// +internal enum TerminalColor +{ + /// + /// Black. + /// + Black = 30, + + /// + /// DarkRed. + /// + DarkRed = 31, + + /// + /// DarkGreen. + /// + DarkGreen = 32, + + /// + /// DarkYellow. + /// + DarkYellow = 33, + + /// + /// DarkBlue. + /// + DarkBlue = 34, + + /// + /// DarkMagenta. + /// + DarkMagenta = 35, + + /// + /// DarkCyan. + /// + DarkCyan = 36, + + /// + /// Gray. This entry looks out of order, but in reality 37 is dark white, which is lighter than bright black = Dark Gray in Console colors. + /// + Gray = 37, + + /// + /// Default. + /// + Default = 39, + + /// + /// DarkGray. This entry looks out of order, but in reality 90 is bright black, which is darker than dark white = Gray in Console colors. + /// + DarkGray = 90, + + /// + /// Red. + /// + Red = 91, + + /// + /// Green. + /// + Green = 92, + + /// + /// Yellow. + /// + Yellow = 93, + + /// + /// Blue. + /// + Blue = 94, + + /// + /// Magenta. + /// + Magenta = 95, + + /// + /// Cyan. + /// + Cyan = 96, + + /// + /// White. + /// + White = 97, +} diff --git a/src/Cli/dotnet/commands/dotnet-test/Terminal/TerminalTestReporter.cs b/src/Cli/dotnet/commands/dotnet-test/Terminal/TerminalTestReporter.cs new file mode 100644 index 000000000000..38eda2d001cf --- /dev/null +++ b/src/Cli/dotnet/commands/dotnet-test/Terminal/TerminalTestReporter.cs @@ -0,0 +1,1019 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Concurrent; +using System.Globalization; +using System.Text.RegularExpressions; +using Microsoft.Testing.Platform.Helpers; +using LocalizableStrings = Microsoft.DotNet.Tools.Test.LocalizableStrings; + +namespace Microsoft.Testing.Platform.OutputDevice.Terminal; + +/// +/// Terminal test reporter that outputs test progress and is capable of writing ANSI or non-ANSI output via the given terminal. +/// +internal sealed partial class TerminalTestReporter : IDisposable +{ + /// + /// The two directory separator characters to be passed to methods like . + /// + private static readonly string[] NewLineStrings = { "\r\n", "\n" }; + + internal const string SingleIndentation = " "; + + internal const string DoubleIndentation = $"{SingleIndentation}{SingleIndentation}"; + + internal Func CreateStopwatch { get; set; } = SystemStopwatch.StartNew; + + internal event EventHandler OnProgressStartUpdate + { + add => _terminalWithProgress.OnProgressStartUpdate += value; + remove => _terminalWithProgress.OnProgressStartUpdate -= value; + } + + internal event EventHandler OnProgressStopUpdate + { + add => _terminalWithProgress.OnProgressStopUpdate += value; + remove => _terminalWithProgress.OnProgressStopUpdate -= value; + } + + private readonly ConcurrentDictionary _assemblies = new(); + + private readonly List _artifacts = new(); + + private readonly TerminalTestReporterOptions _options; + + private readonly TestProgressStateAwareTerminal _terminalWithProgress; + + private readonly uint? _originalConsoleMode; + private bool _isDiscovery; + private DateTimeOffset? _testExecutionStartTime; + + private DateTimeOffset? _testExecutionEndTime; + + private int _buildErrorsCount; + + private bool _wasCancelled; + + private bool? _shouldShowPassedTests; + +#if NET7_0_OR_GREATER + // Specifying no timeout, the regex is linear. And the timeout does not measure the regex only, but measures also any + // thread suspends, so the regex gets blamed incorrectly. + [GeneratedRegex(@$"^ at ((?.+) in (?.+):line (?\d+)|(?.+))$", RegexOptions.ExplicitCapture)] + private static partial Regex GetFrameRegex(); +#else + private static Regex? s_regex; + + [MemberNotNull(nameof(s_regex))] + private static Regex GetFrameRegex() + { + if (s_regex != null) + { + return s_regex; + } + + string atResourceName = "Word_At"; + string inResourceName = "StackTrace_InFileLineNumber"; + + string? atString = null; + string? inString = null; + + // Grab words from localized resource, in case the stack trace is localized. + try + { + // Get these resources: https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx +#pragma warning disable RS0030 // Do not use banned APIs + MethodInfo? getResourceStringMethod = typeof(Environment).GetMethod( + "GetResourceString", + BindingFlags.Static | BindingFlags.NonPublic, null, [typeof(string)], null); +#pragma warning restore RS0030 // Do not use banned APIs + if (getResourceStringMethod is not null) + { + // at + atString = (string?)getResourceStringMethod.Invoke(null, [atResourceName]); + + // in {0}:line {1} + inString = (string?)getResourceStringMethod.Invoke(null, [inResourceName]); + } + } + catch + { + // If we fail, populate the defaults below. + } + + atString = atString == null || atString == atResourceName ? "at" : atString; + inString = inString == null || inString == inResourceName ? "in {0}:line {1}" : inString; + + string inPattern = string.Format(CultureInfo.InvariantCulture, inString, "(?.+)", @"(?\d+)"); + + // Specifying no timeout, the regex is linear. And the timeout does not measure the regex only, but measures also any + // thread suspends, so the regex gets blamed incorrectly. + s_regex = new Regex(@$"^ {atString} ((?.+) {inPattern}|(?.+))$", RegexOptions.Compiled | RegexOptions.ExplicitCapture); + return s_regex; + } +#endif + + private int _counter; + + /// + /// Initializes a new instance of the class with custom terminal and manual refresh for testing. + /// + public TerminalTestReporter(IConsole console, TerminalTestReporterOptions options) + { + _options = options; + + Func showProgress = _options.ShowProgress; + TestProgressStateAwareTerminal terminalWithProgress; + + // When not writing to ANSI we write the progress to screen and leave it there so we don't want to write it more often than every few seconds. + int nonAnsiUpdateCadenceInMs = 3_000; + // When writing to ANSI we update the progress in place and it should look responsive so we update every half second, because we only show seconds on the screen, so it is good enough. + int ansiUpdateCadenceInMs = 500; + if (!_options.UseAnsi || _options.ForceAnsi is false) + { + terminalWithProgress = new TestProgressStateAwareTerminal(new NonAnsiTerminal(console), showProgress, writeProgressImmediatelyAfterOutput: false, updateEvery: nonAnsiUpdateCadenceInMs); + } + else + { + // Autodetect. + (bool consoleAcceptsAnsiCodes, bool _, uint? originalConsoleMode) = NativeMethods.QueryIsScreenAndTryEnableAnsiColorCodes(); + _originalConsoleMode = originalConsoleMode; + terminalWithProgress = consoleAcceptsAnsiCodes || _options.ForceAnsi is true + ? new TestProgressStateAwareTerminal(new AnsiTerminal(console, _options.BaseDirectory), showProgress, writeProgressImmediatelyAfterOutput: true, updateEvery: ansiUpdateCadenceInMs) + : new TestProgressStateAwareTerminal(new NonAnsiTerminal(console), showProgress, writeProgressImmediatelyAfterOutput: false, updateEvery: nonAnsiUpdateCadenceInMs); + } + + _terminalWithProgress = terminalWithProgress; + } + + public void TestExecutionStarted(DateTimeOffset testStartTime, int workerCount, bool isDiscovery) + { + _isDiscovery = isDiscovery; + _testExecutionStartTime = testStartTime; + _terminalWithProgress.StartShowingProgress(workerCount); + } + + public void AssemblyRunStarted(string assembly, string? targetFramework, string? architecture, string? executionId) + { + if (_options.ShowAssembly && _options.ShowAssemblyStartAndComplete) + { + _terminalWithProgress.WriteToTerminal(terminal => + { + terminal.Append(_isDiscovery ? LocalizableStrings.DiscoveringTestsFrom : LocalizableStrings.RunningTestsFrom); + terminal.Append(' '); + AppendAssemblyLinkTargetFrameworkAndArchitecture(terminal, assembly, targetFramework, architecture); + terminal.AppendLine(); + }); + } + + GetOrAddAssemblyRun(assembly, targetFramework, architecture, executionId); + } + + private TestProgressState GetOrAddAssemblyRun(string assembly, string? targetFramework, string? architecture, string? executionId) + { + string key = $"{assembly}|{targetFramework}|{architecture}|{executionId}"; + return _assemblies.GetOrAdd(key, _ => + { + IStopwatch sw = CreateStopwatch(); + var assemblyRun = new TestProgressState(Interlocked.Increment(ref _counter), assembly, targetFramework, architecture, sw); + int slotIndex = _terminalWithProgress.AddWorker(assemblyRun); + assemblyRun.SlotIndex = slotIndex; + + return assemblyRun; + }); + } + + public void TestExecutionCompleted(DateTimeOffset endTime) + { + _testExecutionEndTime = endTime; + _terminalWithProgress.StopShowingProgress(); + + _terminalWithProgress.WriteToTerminal(_isDiscovery ? AppendTestDiscoverySummary : AppendTestRunSummary); + + NativeMethods.RestoreConsoleMode(_originalConsoleMode); + _assemblies.Clear(); + _buildErrorsCount = 0; + _testExecutionStartTime = null; + _testExecutionEndTime = null; + } + + private void AppendTestRunSummary(ITerminal terminal) + { + terminal.AppendLine(); + + IEnumerable> artifactGroups = _artifacts.GroupBy(a => a.OutOfProcess); + if (artifactGroups.Any()) + { + terminal.AppendLine(); + } + + foreach (IGrouping artifactGroup in artifactGroups) + { + terminal.Append(SingleIndentation); + terminal.AppendLine(artifactGroup.Key ? LocalizableStrings.OutOfProcessArtifactsProduced : LocalizableStrings.InProcessArtifactsProduced); + foreach (TestRunArtifact artifact in artifactGroup) + { + terminal.Append(DoubleIndentation); + terminal.Append("- "); + if (!String.IsNullOrWhiteSpace(artifact.TestName)) + { + terminal.Append(LocalizableStrings.ForTest); + terminal.Append(" '"); + terminal.Append(artifact.TestName); + terminal.Append("': "); + } + + terminal.AppendLink(artifact.Path, lineNumber: null); + terminal.AppendLine(); + } + } + + int totalTests = _assemblies.Values.Sum(a => a.TotalTests); + int totalFailedTests = _assemblies.Values.Sum(a => a.FailedTests); + int totalSkippedTests = _assemblies.Values.Sum(a => a.SkippedTests); + + bool notEnoughTests = totalTests < _options.MinimumExpectedTests; + bool allTestsWereSkipped = totalTests == 0 || totalTests == totalSkippedTests; + bool anyTestFailed = totalFailedTests > 0; + bool runFailed = anyTestFailed || notEnoughTests || allTestsWereSkipped || _wasCancelled; + terminal.SetColor(runFailed ? TerminalColor.Red : TerminalColor.Green); + + terminal.Append(LocalizableStrings.TestRunSummary); + terminal.Append(' '); + + if (_wasCancelled) + { + terminal.Append(LocalizableStrings.Aborted); + } + else if (notEnoughTests) + { + terminal.Append(string.Format(CultureInfo.CurrentCulture, LocalizableStrings.MinimumExpectedTestsPolicyViolation, totalTests, _options.MinimumExpectedTests)); + } + else if (allTestsWereSkipped) + { + terminal.Append(LocalizableStrings.ZeroTestsRan); + } + else if (anyTestFailed) + { + terminal.Append(string.Format(CultureInfo.CurrentCulture, "{0}!", LocalizableStrings.Failed)); + } + else + { + terminal.Append(string.Format(CultureInfo.CurrentCulture, "{0}!", LocalizableStrings.Passed)); + } + + if (!_options.ShowAssembly && _assemblies.Count == 1) + { + TestProgressState testProgressState = _assemblies.Values.Single(); + terminal.SetColor(TerminalColor.DarkGray); + terminal.Append(" - "); + terminal.ResetColor(); + AppendAssemblyLinkTargetFrameworkAndArchitecture(terminal, testProgressState.Assembly, testProgressState.TargetFramework, testProgressState.Architecture); + } + + terminal.AppendLine(); + + if (_options.ShowAssembly && _assemblies.Count > 1) + { + foreach (TestProgressState assemblyRun in _assemblies.Values) + { + terminal.Append(SingleIndentation); + AppendAssemblySummary(assemblyRun, terminal); + terminal.AppendLine(); + } + + terminal.AppendLine(); + } + + int total = _assemblies.Values.Sum(t => t.TotalTests); + int failed = _assemblies.Values.Sum(t => t.FailedTests); + int passed = _assemblies.Values.Sum(t => t.PassedTests); + int skipped = _assemblies.Values.Sum(t => t.SkippedTests); + TimeSpan runDuration = _testExecutionStartTime != null && _testExecutionEndTime != null ? (_testExecutionEndTime - _testExecutionStartTime).Value : TimeSpan.Zero; + + bool colorizeFailed = failed > 0; + bool colorizePassed = passed > 0 && _buildErrorsCount == 0 && failed == 0; + bool colorizeSkipped = skipped > 0 && skipped == total && _buildErrorsCount == 0 && failed == 0; + + string totalText = $"{SingleIndentation}total: {total}"; + string failedText = $"{SingleIndentation}failed: {failed}"; + string passedText = $"{SingleIndentation}succeeded: {passed}"; + string skippedText = $"{SingleIndentation}skipped: {skipped}"; + string durationText = $"{SingleIndentation}duration: "; + + terminal.ResetColor(); + terminal.AppendLine(totalText); + if (colorizeFailed) + { + terminal.SetColor(TerminalColor.Red); + } + + terminal.AppendLine(failedText); + + if (colorizeFailed) + { + terminal.ResetColor(); + } + + if (colorizePassed) + { + terminal.SetColor(TerminalColor.Green); + } + + terminal.AppendLine(passedText); + + if (colorizePassed) + { + terminal.ResetColor(); + } + + if (colorizeSkipped) + { + terminal.SetColor(TerminalColor.Yellow); + } + + terminal.AppendLine(skippedText); + + if (colorizeSkipped) + { + terminal.ResetColor(); + } + + terminal.Append(durationText); + AppendLongDuration(terminal, runDuration, wrapInParentheses: false, colorize: false); + terminal.AppendLine(); + } + + /// + /// Print a build result summary to the output. + /// + private static void AppendAssemblyResult(ITerminal terminal, bool succeeded, int countErrors, int countWarnings) + { + if (!succeeded) + { + terminal.SetColor(TerminalColor.Red); + // If the build failed, we print one of three red strings. + string text = (countErrors > 0, countWarnings > 0) switch + { + (true, true) => string.Format(CultureInfo.CurrentCulture, LocalizableStrings.FailedWithErrorsAndWarnings, countErrors, countWarnings), + (true, _) => string.Format(CultureInfo.CurrentCulture, LocalizableStrings.FailedWithErrors, countErrors), + (false, true) => string.Format(CultureInfo.CurrentCulture, LocalizableStrings.FailedWithWarnings, countWarnings), + _ => LocalizableStrings.FailedLowercase, + }; + terminal.Append(text); + terminal.ResetColor(); + } + else if (countWarnings > 0) + { + terminal.SetColor(TerminalColor.Yellow); + terminal.Append($"succeeded with {countWarnings} warning(s)"); + terminal.ResetColor(); + } + else + { + terminal.SetColor(TerminalColor.Green); + terminal.Append(LocalizableStrings.PassedLowercase); + terminal.ResetColor(); + } + } + + internal void TestCompleted( + string assembly, + string? targetFramework, + string? architecture, + string? executionId, + string testNodeUid, + string displayName, + TestOutcome outcome, + TimeSpan duration, + string? errorMessage, + Exception? exception, + string? expected, + string? actual, + string? standardOutput, + string? errorOutput) + { + FlatException[] flatExceptions = ExceptionFlattener.Flatten(errorMessage, exception); + TestCompleted( + assembly, + targetFramework, + architecture, + executionId, + testNodeUid, + displayName, + outcome, + duration, + flatExceptions, + expected, + actual, + standardOutput, + errorOutput); + } + + internal void TestCompleted( + string assembly, + string? targetFramework, + string? architecture, + string? executionId, + string testNodeUid, + string displayName, + TestOutcome outcome, + TimeSpan duration, + FlatException[] exceptions, + string? expected, + string? actual, + string? standardOutput, + string? errorOutput) + { + TestProgressState asm = _assemblies[$"{assembly}|{targetFramework}|{architecture}|{executionId}"]; + + if (_options.ShowActiveTests) + { + asm.TestNodeResultsState?.RemoveRunningTestNode(testNodeUid); + } + + switch (outcome) + { + case TestOutcome.Error: + case TestOutcome.Timeout: + case TestOutcome.Canceled: + case TestOutcome.Fail: + asm.FailedTests++; + asm.TotalTests++; + break; + case TestOutcome.Passed: + asm.PassedTests++; + asm.TotalTests++; + break; + case TestOutcome.Skipped: + asm.SkippedTests++; + asm.TotalTests++; + break; + } + + _terminalWithProgress.UpdateWorker(asm.SlotIndex); + if (outcome != TestOutcome.Passed || GetShowPassedTests()) + { + _terminalWithProgress.WriteToTerminal(terminal => RenderTestCompleted( + terminal, + assembly, + targetFramework, + architecture, + displayName, + outcome, + duration, + exceptions, + expected, + actual, + standardOutput, + errorOutput)); + } + } + + private bool GetShowPassedTests() + { + _shouldShowPassedTests ??= _options.ShowPassedTests(); + return _shouldShowPassedTests.Value; + } + + internal /* for testing */ void RenderTestCompleted( + ITerminal terminal, + string assembly, + string? targetFramework, + string? architecture, + string displayName, + TestOutcome outcome, + TimeSpan duration, + FlatException[] flatExceptions, + string? expected, + string? actual, + string? standardOutput, + string? errorOutput) + { + if (outcome == TestOutcome.Passed && !GetShowPassedTests()) + { + return; + } + + TerminalColor color = outcome switch + { + TestOutcome.Error or TestOutcome.Fail or TestOutcome.Canceled or TestOutcome.Timeout => TerminalColor.Red, + TestOutcome.Skipped => TerminalColor.Yellow, + TestOutcome.Passed => TerminalColor.Green, + _ => throw new NotSupportedException(), + }; + string outcomeText = outcome switch + { + TestOutcome.Fail or TestOutcome.Error => LocalizableStrings.FailedLowercase, + TestOutcome.Skipped => LocalizableStrings.SkippedLowercase, + TestOutcome.Canceled or TestOutcome.Timeout => $"{LocalizableStrings.FailedLowercase} ({LocalizableStrings.CancelledLowercase})", + TestOutcome.Passed => LocalizableStrings.PassedLowercase, + _ => throw new NotSupportedException(), + }; + + terminal.SetColor(color); + terminal.Append(outcomeText); + terminal.ResetColor(); + terminal.Append(' '); + terminal.Append(displayName); + terminal.SetColor(TerminalColor.DarkGray); + terminal.Append(' '); + AppendLongDuration(terminal, duration); + if (_options.ShowAssembly) + { + terminal.AppendLine(); + terminal.Append(SingleIndentation); + terminal.Append(LocalizableStrings.FromFile); + terminal.Append(' '); + AppendAssemblyLinkTargetFrameworkAndArchitecture(terminal, assembly, targetFramework, architecture); + } + + terminal.AppendLine(); + + FormatErrorMessage(terminal, flatExceptions, outcome, 0); + FormatExpectedAndActual(terminal, expected, actual); + FormatStackTrace(terminal, flatExceptions, 0); + FormatInnerExceptions(terminal, flatExceptions); + FormatStandardAndErrorOutput(terminal, standardOutput, errorOutput); + } + + private static void FormatInnerExceptions(ITerminal terminal, FlatException[] exceptions) + { + if (exceptions is null || exceptions.Length == 0) + { + return; + } + + for (int i = 1; i < exceptions.Length; i++) + { + terminal.SetColor(TerminalColor.Red); + terminal.Append(SingleIndentation); + terminal.Append("--->"); + FormatErrorMessage(terminal, exceptions, TestOutcome.Error, i); + FormatStackTrace(terminal, exceptions, i); + } + } + + private static void FormatErrorMessage(ITerminal terminal, FlatException[] exceptions, TestOutcome outcome, int index) + { + string? firstErrorMessage = GetStringFromIndexOrDefault(exceptions, e => e.ErrorMessage, index); + string? firstErrorType = GetStringFromIndexOrDefault(exceptions, e => e.ErrorType, index); + string? firstStackTrace = GetStringFromIndexOrDefault(exceptions, e => e.StackTrace, index); + if (String.IsNullOrWhiteSpace(firstErrorMessage) && String.IsNullOrWhiteSpace(firstErrorType) && String.IsNullOrWhiteSpace(firstStackTrace)) + { + return; + } + + terminal.SetColor(TerminalColor.Red); + + if (firstStackTrace is null) + { + AppendIndentedLine(terminal, firstErrorMessage, SingleIndentation); + } + else if (outcome == TestOutcome.Fail) + { + // For failed tests, we don't prefix the message with the exception type because it is most likely an assertion specific exception like AssertionFailedException, and we prefer to show that without the exception type to avoid additional noise. + AppendIndentedLine(terminal, firstErrorMessage, SingleIndentation); + } + else + { + AppendIndentedLine(terminal, $"{firstErrorType}: {firstErrorMessage}", SingleIndentation); + } + + terminal.ResetColor(); + } + + private static string? GetStringFromIndexOrDefault(FlatException[] exceptions, Func property, int index) => + exceptions != null && exceptions.Length >= index + 1 ? property(exceptions[index]) : null; + + private static void FormatExpectedAndActual(ITerminal terminal, string? expected, string? actual) + { + if (String.IsNullOrWhiteSpace(expected) && String.IsNullOrWhiteSpace(actual)) + { + return; + } + + terminal.SetColor(TerminalColor.Red); + terminal.Append(SingleIndentation); + terminal.AppendLine(LocalizableStrings.Expected); + AppendIndentedLine(terminal, expected, DoubleIndentation); + terminal.Append(SingleIndentation); + terminal.AppendLine(LocalizableStrings.Actual); + AppendIndentedLine(terminal, actual, DoubleIndentation); + terminal.ResetColor(); + } + + private static void FormatStackTrace(ITerminal terminal, FlatException[] exceptions, int index) + { + string? stackTrace = GetStringFromIndexOrDefault(exceptions, e => e.StackTrace, index); + if (String.IsNullOrWhiteSpace(stackTrace)) + { + return; + } + + terminal.SetColor(TerminalColor.DarkGray); + + string[] lines = stackTrace.Split(NewLineStrings, StringSplitOptions.None); + foreach (string line in lines) + { + AppendStackFrame(terminal, line); + } + + terminal.ResetColor(); + } + + private static void FormatStandardAndErrorOutput(ITerminal terminal, string? standardOutput, string? standardError) + { + if (String.IsNullOrWhiteSpace(standardOutput) && String.IsNullOrWhiteSpace(standardError)) + { + return; + } + + terminal.SetColor(TerminalColor.DarkGray); + terminal.Append(SingleIndentation); + terminal.AppendLine(LocalizableStrings.StandardOutput); + string? standardOutputWithoutSpecialChars = NormalizeSpecialCharacters(standardOutput); + AppendIndentedLine(terminal, standardOutputWithoutSpecialChars, DoubleIndentation); + terminal.Append(SingleIndentation); + terminal.AppendLine(LocalizableStrings.StandardError); + string? standardErrorWithoutSpecialChars = NormalizeSpecialCharacters(standardError); + AppendIndentedLine(terminal, standardErrorWithoutSpecialChars, DoubleIndentation); + terminal.ResetColor(); + } + + private static void AppendAssemblyLinkTargetFrameworkAndArchitecture(ITerminal terminal, string assembly, string? targetFramework, string? architecture) + { + terminal.AppendLink(assembly, lineNumber: null); + if (targetFramework != null || architecture != null) + { + terminal.Append(" ("); + if (targetFramework != null) + { + terminal.Append(targetFramework); + terminal.Append('|'); + } + + if (architecture != null) + { + terminal.Append(architecture); + } + + terminal.Append(')'); + } + } + + internal /* for testing */ static void AppendStackFrame(ITerminal terminal, string stackTraceLine) + { + terminal.Append(DoubleIndentation); + Match match = GetFrameRegex().Match(stackTraceLine); + if (match.Success) + { + bool weHaveFilePathAndCodeLine = !String.IsNullOrWhiteSpace(match.Groups["code"].Value); + terminal.Append(LocalizableStrings.StackFrameAt); + terminal.Append(' '); + + if (weHaveFilePathAndCodeLine) + { + terminal.Append(match.Groups["code"].Value); + } + else + { + terminal.Append(match.Groups["code1"].Value); + } + + if (weHaveFilePathAndCodeLine) + { + terminal.Append(' '); + terminal.Append(LocalizableStrings.StackFrameIn); + terminal.Append(' '); + if (!String.IsNullOrWhiteSpace(match.Groups["file"].Value)) + { + int line = int.TryParse(match.Groups["line"].Value, out int value) ? value : 0; + terminal.AppendLink(match.Groups["file"].Value, line); + + // AppendLink finishes by resetting color + terminal.SetColor(TerminalColor.DarkGray); + } + } + + terminal.AppendLine(); + } + else + { + terminal.AppendLine(stackTraceLine); + } + } + + private static void AppendIndentedLine(ITerminal terminal, string? message, string indent) + { + if (String.IsNullOrWhiteSpace(message)) + { + return; + } + + if (!message.Contains('\n')) + { + terminal.Append(indent); + terminal.AppendLine(message); + return; + } + + string[] lines = message.Split(NewLineStrings, StringSplitOptions.None); + foreach (string line in lines) + { + // Here we could check if the messages are longer than then line, and reflow them so a long line is split into multiple + // and prepended by the respective indentation. + // But this does not play nicely with ANSI escape codes. And if you + // run in narrow terminal and then widen it the text does not reflow correctly. And you also have harder time copying + // values when the assertion message is longer. + terminal.Append(indent); + terminal.Append(line); + terminal.AppendLine(); + } + } + + internal void AssemblyRunCompleted(string assembly, string? targetFramework, string? architecture, string? executionId, + // These parameters are useful only for "remote" runs in dotnet test, where we are reporting on multiple processes. + // In single process run, like with testing platform .exe we report these via messages, and run exit. + int? exitCode, string? outputData, string? errorData) + { + TestProgressState assemblyRun = GetOrAddAssemblyRun(assembly, targetFramework, architecture, executionId); + assemblyRun.Stopwatch.Stop(); + + _terminalWithProgress.RemoveWorker(assemblyRun.SlotIndex); + + if (!_isDiscovery && _options.ShowAssembly && _options.ShowAssemblyStartAndComplete) + { + _terminalWithProgress.WriteToTerminal(terminal => AppendAssemblySummary(assemblyRun, terminal)); + } + + if (exitCode is null or 0) + { + // Report nothing, we don't want to report on success, because then we will also report on test-discovery etc. + return; + } + + _terminalWithProgress.WriteToTerminal(terminal => + { + AppendExecutableSummary(terminal, exitCode, outputData, errorData); + terminal.AppendLine(); + }); + } + + private static void AppendExecutableSummary(ITerminal terminal, int? exitCode, string? outputData, string? errorData) + { + terminal.AppendLine(); + terminal.Append(LocalizableStrings.ExitCode); + terminal.Append(": "); + terminal.AppendLine(exitCode?.ToString(CultureInfo.CurrentCulture) ?? ""); + terminal.Append(LocalizableStrings.StandardOutput); + terminal.AppendLine(":"); + terminal.AppendLine(String.IsNullOrWhiteSpace(outputData) ? string.Empty : outputData); + terminal.Append(LocalizableStrings.StandardError); + terminal.AppendLine(":"); + terminal.AppendLine(String.IsNullOrWhiteSpace(errorData) ? string.Empty : errorData); + } + + private static string? NormalizeSpecialCharacters(string? text) + => text?.Replace('\0', '\x2400') + // escape char + .Replace('\x001b', '\x241b'); + + private static void AppendAssemblySummary(TestProgressState assemblyRun, ITerminal terminal) + { + int failedTests = assemblyRun.FailedTests; + int warnings = 0; + + AppendAssemblyLinkTargetFrameworkAndArchitecture(terminal, assemblyRun.Assembly, assemblyRun.TargetFramework, assemblyRun.Architecture); + terminal.Append(' '); + AppendAssemblyResult(terminal, assemblyRun.FailedTests == 0, failedTests, warnings); + terminal.Append(' '); + AppendLongDuration(terminal, assemblyRun.Stopwatch.Elapsed); + } + + /// + /// Appends a long duration in human readable format such as 1h 23m 500ms. + /// + private static void AppendLongDuration(ITerminal terminal, TimeSpan duration, bool wrapInParentheses = true, bool colorize = true) + { + if (colorize) + { + terminal.SetColor(TerminalColor.DarkGray); + } + + HumanReadableDurationFormatter.Append(terminal, duration, wrapInParentheses); + + if (colorize) + { + terminal.ResetColor(); + } + } + + public void Dispose() => _terminalWithProgress.Dispose(); + + public void ArtifactAdded(bool outOfProcess, string? assembly, string? targetFramework, string? architecture, string? executionId, string? testName, string path) + => _artifacts.Add(new TestRunArtifact(outOfProcess, assembly, targetFramework, architecture, executionId, testName, path)); + + /// + /// Let the user know that cancellation was triggered. + /// + public void StartCancelling() + { + _wasCancelled = true; + _terminalWithProgress.WriteToTerminal(terminal => + { + terminal.AppendLine(); + terminal.AppendLine(LocalizableStrings.CancellingTestSession); + terminal.AppendLine(); + }); + } + + internal void WriteErrorMessage(string assembly, string? targetFramework, string? architecture, string? executionId, string text, int? padding) + { + TestProgressState asm = GetOrAddAssemblyRun(assembly, targetFramework, architecture, executionId); + asm.AddError(text); + + _terminalWithProgress.WriteToTerminal(terminal => + { + terminal.SetColor(TerminalColor.Red); + if (padding == null) + { + terminal.AppendLine(text); + } + else + { + AppendIndentedLine(terminal, text, new string(' ', padding.Value)); + } + + terminal.ResetColor(); + }); + } + + internal void WriteWarningMessage(string assembly, string? targetFramework, string? architecture, string? executionId, string text, int? padding) + { + TestProgressState asm = GetOrAddAssemblyRun(assembly, targetFramework, architecture, executionId); + asm.AddWarning(text); + _terminalWithProgress.WriteToTerminal(terminal => + { + terminal.SetColor(TerminalColor.Yellow); + if (padding == null) + { + terminal.AppendLine(text); + } + else + { + AppendIndentedLine(terminal, text, new string(' ', padding.Value)); + } + + terminal.ResetColor(); + }); + } + + internal void WriteErrorMessage(string assembly, string? targetFramework, string? architecture, string? executionId, Exception exception) + => WriteErrorMessage(assembly, targetFramework, architecture, executionId, exception.ToString(), padding: null); + + public void WriteMessage(string text, SystemConsoleColor? color = null, int? padding = null) + { + if (color != null) + { + _terminalWithProgress.WriteToTerminal(terminal => + { + terminal.SetColor(ToTerminalColor(color.ConsoleColor)); + if (padding == null) + { + terminal.AppendLine(text); + } + else + { + AppendIndentedLine(terminal, text, new string(' ', padding.Value)); + } + + terminal.ResetColor(); + }); + } + else + { + _terminalWithProgress.WriteToTerminal(terminal => + { + if (padding == null) + { + terminal.AppendLine(text); + } + else + { + AppendIndentedLine(terminal, text, new string(' ', padding.Value)); + } + }); + } + } + + internal void TestDiscovered( + string assembly, + string? targetFramework, + string? architecture, + string? executionId, + string? displayName, + string? uid) + { + TestProgressState asm = _assemblies[$"{assembly}|{targetFramework}|{architecture}|{executionId}"]; + + // TODO: add mode for discovered tests to the progress bar - jajares + asm.PassedTests++; + asm.TotalTests++; + asm.DiscoveredTests.Add(new(displayName, uid)); + _terminalWithProgress.UpdateWorker(asm.SlotIndex); + } + + public void AppendTestDiscoverySummary(ITerminal terminal) + { + terminal.AppendLine(); + + var assemblies = _assemblies.Select(asm => asm.Value).OrderBy(a => a.Assembly).Where(a => a is not null).ToList(); + + int totalTests = _assemblies.Values.Sum(a => a.TotalTests); + bool runFailed = _wasCancelled; + + foreach (TestProgressState assembly in assemblies) + { + terminal.Append(string.Format(CultureInfo.CurrentCulture, LocalizableStrings.DiscoveredTestsInAssembly, assembly.DiscoveredTests.Count)); + terminal.Append(" - "); + AppendAssemblyLinkTargetFrameworkAndArchitecture(terminal, assembly.Assembly, assembly.TargetFramework, assembly.Architecture); + terminal.AppendLine(); + foreach ((string? displayName, string? uid) in assembly.DiscoveredTests) + { + if (displayName is not null) + { + terminal.Append(SingleIndentation); + terminal.AppendLine(displayName); + } + } + + terminal.AppendLine(); + } + + terminal.SetColor(runFailed ? TerminalColor.Red : TerminalColor.Green); + if (assemblies.Count <= 1) + { + terminal.AppendLine(string.Format(CultureInfo.CurrentCulture, LocalizableStrings.TestDiscoverySummarySingular, totalTests)); + } + else + { + terminal.AppendLine(string.Format(CultureInfo.CurrentCulture, LocalizableStrings.TestDiscoverySummary, totalTests, assemblies.Count)); + } + + terminal.ResetColor(); + terminal.AppendLine(); + + if (_wasCancelled) + { + terminal.Append(LocalizableStrings.Aborted); + terminal.AppendLine(); + } + } + + public void AssemblyDiscoveryCompleted(int testCount) => + _terminalWithProgress.WriteToTerminal(terminal => terminal.Append($"Found {testCount} tests")); + + private static TerminalColor ToTerminalColor(ConsoleColor consoleColor) + => consoleColor switch + { + ConsoleColor.Black => TerminalColor.Black, + ConsoleColor.DarkBlue => TerminalColor.DarkBlue, + ConsoleColor.DarkGreen => TerminalColor.DarkGreen, + ConsoleColor.DarkCyan => TerminalColor.DarkCyan, + ConsoleColor.DarkRed => TerminalColor.DarkRed, + ConsoleColor.DarkMagenta => TerminalColor.DarkMagenta, + ConsoleColor.DarkYellow => TerminalColor.DarkYellow, + ConsoleColor.DarkGray => TerminalColor.DarkGray, + ConsoleColor.Gray => TerminalColor.Gray, + ConsoleColor.Blue => TerminalColor.Blue, + ConsoleColor.Green => TerminalColor.Green, + ConsoleColor.Cyan => TerminalColor.Cyan, + ConsoleColor.Red => TerminalColor.Red, + ConsoleColor.Magenta => TerminalColor.Magenta, + ConsoleColor.Yellow => TerminalColor.Yellow, + ConsoleColor.White => TerminalColor.White, + _ => TerminalColor.Default, + }; + + public void TestInProgress( + string assembly, + string? targetFramework, + string? architecture, + string testNodeUid, + string displayName, + string? executionId) + { + TestProgressState asm = _assemblies[$"{assembly}|{targetFramework}|{architecture}|{executionId}"]; + + if (_options.ShowActiveTests) + { + asm.TestNodeResultsState ??= new(Interlocked.Increment(ref _counter)); + asm.TestNodeResultsState.AddRunningTestNode( + Interlocked.Increment(ref _counter), testNodeUid, displayName, CreateStopwatch()); + } + + _terminalWithProgress.UpdateWorker(asm.SlotIndex); + } +} diff --git a/src/Cli/dotnet/commands/dotnet-test/Terminal/TerminalTestReporterOptions.cs b/src/Cli/dotnet/commands/dotnet-test/Terminal/TerminalTestReporterOptions.cs new file mode 100644 index 000000000000..478453758d30 --- /dev/null +++ b/src/Cli/dotnet/commands/dotnet-test/Terminal/TerminalTestReporterOptions.cs @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.Testing.Platform.OutputDevice.Terminal; + +internal sealed class TerminalTestReporterOptions +{ + /// + /// Gets path to which all other paths in output should be relative. + /// + public string? BaseDirectory { get; init; } + + /// + /// Gets a value indicating whether we should show passed tests. + /// + public Func ShowPassedTests { get; init; } = () => true; + + /// + /// Gets a value indicating whether we should show information about which assembly is the source of the data on screen. Turn this off when running directly from an exe to reduce noise, because the path will always be the same. + /// + public bool ShowAssembly { get; init; } + + /// + /// Gets a value indicating whether we should show information about which assembly started or completed. Turn this off when running directly from an exe to reduce noise, because the path will always be the same. + /// + public bool ShowAssemblyStartAndComplete { get; init; } + + /// + /// Gets minimum amount of tests to run. + /// + public int MinimumExpectedTests { get; init; } + + /// + /// Gets a value indicating whether we should write the progress periodically to screen. When ANSI is allowed we update the progress as often as we can. When ANSI is not allowed we update it every 3 seconds. + /// This is a callback to nullable bool, because we don't know if we are running as test host controller until after we setup the console. So we should be polling for the value, until we get non-null boolean + /// and then cache that value. + /// + public Func ShowProgress { get; init; } = () => true; + + /// + /// Gets a value indicating whether the active tests should be visible when the progress is shown. + /// + public bool ShowActiveTests { get; init; } + + /// + /// Gets a value indicating whether we should use ANSI escape codes or disable them. When true the capabilities of the console are autodetected. + /// + public bool UseAnsi { get; init; } + + /// + /// Gets a value indicating whether we should force ANSI escape codes. When true the ANSI is used without auto-detecting capabilities of the console. This is needed only for testing. + /// + internal /* for testing */ bool? ForceAnsi { get; init; } +} diff --git a/src/Cli/dotnet/commands/dotnet-test/Terminal/TestDetailState.cs b/src/Cli/dotnet/commands/dotnet-test/Terminal/TestDetailState.cs new file mode 100644 index 000000000000..ad00e3ee42af --- /dev/null +++ b/src/Cli/dotnet/commands/dotnet-test/Terminal/TestDetailState.cs @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.Testing.Platform.Helpers; + +namespace Microsoft.Testing.Platform.OutputDevice.Terminal; + +internal sealed class TestDetailState +{ + private string _text; + + public TestDetailState(long id, IStopwatch? stopwatch, string text) + { + Id = id; + Stopwatch = stopwatch; + _text = text; + } + + public long Id { get; } + + public long Version { get; set; } + + public IStopwatch? Stopwatch { get; } + + public string Text + { + get => _text; + set + { + if (!_text.Equals(value, StringComparison.Ordinal)) + { + Version++; + _text = value; + } + } + } +} diff --git a/src/Cli/dotnet/commands/dotnet-test/Terminal/TestNodeResultsState.cs b/src/Cli/dotnet/commands/dotnet-test/Terminal/TestNodeResultsState.cs new file mode 100644 index 000000000000..c7a6609e5410 --- /dev/null +++ b/src/Cli/dotnet/commands/dotnet-test/Terminal/TestNodeResultsState.cs @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Concurrent; +using System.Globalization; +using Microsoft.Testing.Platform.Helpers; +using LocalizableStrings = Microsoft.DotNet.Tools.Test.LocalizableStrings; + +namespace Microsoft.Testing.Platform.OutputDevice.Terminal; + +internal sealed class TestNodeResultsState +{ + public TestNodeResultsState(long id) + { + Id = id; + _summaryDetail = new(id, stopwatch: null, text: string.Empty); + } + + public long Id { get; } + + private readonly TestDetailState _summaryDetail; + private readonly ConcurrentDictionary _testNodeProgressStates = new(); + + public int Count => _testNodeProgressStates.Count; + + public void AddRunningTestNode(int id, string uid, string name, IStopwatch stopwatch) => _testNodeProgressStates[uid] = new TestDetailState(id, stopwatch, name); + + public void RemoveRunningTestNode(string uid) => _testNodeProgressStates.TryRemove(uid, out _); + + public IEnumerable GetRunningTasks(int maxCount) + { + var sortedDetails = _testNodeProgressStates + .Select(d => d.Value) + .OrderByDescending(d => d.Stopwatch?.Elapsed ?? TimeSpan.Zero) + .ToList(); + + bool tooManyItems = sortedDetails.Count > maxCount; + + if (tooManyItems) + { + // Note: If there's too many items to display, the summary will take up one line. + // As such, we can only take maxCount - 1 items. + int itemsToTake = maxCount - 1; + _summaryDetail.Text = + itemsToTake == 0 + // Note: If itemsToTake is 0, then we only show two lines, the project summary and the number of running tests. + ? string.Format(CultureInfo.CurrentCulture, LocalizableStrings.ActiveTestsRunning_FullTestsCount, sortedDetails.Count) + // If itemsToTake is larger, then we show the project summary, active tests, and the number of active tests that are not shown. + : $"... {string.Format(CultureInfo.CurrentCulture, LocalizableStrings.ActiveTestsRunning_MoreTestsCount, sortedDetails.Count - itemsToTake)}"; + sortedDetails = sortedDetails.Take(itemsToTake).ToList(); + } + + foreach (TestDetailState? detail in sortedDetails) + { + yield return detail; + } + + if (tooManyItems) + { + yield return _summaryDetail; + } + } +} diff --git a/src/Cli/dotnet/commands/dotnet-test/Terminal/TestOutcome.cs b/src/Cli/dotnet/commands/dotnet-test/Terminal/TestOutcome.cs new file mode 100644 index 000000000000..b60208af292e --- /dev/null +++ b/src/Cli/dotnet/commands/dotnet-test/Terminal/TestOutcome.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.Testing.Platform.OutputDevice.Terminal; + +/// +/// Outcome of a test. +/// +internal enum TestOutcome +{ + /// + /// Error. + /// + Error, + + /// + /// Fail. + /// + Fail, + + /// + /// Passed. + /// + Passed, + + /// + /// Skipped. + /// + Skipped, + + /// + /// Timeout. + /// + Timeout, + + /// + /// Canceled. + /// + Canceled, +} diff --git a/src/Cli/dotnet/commands/dotnet-test/Terminal/TestProgressState.cs b/src/Cli/dotnet/commands/dotnet-test/Terminal/TestProgressState.cs new file mode 100644 index 000000000000..7824e12bc0fa --- /dev/null +++ b/src/Cli/dotnet/commands/dotnet-test/Terminal/TestProgressState.cs @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.Testing.Platform.Helpers; + +namespace Microsoft.Testing.Platform.OutputDevice.Terminal; + +internal sealed class TestProgressState +{ + public TestProgressState(long id, string assembly, string? targetFramework, string? architecture, IStopwatch stopwatch) + { + Id = id; + Assembly = assembly; + TargetFramework = targetFramework; + Architecture = architecture; + Stopwatch = stopwatch; + AssemblyName = Path.GetFileName(assembly)!; + } + + public string Assembly { get; } + + public string AssemblyName { get; } + + public string? TargetFramework { get; } + + public string? Architecture { get; } + + public IStopwatch Stopwatch { get; } + + public List Attachments { get; } = new(); + + public List Messages { get; } = new(); + + public int FailedTests { get; internal set; } + + public int PassedTests { get; internal set; } + + public int SkippedTests { get; internal set; } + + public int TotalTests { get; internal set; } + + public TestNodeResultsState? TestNodeResultsState { get; internal set; } + + public int SlotIndex { get; internal set; } + + public long Id { get; internal set; } + + public long Version { get; internal set; } + + public List<(string? DisplayName, string? UID)> DiscoveredTests { get; internal set; } = new(); + + internal void AddError(string text) + => Messages.Add(new ErrorMessage(text)); + + internal void AddWarning(string text) + => Messages.Add(new WarningMessage(text)); +} diff --git a/src/Cli/dotnet/commands/dotnet-test/Terminal/TestProgressStateAwareTerminal.cs b/src/Cli/dotnet/commands/dotnet-test/Terminal/TestProgressStateAwareTerminal.cs new file mode 100644 index 000000000000..d369d99059ed --- /dev/null +++ b/src/Cli/dotnet/commands/dotnet-test/Terminal/TestProgressStateAwareTerminal.cs @@ -0,0 +1,205 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.Testing.Platform.OutputDevice.Terminal; + +/// +/// Terminal that updates the progress in place when progress reporting is enabled. +/// +internal sealed partial class TestProgressStateAwareTerminal : IDisposable +{ + /// + /// A cancellation token to signal the rendering thread that it should exit. + /// + private readonly CancellationTokenSource _cts = new(); + + /// + /// Protects access to state shared between the logger callbacks and the rendering thread. + /// + private readonly Lock _lock = new(); + + private readonly ITerminal _terminal; + private readonly Func _showProgress; + private readonly bool _writeProgressImmediatelyAfterOutput; + private readonly int _updateEvery; + private TestProgressState?[] _progressItems = []; + private bool? _showProgressCached; + + /// + /// The thread that performs periodic refresh of the console output. + /// + private Thread? _refresher; + private long _counter; + + public event EventHandler? OnProgressStartUpdate; + + public event EventHandler? OnProgressStopUpdate; + + /// + /// The thread proc. + /// + private void ThreadProc() + { + try + { + while (!_cts.Token.WaitHandle.WaitOne(_updateEvery)) + { + lock (_lock) + { + OnProgressStartUpdate?.Invoke(this, EventArgs.Empty); + _terminal.StartUpdate(); + try + { + _terminal.RenderProgress(_progressItems); + } + finally + { + _terminal.StopUpdate(); + OnProgressStopUpdate?.Invoke(this, EventArgs.Empty); + } + } + } + } + catch (ObjectDisposedException) + { + // When we dispose _cts too early this will throw. + } + + _terminal.EraseProgress(); + } + + public TestProgressStateAwareTerminal(ITerminal terminal, Func showProgress, bool writeProgressImmediatelyAfterOutput, int updateEvery) + { + _terminal = terminal; + _showProgress = showProgress; + _writeProgressImmediatelyAfterOutput = writeProgressImmediatelyAfterOutput; + _updateEvery = updateEvery; + } + + public int AddWorker(TestProgressState testWorker) + { + if (GetShowProgress()) + { + for (int i = 0; i < _progressItems.Length; i++) + { + if (_progressItems[i] == null) + { + _progressItems[i] = testWorker; + return i; + } + } + + throw new InvalidOperationException("No empty slot found"); + } + + return 0; + } + + public void StartShowingProgress(int workerCount) + { + if (GetShowProgress()) + { + _progressItems = new TestProgressState[workerCount]; + _terminal.StartBusyIndicator(); + // If we crash unexpectedly without completing this thread we don't want it to keep the process running. + _refresher = new Thread(ThreadProc) { IsBackground = true }; + _refresher.Start(); + } + } + + internal void StopShowingProgress() + { + if (GetShowProgress()) + { + _cts.Cancel(); + _refresher?.Join(); + + _terminal.EraseProgress(); + _terminal.StopBusyIndicator(); + } + } + + public void Dispose() => + ((IDisposable)_cts).Dispose(); + + internal void WriteToTerminal(Action write) + { + if (GetShowProgress()) + { + lock (_lock) + { + try + { + _terminal.StartUpdate(); + _terminal.EraseProgress(); + write(_terminal); + if (_writeProgressImmediatelyAfterOutput) + { + _terminal.RenderProgress(_progressItems); + } + } + finally + { + _terminal.StopUpdate(); + } + } + } + else + { + lock (_lock) + { + try + { + _terminal.StartUpdate(); + write(_terminal); + } + finally + { + _terminal.StopUpdate(); + } + } + } + } + + internal void RemoveWorker(int slotIndex) + { + if (GetShowProgress()) + { + _progressItems[slotIndex] = null; + } + } + + internal void UpdateWorker(int slotIndex) + { + if (GetShowProgress()) + { + // We increase the counter to say that this version of data is newer than what we had before and + // it should be completely re-rendered. Another approach would be to use timestamps, or to replace the + // instance and compare that, but that means more objects floating around. + _counter++; + + TestProgressState? progress = _progressItems[slotIndex]; + if (progress != null) + { + progress.Version = _counter; + } + } + } + + private bool GetShowProgress() + { + if (_showProgressCached != null) + { + return _showProgressCached.Value; + } + + // Get the value from the func until we get the first non-null value. + bool? showProgress = _showProgress(); + if (showProgress != null) + { + _showProgressCached = showProgress; + } + + return showProgress == true; + } +} diff --git a/src/Cli/dotnet/commands/dotnet-test/Terminal/TestRunArtifact.cs b/src/Cli/dotnet/commands/dotnet-test/Terminal/TestRunArtifact.cs new file mode 100644 index 000000000000..e8eba56edc34 --- /dev/null +++ b/src/Cli/dotnet/commands/dotnet-test/Terminal/TestRunArtifact.cs @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.Testing.Platform.OutputDevice.Terminal; + +/// +/// An artifact / attachment that was reported during run. +/// +internal sealed record TestRunArtifact(bool OutOfProcess, string? Assembly, string? TargetFramework, string? Architecture, string? ExecutionId, string? TestName, string Path); diff --git a/src/Cli/dotnet/commands/dotnet-test/Terminal/WarningMessage.cs b/src/Cli/dotnet/commands/dotnet-test/Terminal/WarningMessage.cs new file mode 100644 index 000000000000..c938898aa76a --- /dev/null +++ b/src/Cli/dotnet/commands/dotnet-test/Terminal/WarningMessage.cs @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.Testing.Platform.OutputDevice.Terminal; + +/// +/// A warning message that was sent during run. +/// +internal sealed record WarningMessage(string Text) : IProgressMessage; diff --git a/src/Cli/dotnet/commands/dotnet-test/xlf/LocalizableStrings.cs.xlf b/src/Cli/dotnet/commands/dotnet-test/xlf/LocalizableStrings.cs.xlf index 2ad19fb0b1b8..a6223eb47171 100644 --- a/src/Cli/dotnet/commands/dotnet-test/xlf/LocalizableStrings.cs.xlf +++ b/src/Cli/dotnet/commands/dotnet-test/xlf/LocalizableStrings.cs.xlf @@ -2,6 +2,26 @@ + + Aborted + Aborted + + + + {0} tests running + {0} tests running + + + + and {0} more + and {0} more + + + + Actual + Actual + + .NET Test Driver Testovací ovladač .NET @@ -12,6 +32,16 @@ Testovací ovladač pro platformu .NET + + canceled + canceled + + + + Canceling the test session... + Canceling the test session... + + The additional msbuild parameters to pass. Další parametry msbuild, které se mají předat. @@ -260,11 +290,71 @@ Pokud zadaný adresář neexistuje, bude vytvořen. Test application(s) that support VSTest are not supported. + + Console is already in batching mode. + Console is already in batching mode. + Exception that is thrown when console is already collecting input into a batch (into a string builder), and code asks to enable batching mode again. + DUMP_TYPE DUMP_TYPE + + Discovered {0} tests in assembly + Discovered {0} tests in assembly + 0 is count, the sentence is followed by the path of the assebly + + + Discovering tests from + Discovering tests from + + + + Exit code + Exit code + + + + Expected + Expected + + + + Failed + Failed + + + + failed + failed + + + + failed with {0} error(s) + failed with {0} error(s) + + + + failed with {0} error(s) and {1} warning(s) + failed with {0} error(s) and {1} warning(s) + + + + failed with {0} warning(s) + failed with {0} warning(s) + + + + For test + For test + is followed by test name + + + from + from + from followed by a file name to point to the file from which test is originating + DUMP_TYPE DUMP_TYPE @@ -280,6 +370,16 @@ Pokud zadaný adresář neexistuje, bude vytvořen. Následující argumenty se ignorovaly: {0} + + In process file artifacts produced: + In process file artifacts produced: + + + + Minimum expected tests policy violation, tests ran {0}, minimum expected {1} + Minimum expected tests policy violation, tests ran {0}, minimum expected {1} + {0}, {1} number of tests + No serializer registered with ID '{0}' Není zaregistrovaný žádný serializátor s ID {0}. @@ -290,6 +390,21 @@ Pokud zadaný adresář neexistuje, bude vytvořen. Není zaregistrovaný žádný serializátor s typem {0}. + + Out of process file artifacts produced: + Out of process file artifacts produced: + + + + Passed + Passed + + + + passed + passed + + @@ -307,6 +422,56 @@ Argumenty RunSettings: Příklad: dotnet test -- MSTest.DeploymentEnabled=false MSTest.MapInconclusiveToFailed=True + + Running tests from + Running tests from + + + + skipped + skipped + + + + at + at + at that is used for a stack frame location in a stack trace, is followed by a class and method name + + + in + in + in that is used in stack frame it is followed by file name + + + Error output + Error output + + + + Standard output + Standard output + + + + Discovered {0} tests in {1} assemblies. + Discovered {0} tests in {1} assemblies. + 0 is number of tests, 1 is count of assemblies + + + Discovered {0} tests. + Discovered {0} tests. + 0 is number of tests + + + Test run summary: + Test run summary: + + + + Zero tests ran + Zero tests ran + + DATA_COLLECTOR_NAME DATA_COLLECTOR_NAME diff --git a/src/Cli/dotnet/commands/dotnet-test/xlf/LocalizableStrings.de.xlf b/src/Cli/dotnet/commands/dotnet-test/xlf/LocalizableStrings.de.xlf index 230a26c4cdf8..3d6a1cee6b69 100644 --- a/src/Cli/dotnet/commands/dotnet-test/xlf/LocalizableStrings.de.xlf +++ b/src/Cli/dotnet/commands/dotnet-test/xlf/LocalizableStrings.de.xlf @@ -2,6 +2,26 @@ + + Aborted + Aborted + + + + {0} tests running + {0} tests running + + + + and {0} more + and {0} more + + + + Actual + Actual + + .NET Test Driver .NET-Testtreiber @@ -12,6 +32,16 @@ Testtreiber für die .NET-Plattform + + canceled + canceled + + + + Canceling the test session... + Canceling the test session... + + The additional msbuild parameters to pass. Die zusätzlichen MSBuild-Parameter, die weitergegeben werden sollen. @@ -260,11 +290,71 @@ Das angegebene Verzeichnis wird erstellt, wenn es nicht vorhanden ist. Test application(s) that support VSTest are not supported. + + Console is already in batching mode. + Console is already in batching mode. + Exception that is thrown when console is already collecting input into a batch (into a string builder), and code asks to enable batching mode again. + DUMP_TYPE DUMP_TYPE + + Discovered {0} tests in assembly + Discovered {0} tests in assembly + 0 is count, the sentence is followed by the path of the assebly + + + Discovering tests from + Discovering tests from + + + + Exit code + Exit code + + + + Expected + Expected + + + + Failed + Failed + + + + failed + failed + + + + failed with {0} error(s) + failed with {0} error(s) + + + + failed with {0} error(s) and {1} warning(s) + failed with {0} error(s) and {1} warning(s) + + + + failed with {0} warning(s) + failed with {0} warning(s) + + + + For test + For test + is followed by test name + + + from + from + from followed by a file name to point to the file from which test is originating + DUMP_TYPE DUMP_TYPE @@ -280,6 +370,16 @@ Das angegebene Verzeichnis wird erstellt, wenn es nicht vorhanden ist. Die folgenden Argumente wurden ignoriert: {0} + + In process file artifacts produced: + In process file artifacts produced: + + + + Minimum expected tests policy violation, tests ran {0}, minimum expected {1} + Minimum expected tests policy violation, tests ran {0}, minimum expected {1} + {0}, {1} number of tests + No serializer registered with ID '{0}' Es ist kein Serialisierungsmodul mit der ID "{0}" registriert @@ -290,6 +390,21 @@ Das angegebene Verzeichnis wird erstellt, wenn es nicht vorhanden ist. Es ist kein Serialisierungsmodul mit dem Typ "{0}" registriert + + Out of process file artifacts produced: + Out of process file artifacts produced: + + + + Passed + Passed + + + + passed + passed + + @@ -307,6 +422,56 @@ RunSettings-Argumente: Beispiel: "dotnet test -- MSTest.DeploymentEnabled=false MSTest.MapInconclusiveToFailed=True" + + Running tests from + Running tests from + + + + skipped + skipped + + + + at + at + at that is used for a stack frame location in a stack trace, is followed by a class and method name + + + in + in + in that is used in stack frame it is followed by file name + + + Error output + Error output + + + + Standard output + Standard output + + + + Discovered {0} tests in {1} assemblies. + Discovered {0} tests in {1} assemblies. + 0 is number of tests, 1 is count of assemblies + + + Discovered {0} tests. + Discovered {0} tests. + 0 is number of tests + + + Test run summary: + Test run summary: + + + + Zero tests ran + Zero tests ran + + DATA_COLLECTOR_NAME DATA_COLLECTOR_NAME diff --git a/src/Cli/dotnet/commands/dotnet-test/xlf/LocalizableStrings.es.xlf b/src/Cli/dotnet/commands/dotnet-test/xlf/LocalizableStrings.es.xlf index b4504cd41ca0..59c5a9f68690 100644 --- a/src/Cli/dotnet/commands/dotnet-test/xlf/LocalizableStrings.es.xlf +++ b/src/Cli/dotnet/commands/dotnet-test/xlf/LocalizableStrings.es.xlf @@ -2,6 +2,26 @@ + + Aborted + Aborted + + + + {0} tests running + {0} tests running + + + + and {0} more + and {0} more + + + + Actual + Actual + + .NET Test Driver Controlador de pruebas de .NET @@ -12,6 +32,16 @@ Controlador de pruebas para la plataforma .NET + + canceled + canceled + + + + Canceling the test session... + Canceling the test session... + + The additional msbuild parameters to pass. Parámetros adicionales de msbuild que se van a pasar. @@ -262,11 +292,71 @@ Si no existe, se creará el directorio especificado. Test application(s) that support VSTest are not supported. + + Console is already in batching mode. + Console is already in batching mode. + Exception that is thrown when console is already collecting input into a batch (into a string builder), and code asks to enable batching mode again. + DUMP_TYPE DUMP_TYPE + + Discovered {0} tests in assembly + Discovered {0} tests in assembly + 0 is count, the sentence is followed by the path of the assebly + + + Discovering tests from + Discovering tests from + + + + Exit code + Exit code + + + + Expected + Expected + + + + Failed + Failed + + + + failed + failed + + + + failed with {0} error(s) + failed with {0} error(s) + + + + failed with {0} error(s) and {1} warning(s) + failed with {0} error(s) and {1} warning(s) + + + + failed with {0} warning(s) + failed with {0} warning(s) + + + + For test + For test + is followed by test name + + + from + from + from followed by a file name to point to the file from which test is originating + DUMP_TYPE DUMP_TYPE @@ -282,6 +372,16 @@ Si no existe, se creará el directorio especificado. Se han omitido los argumentos siguientes: "{0}" + + In process file artifacts produced: + In process file artifacts produced: + + + + Minimum expected tests policy violation, tests ran {0}, minimum expected {1} + Minimum expected tests policy violation, tests ran {0}, minimum expected {1} + {0}, {1} number of tests + No serializer registered with ID '{0}' No hay ningún serializador registrado con el id. '{0}' @@ -292,6 +392,21 @@ Si no existe, se creará el directorio especificado. No hay ningún serializador registrado con el tipo '{0}' + + Out of process file artifacts produced: + Out of process file artifacts produced: + + + + Passed + Passed + + + + passed + passed + + @@ -309,6 +424,56 @@ Argumentos RunSettings: Ejemplo: dotnet test -- MSTest.DeploymentEnabled=false MSTest.MapInconclusiveToFailed=True + + Running tests from + Running tests from + + + + skipped + skipped + + + + at + at + at that is used for a stack frame location in a stack trace, is followed by a class and method name + + + in + in + in that is used in stack frame it is followed by file name + + + Error output + Error output + + + + Standard output + Standard output + + + + Discovered {0} tests in {1} assemblies. + Discovered {0} tests in {1} assemblies. + 0 is number of tests, 1 is count of assemblies + + + Discovered {0} tests. + Discovered {0} tests. + 0 is number of tests + + + Test run summary: + Test run summary: + + + + Zero tests ran + Zero tests ran + + DATA_COLLECTOR_NAME DATA_COLLECTOR_NAME diff --git a/src/Cli/dotnet/commands/dotnet-test/xlf/LocalizableStrings.fr.xlf b/src/Cli/dotnet/commands/dotnet-test/xlf/LocalizableStrings.fr.xlf index 080d51785fda..6380002a7245 100644 --- a/src/Cli/dotnet/commands/dotnet-test/xlf/LocalizableStrings.fr.xlf +++ b/src/Cli/dotnet/commands/dotnet-test/xlf/LocalizableStrings.fr.xlf @@ -2,6 +2,26 @@ + + Aborted + Aborted + + + + {0} tests running + {0} tests running + + + + and {0} more + and {0} more + + + + Actual + Actual + + .NET Test Driver Pilote de test .NET @@ -12,6 +32,16 @@ Pilote de test pour la plateforme .NET + + canceled + canceled + + + + Canceling the test session... + Canceling the test session... + + The additional msbuild parameters to pass. Paramètres msbuild supplémentaires à transmettre. @@ -260,11 +290,71 @@ Le répertoire spécifié est créé, s'il n'existe pas déjà. Test application(s) that support VSTest are not supported. + + Console is already in batching mode. + Console is already in batching mode. + Exception that is thrown when console is already collecting input into a batch (into a string builder), and code asks to enable batching mode again. + DUMP_TYPE DUMP_TYPE + + Discovered {0} tests in assembly + Discovered {0} tests in assembly + 0 is count, the sentence is followed by the path of the assebly + + + Discovering tests from + Discovering tests from + + + + Exit code + Exit code + + + + Expected + Expected + + + + Failed + Failed + + + + failed + failed + + + + failed with {0} error(s) + failed with {0} error(s) + + + + failed with {0} error(s) and {1} warning(s) + failed with {0} error(s) and {1} warning(s) + + + + failed with {0} warning(s) + failed with {0} warning(s) + + + + For test + For test + is followed by test name + + + from + from + from followed by a file name to point to the file from which test is originating + DUMP_TYPE DUMP_TYPE @@ -280,6 +370,16 @@ Le répertoire spécifié est créé, s'il n'existe pas déjà. Les arguments suivants ont été ignorés : "{0}" + + In process file artifacts produced: + In process file artifacts produced: + + + + Minimum expected tests policy violation, tests ran {0}, minimum expected {1} + Minimum expected tests policy violation, tests ran {0}, minimum expected {1} + {0}, {1} number of tests + No serializer registered with ID '{0}' Aucun sérialiseur inscrit avec l’ID « {0} » @@ -290,6 +390,21 @@ Le répertoire spécifié est créé, s'il n'existe pas déjà. Aucun sérialiseur inscrit avec le type « {0} » + + Out of process file artifacts produced: + Out of process file artifacts produced: + + + + Passed + Passed + + + + passed + passed + + @@ -307,6 +422,56 @@ Arguments de RunSettings : Exemple : dotnet test -- MSTest.DeploymentEnabled=false MSTest.MapInconclusiveToFailed=True + + Running tests from + Running tests from + + + + skipped + skipped + + + + at + at + at that is used for a stack frame location in a stack trace, is followed by a class and method name + + + in + in + in that is used in stack frame it is followed by file name + + + Error output + Error output + + + + Standard output + Standard output + + + + Discovered {0} tests in {1} assemblies. + Discovered {0} tests in {1} assemblies. + 0 is number of tests, 1 is count of assemblies + + + Discovered {0} tests. + Discovered {0} tests. + 0 is number of tests + + + Test run summary: + Test run summary: + + + + Zero tests ran + Zero tests ran + + DATA_COLLECTOR_NAME DATA_COLLECTOR_NAME diff --git a/src/Cli/dotnet/commands/dotnet-test/xlf/LocalizableStrings.it.xlf b/src/Cli/dotnet/commands/dotnet-test/xlf/LocalizableStrings.it.xlf index 31ea56d050fe..aeba5fa66868 100644 --- a/src/Cli/dotnet/commands/dotnet-test/xlf/LocalizableStrings.it.xlf +++ b/src/Cli/dotnet/commands/dotnet-test/xlf/LocalizableStrings.it.xlf @@ -2,6 +2,26 @@ + + Aborted + Aborted + + + + {0} tests running + {0} tests running + + + + and {0} more + and {0} more + + + + Actual + Actual + + .NET Test Driver Driver di test .NET @@ -12,6 +32,16 @@ Driver di test per la piattaforma .NET + + canceled + canceled + + + + Canceling the test session... + Canceling the test session... + + The additional msbuild parameters to pass. Parametri msbuild aggiuntivi da passare. @@ -260,11 +290,71 @@ Se non esiste, la directory specificata verrà creata. Test application(s) that support VSTest are not supported. + + Console is already in batching mode. + Console is already in batching mode. + Exception that is thrown when console is already collecting input into a batch (into a string builder), and code asks to enable batching mode again. + DUMP_TYPE DUMP_TYPE + + Discovered {0} tests in assembly + Discovered {0} tests in assembly + 0 is count, the sentence is followed by the path of the assebly + + + Discovering tests from + Discovering tests from + + + + Exit code + Exit code + + + + Expected + Expected + + + + Failed + Failed + + + + failed + failed + + + + failed with {0} error(s) + failed with {0} error(s) + + + + failed with {0} error(s) and {1} warning(s) + failed with {0} error(s) and {1} warning(s) + + + + failed with {0} warning(s) + failed with {0} warning(s) + + + + For test + For test + is followed by test name + + + from + from + from followed by a file name to point to the file from which test is originating + DUMP_TYPE DUMP_TYPE @@ -280,6 +370,16 @@ Se non esiste, la directory specificata verrà creata. Gli argomenti seguenti sono stati ignorati: "{0}" + + In process file artifacts produced: + In process file artifacts produced: + + + + Minimum expected tests policy violation, tests ran {0}, minimum expected {1} + Minimum expected tests policy violation, tests ran {0}, minimum expected {1} + {0}, {1} number of tests + No serializer registered with ID '{0}' Nessun serializzatore registrato con ID '{0}' @@ -290,6 +390,21 @@ Se non esiste, la directory specificata verrà creata. Nessun serializzatore registrato con tipo '{0}' + + Out of process file artifacts produced: + Out of process file artifacts produced: + + + + Passed + Passed + + + + passed + passed + + @@ -307,6 +422,56 @@ Argomenti di RunSettings: Esempio: dotnet test -- MSTest.DeploymentEnabled=false MSTest.MapInconclusiveToFailed=True + + Running tests from + Running tests from + + + + skipped + skipped + + + + at + at + at that is used for a stack frame location in a stack trace, is followed by a class and method name + + + in + in + in that is used in stack frame it is followed by file name + + + Error output + Error output + + + + Standard output + Standard output + + + + Discovered {0} tests in {1} assemblies. + Discovered {0} tests in {1} assemblies. + 0 is number of tests, 1 is count of assemblies + + + Discovered {0} tests. + Discovered {0} tests. + 0 is number of tests + + + Test run summary: + Test run summary: + + + + Zero tests ran + Zero tests ran + + DATA_COLLECTOR_NAME DATA_COLLECTOR_NAME diff --git a/src/Cli/dotnet/commands/dotnet-test/xlf/LocalizableStrings.ja.xlf b/src/Cli/dotnet/commands/dotnet-test/xlf/LocalizableStrings.ja.xlf index cc1420582f48..79dd2f6c3916 100644 --- a/src/Cli/dotnet/commands/dotnet-test/xlf/LocalizableStrings.ja.xlf +++ b/src/Cli/dotnet/commands/dotnet-test/xlf/LocalizableStrings.ja.xlf @@ -2,6 +2,26 @@ + + Aborted + Aborted + + + + {0} tests running + {0} tests running + + + + and {0} more + and {0} more + + + + Actual + Actual + + .NET Test Driver .NET Test Driver @@ -12,6 +32,16 @@ .NET Platform 用テスト ドライバー + + canceled + canceled + + + + Canceling the test session... + Canceling the test session... + + The additional msbuild parameters to pass. 渡す追加の msbuild パラメーター。 @@ -260,11 +290,71 @@ The specified directory will be created if it does not exist. Test application(s) that support VSTest are not supported. + + Console is already in batching mode. + Console is already in batching mode. + Exception that is thrown when console is already collecting input into a batch (into a string builder), and code asks to enable batching mode again. + DUMP_TYPE DUMP_TYPE + + Discovered {0} tests in assembly + Discovered {0} tests in assembly + 0 is count, the sentence is followed by the path of the assebly + + + Discovering tests from + Discovering tests from + + + + Exit code + Exit code + + + + Expected + Expected + + + + Failed + Failed + + + + failed + failed + + + + failed with {0} error(s) + failed with {0} error(s) + + + + failed with {0} error(s) and {1} warning(s) + failed with {0} error(s) and {1} warning(s) + + + + failed with {0} warning(s) + failed with {0} warning(s) + + + + For test + For test + is followed by test name + + + from + from + from followed by a file name to point to the file from which test is originating + DUMP_TYPE DUMP_TYPE @@ -280,6 +370,16 @@ The specified directory will be created if it does not exist. 次の引数は無視されました: "{0}" + + In process file artifacts produced: + In process file artifacts produced: + + + + Minimum expected tests policy violation, tests ran {0}, minimum expected {1} + Minimum expected tests policy violation, tests ran {0}, minimum expected {1} + {0}, {1} number of tests + No serializer registered with ID '{0}' ID '{0}' で登録されたシリアライザーがありません @@ -290,6 +390,21 @@ The specified directory will be created if it does not exist. 型 '{0}' で登録されたシリアライザーがありません + + Out of process file artifacts produced: + Out of process file artifacts produced: + + + + Passed + Passed + + + + passed + passed + + @@ -307,6 +422,56 @@ RunSettings 引数: 例: dotnet test -- MSTest.DeploymentEnabled=false MSTest.MapInconclusiveToFailed=True + + Running tests from + Running tests from + + + + skipped + skipped + + + + at + at + at that is used for a stack frame location in a stack trace, is followed by a class and method name + + + in + in + in that is used in stack frame it is followed by file name + + + Error output + Error output + + + + Standard output + Standard output + + + + Discovered {0} tests in {1} assemblies. + Discovered {0} tests in {1} assemblies. + 0 is number of tests, 1 is count of assemblies + + + Discovered {0} tests. + Discovered {0} tests. + 0 is number of tests + + + Test run summary: + Test run summary: + + + + Zero tests ran + Zero tests ran + + DATA_COLLECTOR_NAME DATA_COLLECTOR_NAME diff --git a/src/Cli/dotnet/commands/dotnet-test/xlf/LocalizableStrings.ko.xlf b/src/Cli/dotnet/commands/dotnet-test/xlf/LocalizableStrings.ko.xlf index 37bb82ef49af..d62f9b45676d 100644 --- a/src/Cli/dotnet/commands/dotnet-test/xlf/LocalizableStrings.ko.xlf +++ b/src/Cli/dotnet/commands/dotnet-test/xlf/LocalizableStrings.ko.xlf @@ -2,6 +2,26 @@ + + Aborted + Aborted + + + + {0} tests running + {0} tests running + + + + and {0} more + and {0} more + + + + Actual + Actual + + .NET Test Driver .NET 테스트 드라이버 @@ -12,6 +32,16 @@ .NET 플랫폼용 테스트 드라이버입니다. + + canceled + canceled + + + + Canceling the test session... + Canceling the test session... + + The additional msbuild parameters to pass. 전달할 추가 msbuild 매개 변수입니다. @@ -260,11 +290,71 @@ The specified directory will be created if it does not exist. Test application(s) that support VSTest are not supported. + + Console is already in batching mode. + Console is already in batching mode. + Exception that is thrown when console is already collecting input into a batch (into a string builder), and code asks to enable batching mode again. + DUMP_TYPE DUMP_TYPE + + Discovered {0} tests in assembly + Discovered {0} tests in assembly + 0 is count, the sentence is followed by the path of the assebly + + + Discovering tests from + Discovering tests from + + + + Exit code + Exit code + + + + Expected + Expected + + + + Failed + Failed + + + + failed + failed + + + + failed with {0} error(s) + failed with {0} error(s) + + + + failed with {0} error(s) and {1} warning(s) + failed with {0} error(s) and {1} warning(s) + + + + failed with {0} warning(s) + failed with {0} warning(s) + + + + For test + For test + is followed by test name + + + from + from + from followed by a file name to point to the file from which test is originating + DUMP_TYPE DUMP_TYPE @@ -280,6 +370,16 @@ The specified directory will be created if it does not exist. "{0}" 인수가 무시되었습니다. + + In process file artifacts produced: + In process file artifacts produced: + + + + Minimum expected tests policy violation, tests ran {0}, minimum expected {1} + Minimum expected tests policy violation, tests ran {0}, minimum expected {1} + {0}, {1} number of tests + No serializer registered with ID '{0}' ID '{0}'(으)로 등록된 직렬 변환기가 없습니다. @@ -290,6 +390,21 @@ The specified directory will be created if it does not exist. '{0}' 형식으로 등록된 직렬 변환기가 없습니다. + + Out of process file artifacts produced: + Out of process file artifacts produced: + + + + Passed + Passed + + + + passed + passed + + @@ -307,6 +422,56 @@ RunSettings 인수: 예: dotnet test -- MSTest.DeploymentEnabled=false MSTest.MapInconclusiveToFailed=True + + Running tests from + Running tests from + + + + skipped + skipped + + + + at + at + at that is used for a stack frame location in a stack trace, is followed by a class and method name + + + in + in + in that is used in stack frame it is followed by file name + + + Error output + Error output + + + + Standard output + Standard output + + + + Discovered {0} tests in {1} assemblies. + Discovered {0} tests in {1} assemblies. + 0 is number of tests, 1 is count of assemblies + + + Discovered {0} tests. + Discovered {0} tests. + 0 is number of tests + + + Test run summary: + Test run summary: + + + + Zero tests ran + Zero tests ran + + DATA_COLLECTOR_NAME DATA_COLLECTOR_NAME diff --git a/src/Cli/dotnet/commands/dotnet-test/xlf/LocalizableStrings.pl.xlf b/src/Cli/dotnet/commands/dotnet-test/xlf/LocalizableStrings.pl.xlf index 8d40d724227b..a1502c36aafb 100644 --- a/src/Cli/dotnet/commands/dotnet-test/xlf/LocalizableStrings.pl.xlf +++ b/src/Cli/dotnet/commands/dotnet-test/xlf/LocalizableStrings.pl.xlf @@ -2,6 +2,26 @@ + + Aborted + Aborted + + + + {0} tests running + {0} tests running + + + + and {0} more + and {0} more + + + + Actual + Actual + + .NET Test Driver Sterownik testów platformy .NET @@ -12,6 +32,16 @@ Sterownik testów dla platformy .NET + + canceled + canceled + + + + Canceling the test session... + Canceling the test session... + + The additional msbuild parameters to pass. Dodatkowe parametry programu MSBuild do przekazania. @@ -260,11 +290,71 @@ Jeśli określony katalog nie istnieje, zostanie utworzony. Test application(s) that support VSTest are not supported. + + Console is already in batching mode. + Console is already in batching mode. + Exception that is thrown when console is already collecting input into a batch (into a string builder), and code asks to enable batching mode again. + DUMP_TYPE DUMP_TYPE + + Discovered {0} tests in assembly + Discovered {0} tests in assembly + 0 is count, the sentence is followed by the path of the assebly + + + Discovering tests from + Discovering tests from + + + + Exit code + Exit code + + + + Expected + Expected + + + + Failed + Failed + + + + failed + failed + + + + failed with {0} error(s) + failed with {0} error(s) + + + + failed with {0} error(s) and {1} warning(s) + failed with {0} error(s) and {1} warning(s) + + + + failed with {0} warning(s) + failed with {0} warning(s) + + + + For test + For test + is followed by test name + + + from + from + from followed by a file name to point to the file from which test is originating + DUMP_TYPE DUMP_TYPE @@ -280,6 +370,16 @@ Jeśli określony katalog nie istnieje, zostanie utworzony. Następujące argumenty zostały zignorowane: „{0}” + + In process file artifacts produced: + In process file artifacts produced: + + + + Minimum expected tests policy violation, tests ran {0}, minimum expected {1} + Minimum expected tests policy violation, tests ran {0}, minimum expected {1} + {0}, {1} number of tests + No serializer registered with ID '{0}' Nie zarejestrowano serializatora z identyfikatorem „{0}” @@ -290,6 +390,21 @@ Jeśli określony katalog nie istnieje, zostanie utworzony. Nie zarejestrowano serializatora z typem „{0}” + + Out of process file artifacts produced: + Out of process file artifacts produced: + + + + Passed + Passed + + + + passed + passed + + @@ -307,6 +422,56 @@ Argumenty RunSettings: Przykład: dotnet test -- MSTest.DeploymentEnabled=false MSTest.MapInconclusiveToFailed=True + + Running tests from + Running tests from + + + + skipped + skipped + + + + at + at + at that is used for a stack frame location in a stack trace, is followed by a class and method name + + + in + in + in that is used in stack frame it is followed by file name + + + Error output + Error output + + + + Standard output + Standard output + + + + Discovered {0} tests in {1} assemblies. + Discovered {0} tests in {1} assemblies. + 0 is number of tests, 1 is count of assemblies + + + Discovered {0} tests. + Discovered {0} tests. + 0 is number of tests + + + Test run summary: + Test run summary: + + + + Zero tests ran + Zero tests ran + + DATA_COLLECTOR_NAME DATA_COLLECTOR_NAME diff --git a/src/Cli/dotnet/commands/dotnet-test/xlf/LocalizableStrings.pt-BR.xlf b/src/Cli/dotnet/commands/dotnet-test/xlf/LocalizableStrings.pt-BR.xlf index 5eaa4668710e..1c0d11326e41 100644 --- a/src/Cli/dotnet/commands/dotnet-test/xlf/LocalizableStrings.pt-BR.xlf +++ b/src/Cli/dotnet/commands/dotnet-test/xlf/LocalizableStrings.pt-BR.xlf @@ -2,6 +2,26 @@ + + Aborted + Aborted + + + + {0} tests running + {0} tests running + + + + and {0} more + and {0} more + + + + Actual + Actual + + .NET Test Driver Driver de Teste do .NET @@ -12,6 +32,16 @@ Driver de Teste para a Plataforma .NET + + canceled + canceled + + + + Canceling the test session... + Canceling the test session... + + The additional msbuild parameters to pass. Os parâmetros msbuild adicionais a serem aprovados. @@ -260,11 +290,71 @@ O diretório especificado será criado se ele ainda não existir. Test application(s) that support VSTest are not supported. + + Console is already in batching mode. + Console is already in batching mode. + Exception that is thrown when console is already collecting input into a batch (into a string builder), and code asks to enable batching mode again. + DUMP_TYPE DUMP_TYPE + + Discovered {0} tests in assembly + Discovered {0} tests in assembly + 0 is count, the sentence is followed by the path of the assebly + + + Discovering tests from + Discovering tests from + + + + Exit code + Exit code + + + + Expected + Expected + + + + Failed + Failed + + + + failed + failed + + + + failed with {0} error(s) + failed with {0} error(s) + + + + failed with {0} error(s) and {1} warning(s) + failed with {0} error(s) and {1} warning(s) + + + + failed with {0} warning(s) + failed with {0} warning(s) + + + + For test + For test + is followed by test name + + + from + from + from followed by a file name to point to the file from which test is originating + DUMP_TYPE DUMP_TYPE @@ -280,6 +370,16 @@ O diretório especificado será criado se ele ainda não existir. Os argumentos a seguir foram ignorados: "{0}" + + In process file artifacts produced: + In process file artifacts produced: + + + + Minimum expected tests policy violation, tests ran {0}, minimum expected {1} + Minimum expected tests policy violation, tests ran {0}, minimum expected {1} + {0}, {1} number of tests + No serializer registered with ID '{0}' Nenhum serializador registrado com a ID '{0}' @@ -290,6 +390,21 @@ O diretório especificado será criado se ele ainda não existir. Nenhum serializador registrado com o tipo '{0}' + + Out of process file artifacts produced: + Out of process file artifacts produced: + + + + Passed + Passed + + + + passed + passed + + @@ -307,6 +422,56 @@ Argumentos de RunSettings: Exemplo: dotnet test -- MSTest.DeploymentEnabled=false MSTest.MapInconclusiveToFailed=True + + Running tests from + Running tests from + + + + skipped + skipped + + + + at + at + at that is used for a stack frame location in a stack trace, is followed by a class and method name + + + in + in + in that is used in stack frame it is followed by file name + + + Error output + Error output + + + + Standard output + Standard output + + + + Discovered {0} tests in {1} assemblies. + Discovered {0} tests in {1} assemblies. + 0 is number of tests, 1 is count of assemblies + + + Discovered {0} tests. + Discovered {0} tests. + 0 is number of tests + + + Test run summary: + Test run summary: + + + + Zero tests ran + Zero tests ran + + DATA_COLLECTOR_NAME DATA_COLLECTOR_NAME diff --git a/src/Cli/dotnet/commands/dotnet-test/xlf/LocalizableStrings.ru.xlf b/src/Cli/dotnet/commands/dotnet-test/xlf/LocalizableStrings.ru.xlf index a9669d9915c4..9bf7317c7e0b 100644 --- a/src/Cli/dotnet/commands/dotnet-test/xlf/LocalizableStrings.ru.xlf +++ b/src/Cli/dotnet/commands/dotnet-test/xlf/LocalizableStrings.ru.xlf @@ -2,6 +2,26 @@ + + Aborted + Aborted + + + + {0} tests running + {0} tests running + + + + and {0} more + and {0} more + + + + Actual + Actual + + .NET Test Driver Драйвер тестов .NET @@ -12,6 +32,16 @@ Драйвер тестов для платформы .NET + + canceled + canceled + + + + Canceling the test session... + Canceling the test session... + + The additional msbuild parameters to pass. Дополнительные передаваемые параметры msbuild. @@ -260,11 +290,71 @@ The specified directory will be created if it does not exist. Test application(s) that support VSTest are not supported. + + Console is already in batching mode. + Console is already in batching mode. + Exception that is thrown when console is already collecting input into a batch (into a string builder), and code asks to enable batching mode again. + DUMP_TYPE DUMP_TYPE + + Discovered {0} tests in assembly + Discovered {0} tests in assembly + 0 is count, the sentence is followed by the path of the assebly + + + Discovering tests from + Discovering tests from + + + + Exit code + Exit code + + + + Expected + Expected + + + + Failed + Failed + + + + failed + failed + + + + failed with {0} error(s) + failed with {0} error(s) + + + + failed with {0} error(s) and {1} warning(s) + failed with {0} error(s) and {1} warning(s) + + + + failed with {0} warning(s) + failed with {0} warning(s) + + + + For test + For test + is followed by test name + + + from + from + from followed by a file name to point to the file from which test is originating + DUMP_TYPE DUMP_TYPE @@ -280,6 +370,16 @@ The specified directory will be created if it does not exist. Следующие аргументы пропущены: "{0}" + + In process file artifacts produced: + In process file artifacts produced: + + + + Minimum expected tests policy violation, tests ran {0}, minimum expected {1} + Minimum expected tests policy violation, tests ran {0}, minimum expected {1} + {0}, {1} number of tests + No serializer registered with ID '{0}' Не зарегистрирован сериализатор с ИД "{0}" @@ -290,6 +390,21 @@ The specified directory will be created if it does not exist. Не зарегистрирован сериализатор с типом "{0}" + + Out of process file artifacts produced: + Out of process file artifacts produced: + + + + Passed + Passed + + + + passed + passed + + @@ -307,6 +422,56 @@ RunSettings arguments: Пример: dotnet test -- MSTest.DeploymentEnabled=false MSTest.MapInconclusiveToFailed=True + + Running tests from + Running tests from + + + + skipped + skipped + + + + at + at + at that is used for a stack frame location in a stack trace, is followed by a class and method name + + + in + in + in that is used in stack frame it is followed by file name + + + Error output + Error output + + + + Standard output + Standard output + + + + Discovered {0} tests in {1} assemblies. + Discovered {0} tests in {1} assemblies. + 0 is number of tests, 1 is count of assemblies + + + Discovered {0} tests. + Discovered {0} tests. + 0 is number of tests + + + Test run summary: + Test run summary: + + + + Zero tests ran + Zero tests ran + + DATA_COLLECTOR_NAME DATA_COLLECTOR_NAME diff --git a/src/Cli/dotnet/commands/dotnet-test/xlf/LocalizableStrings.tr.xlf b/src/Cli/dotnet/commands/dotnet-test/xlf/LocalizableStrings.tr.xlf index 68bcd480e31b..6ea2479602ae 100644 --- a/src/Cli/dotnet/commands/dotnet-test/xlf/LocalizableStrings.tr.xlf +++ b/src/Cli/dotnet/commands/dotnet-test/xlf/LocalizableStrings.tr.xlf @@ -2,6 +2,26 @@ + + Aborted + Aborted + + + + {0} tests running + {0} tests running + + + + and {0} more + and {0} more + + + + Actual + Actual + + .NET Test Driver .NET Test Sürücüsü @@ -12,6 +32,16 @@ .NET Platformunun Test Sürücüsü + + canceled + canceled + + + + Canceling the test session... + Canceling the test session... + + The additional msbuild parameters to pass. Geçirilecek ek msbuild parametreleri. @@ -260,11 +290,71 @@ Belirtilen dizin yoksa oluşturulur. Test application(s) that support VSTest are not supported. + + Console is already in batching mode. + Console is already in batching mode. + Exception that is thrown when console is already collecting input into a batch (into a string builder), and code asks to enable batching mode again. + DUMP_TYPE DÖKÜM_TÜRÜ + + Discovered {0} tests in assembly + Discovered {0} tests in assembly + 0 is count, the sentence is followed by the path of the assebly + + + Discovering tests from + Discovering tests from + + + + Exit code + Exit code + + + + Expected + Expected + + + + Failed + Failed + + + + failed + failed + + + + failed with {0} error(s) + failed with {0} error(s) + + + + failed with {0} error(s) and {1} warning(s) + failed with {0} error(s) and {1} warning(s) + + + + failed with {0} warning(s) + failed with {0} warning(s) + + + + For test + For test + is followed by test name + + + from + from + from followed by a file name to point to the file from which test is originating + DUMP_TYPE DÖKÜM_TÜRÜ @@ -280,6 +370,16 @@ Belirtilen dizin yoksa oluşturulur. Şu bağımsız değişkenler yoksayıldı: "{0}" + + In process file artifacts produced: + In process file artifacts produced: + + + + Minimum expected tests policy violation, tests ran {0}, minimum expected {1} + Minimum expected tests policy violation, tests ran {0}, minimum expected {1} + {0}, {1} number of tests + No serializer registered with ID '{0}' '{0}' kimliğiyle kayıtlı seri hale getirici yok @@ -290,6 +390,21 @@ Belirtilen dizin yoksa oluşturulur. '{0}' türüyle kayıtlı seri hale getirici yok + + Out of process file artifacts produced: + Out of process file artifacts produced: + + + + Passed + Passed + + + + passed + passed + + @@ -307,6 +422,56 @@ RunSettings bağımsız değişkenleri: Örnek: dotnet test -- MSTest.DeploymentEnabled=false MSTest.MapInconclusiveToFailed=True + + Running tests from + Running tests from + + + + skipped + skipped + + + + at + at + at that is used for a stack frame location in a stack trace, is followed by a class and method name + + + in + in + in that is used in stack frame it is followed by file name + + + Error output + Error output + + + + Standard output + Standard output + + + + Discovered {0} tests in {1} assemblies. + Discovered {0} tests in {1} assemblies. + 0 is number of tests, 1 is count of assemblies + + + Discovered {0} tests. + Discovered {0} tests. + 0 is number of tests + + + Test run summary: + Test run summary: + + + + Zero tests ran + Zero tests ran + + DATA_COLLECTOR_NAME DATA_COLLECTOR_NAME diff --git a/src/Cli/dotnet/commands/dotnet-test/xlf/LocalizableStrings.zh-Hans.xlf b/src/Cli/dotnet/commands/dotnet-test/xlf/LocalizableStrings.zh-Hans.xlf index e7ac40be829b..7a61d54d86e8 100644 --- a/src/Cli/dotnet/commands/dotnet-test/xlf/LocalizableStrings.zh-Hans.xlf +++ b/src/Cli/dotnet/commands/dotnet-test/xlf/LocalizableStrings.zh-Hans.xlf @@ -2,6 +2,26 @@ + + Aborted + Aborted + + + + {0} tests running + {0} tests running + + + + and {0} more + and {0} more + + + + Actual + Actual + + .NET Test Driver .NET 测试驱动程序 @@ -12,6 +32,16 @@ 适用于 .NET 平台的测试驱动程序 + + canceled + canceled + + + + Canceling the test session... + Canceling the test session... + + The additional msbuild parameters to pass. 要传递的其他 msbuild 参数。 @@ -260,11 +290,71 @@ The specified directory will be created if it does not exist. Test application(s) that support VSTest are not supported. + + Console is already in batching mode. + Console is already in batching mode. + Exception that is thrown when console is already collecting input into a batch (into a string builder), and code asks to enable batching mode again. + DUMP_TYPE DUMP_TYPE + + Discovered {0} tests in assembly + Discovered {0} tests in assembly + 0 is count, the sentence is followed by the path of the assebly + + + Discovering tests from + Discovering tests from + + + + Exit code + Exit code + + + + Expected + Expected + + + + Failed + Failed + + + + failed + failed + + + + failed with {0} error(s) + failed with {0} error(s) + + + + failed with {0} error(s) and {1} warning(s) + failed with {0} error(s) and {1} warning(s) + + + + failed with {0} warning(s) + failed with {0} warning(s) + + + + For test + For test + is followed by test name + + + from + from + from followed by a file name to point to the file from which test is originating + DUMP_TYPE DUMP_TYPE @@ -280,6 +370,16 @@ The specified directory will be created if it does not exist. 已忽略以下参数:“{0}” + + In process file artifacts produced: + In process file artifacts produced: + + + + Minimum expected tests policy violation, tests ran {0}, minimum expected {1} + Minimum expected tests policy violation, tests ran {0}, minimum expected {1} + {0}, {1} number of tests + No serializer registered with ID '{0}' 没有使用 ID“{0}”注册序列化程序 @@ -290,6 +390,21 @@ The specified directory will be created if it does not exist. 没有使用类型“{0}”注册序列化程序 + + Out of process file artifacts produced: + Out of process file artifacts produced: + + + + Passed + Passed + + + + passed + passed + + @@ -307,6 +422,56 @@ RunSettings 参数: 示例: dotnet test -- MSTest.DeploymentEnabled=false MSTest.MapInconclusiveToFailed=True + + Running tests from + Running tests from + + + + skipped + skipped + + + + at + at + at that is used for a stack frame location in a stack trace, is followed by a class and method name + + + in + in + in that is used in stack frame it is followed by file name + + + Error output + Error output + + + + Standard output + Standard output + + + + Discovered {0} tests in {1} assemblies. + Discovered {0} tests in {1} assemblies. + 0 is number of tests, 1 is count of assemblies + + + Discovered {0} tests. + Discovered {0} tests. + 0 is number of tests + + + Test run summary: + Test run summary: + + + + Zero tests ran + Zero tests ran + + DATA_COLLECTOR_NAME DATA_COLLECTOR_NAME diff --git a/src/Cli/dotnet/commands/dotnet-test/xlf/LocalizableStrings.zh-Hant.xlf b/src/Cli/dotnet/commands/dotnet-test/xlf/LocalizableStrings.zh-Hant.xlf index 222eb321c2c4..2bc5bcf940a9 100644 --- a/src/Cli/dotnet/commands/dotnet-test/xlf/LocalizableStrings.zh-Hant.xlf +++ b/src/Cli/dotnet/commands/dotnet-test/xlf/LocalizableStrings.zh-Hant.xlf @@ -2,6 +2,26 @@ + + Aborted + Aborted + + + + {0} tests running + {0} tests running + + + + and {0} more + and {0} more + + + + Actual + Actual + + .NET Test Driver .NET 測試驅動程式 @@ -12,6 +32,16 @@ 適用於 .NET 平台的測試驅動程式 + + canceled + canceled + + + + Canceling the test session... + Canceling the test session... + + The additional msbuild parameters to pass. 要傳遞的其他 msbuild 參數。 @@ -260,11 +290,71 @@ The specified directory will be created if it does not exist. Test application(s) that support VSTest are not supported. + + Console is already in batching mode. + Console is already in batching mode. + Exception that is thrown when console is already collecting input into a batch (into a string builder), and code asks to enable batching mode again. + DUMP_TYPE DUMP_TYPE + + Discovered {0} tests in assembly + Discovered {0} tests in assembly + 0 is count, the sentence is followed by the path of the assebly + + + Discovering tests from + Discovering tests from + + + + Exit code + Exit code + + + + Expected + Expected + + + + Failed + Failed + + + + failed + failed + + + + failed with {0} error(s) + failed with {0} error(s) + + + + failed with {0} error(s) and {1} warning(s) + failed with {0} error(s) and {1} warning(s) + + + + failed with {0} warning(s) + failed with {0} warning(s) + + + + For test + For test + is followed by test name + + + from + from + from followed by a file name to point to the file from which test is originating + DUMP_TYPE DUMP_TYPE @@ -280,6 +370,16 @@ The specified directory will be created if it does not exist. 已忽略下列引數: "{0}" + + In process file artifacts produced: + In process file artifacts produced: + + + + Minimum expected tests policy violation, tests ran {0}, minimum expected {1} + Minimum expected tests policy violation, tests ran {0}, minimum expected {1} + {0}, {1} number of tests + No serializer registered with ID '{0}' 沒有使用識別碼 '{0}' 註冊的序列化程式 @@ -290,6 +390,21 @@ The specified directory will be created if it does not exist. 沒有使用類型 '{0}' 註冊的序列化程式 + + Out of process file artifacts produced: + Out of process file artifacts produced: + + + + Passed + Passed + + + + passed + passed + + @@ -307,6 +422,56 @@ RunSettings 引數: 範例: dotnet test -- MSTest.DeploymentEnabled=false MSTest.MapInconclusiveToFailed=True + + Running tests from + Running tests from + + + + skipped + skipped + + + + at + at + at that is used for a stack frame location in a stack trace, is followed by a class and method name + + + in + in + in that is used in stack frame it is followed by file name + + + Error output + Error output + + + + Standard output + Standard output + + + + Discovered {0} tests in {1} assemblies. + Discovered {0} tests in {1} assemblies. + 0 is number of tests, 1 is count of assemblies + + + Discovered {0} tests. + Discovered {0} tests. + 0 is number of tests + + + Test run summary: + Test run summary: + + + + Zero tests ran + Zero tests ran + + DATA_COLLECTOR_NAME DATA_COLLECTOR_NAME diff --git a/src/Cli/dotnet/dotnet.csproj b/src/Cli/dotnet/dotnet.csproj index f19ef88ebd53..82db3621e7fc 100644 --- a/src/Cli/dotnet/dotnet.csproj +++ b/src/Cli/dotnet/dotnet.csproj @@ -100,7 +100,6 @@ - From 9c267ca54a5e8834e6cc5edf94f66b8a1d3cba15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Jare=C5=A1?= Date: Thu, 9 Jan 2025 21:09:55 +0100 Subject: [PATCH 20/20] remove version of mtp because it is not used --- eng/Versions.props | 4 ---- 1 file changed, 4 deletions(-) diff --git a/eng/Versions.props b/eng/Versions.props index 3487f318cbf9..b9abd2a4187a 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -160,10 +160,6 @@ 17.13.0-preview-25058-03 17.13.0-preview-25058-03 - - - 1.6.0-preview.25059.11 - 10.0.0-preview.24629.1