From 74e7850a30df2f80228a9cf189434765fd7578c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Sat, 23 May 2026 18:00:00 +0200 Subject: [PATCH 1/2] Add --crash-sequence option to MTP CrashDump extension (#7262) Adds a testhost-side sequence file that records test state transitions (STARTED/ENDED) so the controller can list the tests that were running at the time of a crash without requiring dump analysis. Mirrors HangDump's UX but uses file-based persistence because the testhost is dead by the time the controller inspects it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../CrashDumpCommandLineOptions.cs | 1 + .../CrashDumpCommandLineProvider.cs | 11 +- .../CrashDumpConfiguration.cs | 2 + .../CrashDumpEnvironmentVariableProvider.cs | 67 +++++ .../CrashDumpExtensions.cs | 14 + .../CrashDumpProcessLifetimeHandler.cs | 117 ++++++++ .../CrashDumpSequenceLogger.cs | 251 ++++++++++++++++++ ...rosoft.Testing.Extensions.CrashDump.csproj | 1 + .../Resources/CrashDumpResources.resx | 20 ++ .../Resources/xlf/CrashDumpResources.cs.xlf | 34 +++ .../Resources/xlf/CrashDumpResources.de.xlf | 34 +++ .../Resources/xlf/CrashDumpResources.es.xlf | 34 +++ .../Resources/xlf/CrashDumpResources.fr.xlf | 34 +++ .../Resources/xlf/CrashDumpResources.it.xlf | 34 +++ .../Resources/xlf/CrashDumpResources.ja.xlf | 34 +++ .../Resources/xlf/CrashDumpResources.ko.xlf | 34 +++ .../Resources/xlf/CrashDumpResources.pl.xlf | 34 +++ .../xlf/CrashDumpResources.pt-BR.xlf | 34 +++ .../Resources/xlf/CrashDumpResources.ru.xlf | 34 +++ .../Resources/xlf/CrashDumpResources.tr.xlf | 34 +++ .../xlf/CrashDumpResources.zh-Hans.xlf | 34 +++ .../xlf/CrashDumpResources.zh-Hant.xlf | 34 +++ .../CrashDumpTests.cs | 137 +++++++++- .../HelpInfoAllExtensionsTests.cs | 10 + .../CrashDumpTests.cs | 32 +++ 25 files changed, 1101 insertions(+), 4 deletions(-) create mode 100644 src/Platform/Microsoft.Testing.Extensions.CrashDump/CrashDumpSequenceLogger.cs diff --git a/src/Platform/Microsoft.Testing.Extensions.CrashDump/CrashDumpCommandLineOptions.cs b/src/Platform/Microsoft.Testing.Extensions.CrashDump/CrashDumpCommandLineOptions.cs index 45b2b5924c..98f0339630 100644 --- a/src/Platform/Microsoft.Testing.Extensions.CrashDump/CrashDumpCommandLineOptions.cs +++ b/src/Platform/Microsoft.Testing.Extensions.CrashDump/CrashDumpCommandLineOptions.cs @@ -13,4 +13,5 @@ internal static class CrashDumpCommandLineOptions public const string CrashReportOptionName = "crash-report"; public const string CrashDumpFileNameOptionName = "crashdump-filename"; public const string CrashDumpTypeOptionName = "crashdump-type"; + public const string CrashSequenceOptionName = "crash-sequence"; } diff --git a/src/Platform/Microsoft.Testing.Extensions.CrashDump/CrashDumpCommandLineProvider.cs b/src/Platform/Microsoft.Testing.Extensions.CrashDump/CrashDumpCommandLineProvider.cs index 9f406a9e25..cf5a6a52ab 100644 --- a/src/Platform/Microsoft.Testing.Extensions.CrashDump/CrashDumpCommandLineProvider.cs +++ b/src/Platform/Microsoft.Testing.Extensions.CrashDump/CrashDumpCommandLineProvider.cs @@ -15,6 +15,7 @@ internal sealed class CrashDumpCommandLineProvider : ICommandLineOptionsProvider [ new(CrashDumpCommandLineOptions.CrashDumpOptionName, CrashDumpResources.CrashDumpOptionDescription, ArgumentArity.Zero, false), new(CrashDumpCommandLineOptions.CrashReportOptionName, CrashDumpResources.CrashReportOptionDescription, ArgumentArity.Zero, false), + new(CrashDumpCommandLineOptions.CrashSequenceOptionName, CrashDumpResources.CrashSequenceOptionDescription, ArgumentArity.ExactlyOne, false), new(CrashDumpCommandLineOptions.CrashDumpFileNameOptionName, CrashDumpResources.CrashDumpFileNameOptionDescription, ArgumentArity.ExactlyOne, false), new(CrashDumpCommandLineOptions.CrashDumpTypeOptionName, CrashDumpResources.CrashDumpTypeOptionDescription, ArgumentArity.ExactlyOne, false) ]; @@ -40,6 +41,13 @@ public Task ValidateOptionArgumentsAsync(CommandLineOption com return ValidationResult.InvalidTask(string.Format(CultureInfo.InvariantCulture, CrashDumpResources.CrashDumpTypeOptionInvalidType, arguments[0])); } } + else if (commandOption.Name == CrashDumpCommandLineOptions.CrashSequenceOptionName) + { + if (!CommandLineOptionArgumentValidator.IsValidBooleanArgument(arguments[0])) + { + return ValidationResult.InvalidTask(CrashDumpResources.CrashSequenceOptionInvalidArgument); + } + } // TODO: Validate that the file name ends with '.dmp'? return ValidationResult.ValidTask; @@ -59,7 +67,8 @@ private static bool IsCrashReportUnsupportedOnCurrentPlatform(ICommandLineOption private static bool IsCrashDumpMainOptionMissing(ICommandLineOptions commandLineOptions) { bool hasCrashDumpSubOption = commandLineOptions.IsOptionSet(CrashDumpCommandLineOptions.CrashDumpFileNameOptionName) || - commandLineOptions.IsOptionSet(CrashDumpCommandLineOptions.CrashDumpTypeOptionName); + commandLineOptions.IsOptionSet(CrashDumpCommandLineOptions.CrashDumpTypeOptionName) || + commandLineOptions.IsOptionSet(CrashDumpCommandLineOptions.CrashSequenceOptionName); bool hasCrashDumpMainOption = commandLineOptions.IsOptionSet(CrashDumpCommandLineOptions.CrashDumpOptionName) || commandLineOptions.IsOptionSet(CrashDumpCommandLineOptions.CrashReportOptionName); diff --git a/src/Platform/Microsoft.Testing.Extensions.CrashDump/CrashDumpConfiguration.cs b/src/Platform/Microsoft.Testing.Extensions.CrashDump/CrashDumpConfiguration.cs index b2d64c75d5..225b0c5761 100644 --- a/src/Platform/Microsoft.Testing.Extensions.CrashDump/CrashDumpConfiguration.cs +++ b/src/Platform/Microsoft.Testing.Extensions.CrashDump/CrashDumpConfiguration.cs @@ -7,5 +7,7 @@ internal sealed class CrashDumpConfiguration { public string? DumpFileNamePattern { get; set; } + public string? SequenceFileName { get; set; } + public bool Enable { get; set; } = true; } diff --git a/src/Platform/Microsoft.Testing.Extensions.CrashDump/CrashDumpEnvironmentVariableProvider.cs b/src/Platform/Microsoft.Testing.Extensions.CrashDump/CrashDumpEnvironmentVariableProvider.cs index ab076f46c1..db70b24380 100644 --- a/src/Platform/Microsoft.Testing.Extensions.CrashDump/CrashDumpEnvironmentVariableProvider.cs +++ b/src/Platform/Microsoft.Testing.Extensions.CrashDump/CrashDumpEnvironmentVariableProvider.cs @@ -13,6 +13,8 @@ namespace Microsoft.Testing.Extensions.Diagnostics; internal sealed class CrashDumpEnvironmentVariableProvider : ITestHostEnvironmentVariableProvider { + public const string SequenceFileEnvironmentVariableName = "TESTINGPLATFORM_CRASHDUMP_SEQUENCE_FILE"; + private const string EnableMiniDumpVariable = "DbgEnableMiniDump"; private const string MiniDumpTypeVariable = "DbgMiniDumpType"; private const string MiniDumpNameVariable = "DbgMiniDumpName"; @@ -27,6 +29,7 @@ internal sealed class CrashDumpEnvironmentVariableProvider : ITestHostEnvironmen private readonly ILogger _logger; private string? _miniDumpNameValue; + private string? _sequenceFileValue; public CrashDumpEnvironmentVariableProvider( IConfiguration configuration, @@ -135,15 +138,79 @@ public Task UpdateAsync(IEnvironmentVariables environmentVariables) environmentVariables.SetVariable(new($"{prefix}{MiniDumpNameVariable}", _miniDumpNameValue, false, true)); } + if (IsSequenceLoggingEnabled()) + { + // The sequence file is written directly by the testhost (not by the .NET runtime), so it + // does not need any of the runtime "createdump" placeholders (%p, %e, %h, %t). We compose + // a deterministic path next to the (eventual) dump file so the testhost and the controller + // agree on the exact path to write to / read from without any per-side expansion. + // + // The path includes a per-controller-instance unique token so that parallel testhost + // launches targeting the same results directory cannot stomp on each other's sequence + // files (a write collision would otherwise be silently lost; a graceful exit of one host + // would also delete the sibling host's sequence file before it could be published). + string uniqueToken = Guid.NewGuid().ToString("N").Substring(0, 8); + string sequenceFileName = dumpFileName is not null + ? $"{StripRuntimePlaceholders(dumpFileName[0])}_{uniqueToken}.sequence.log" + : $"{testAppName}_{uniqueToken}_crash.sequence.log"; + _sequenceFileValue = Path.Combine(_configuration.GetTestResultDirectory(), sequenceFileName); + _crashDumpGeneratorConfiguration.SequenceFileName = _sequenceFileValue; + environmentVariables.SetVariable(new(SequenceFileEnvironmentVariableName, _sequenceFileValue, isSecret: false, isLocked: true)); + } + if (_logger.IsEnabled(LogLevel.Trace)) { _logger.LogTrace($"{MiniDumpNameVariable}: {_miniDumpNameValue}"); _logger.LogTrace($"{MiniDumpTypeVariable}: {miniDumpTypeValue}"); + if (_sequenceFileValue is not null) + { + _logger.LogTrace($"{SequenceFileEnvironmentVariableName}: {_sequenceFileValue}"); + } } return Task.CompletedTask; } + private bool IsSequenceLoggingEnabled() + { + if (!_commandLineOptions.TryGetOptionArgumentList(CrashDumpCommandLineOptions.CrashSequenceOptionName, out string[]? arguments)) + { + // Default: on whenever --crashdump or --crash-report is set (IsEnabledAsync gates this method). + return true; + } + + return !CommandLineOptionArgumentValidator.IsOffValue(arguments[0]); + } + + private static string StripRuntimePlaceholders(string pattern) + { + // Drop the .NET runtime's "createdump" placeholders (%p, %e, %h, %t, ...) from a dump filename + // pattern so we obtain a concrete, deterministic file path that the testhost extension and the + // controller can agree on without any per-side expansion. "%%" remains a literal "%" per the + // runtime's escaping convention. + var sb = new StringBuilder(pattern.Length); + for (int i = 0; i < pattern.Length; i++) + { + if (pattern[i] == '%' && i + 1 < pattern.Length) + { + if (pattern[i + 1] == '%') + { + sb.Append('%'); + i++; + continue; + } + + // Drop the single-character placeholder (e.g. %p, %e, %h, %t). + i++; + continue; + } + + sb.Append(pattern[i]); + } + + return sb.ToString(); + } + public Task ValidateTestHostEnvironmentVariablesAsync(IReadOnlyEnvironmentVariables environmentVariables) { #if !NETCOREAPP diff --git a/src/Platform/Microsoft.Testing.Extensions.CrashDump/CrashDumpExtensions.cs b/src/Platform/Microsoft.Testing.Extensions.CrashDump/CrashDumpExtensions.cs index c5d4dcf211..c1b7dc50ec 100644 --- a/src/Platform/Microsoft.Testing.Extensions.CrashDump/CrashDumpExtensions.cs +++ b/src/Platform/Microsoft.Testing.Extensions.CrashDump/CrashDumpExtensions.cs @@ -3,6 +3,7 @@ using Microsoft.Testing.Extensions.Diagnostics; using Microsoft.Testing.Platform.Builder; +using Microsoft.Testing.Platform.Extensions; using Microsoft.Testing.Platform.Services; namespace Microsoft.Testing.Extensions; @@ -43,5 +44,18 @@ public static void AddCrashDumpProvider(this ITestApplicationBuilder builder, bo crashDumpGeneratorConfiguration)); builder.CommandLine.AddProvider(() => new CrashDumpCommandLineProvider()); + + // Testhost-side extension that journals test state transitions to a sequence file so the + // controller can list "tests still running at the time of the crash" without needing a dump. + // The same instance plays both an IDataConsumer and an ITestSessionLifetimeHandler role, so + // we register a composite factory like HangDumpExtensions does for HangDumpActivityIndicator. + var sequenceLoggerComposite = new CompositeExtensionFactory(serviceProvider + => new CrashDumpSequenceLogger( + serviceProvider.GetEnvironment(), + serviceProvider.GetClock(), + serviceProvider.GetLoggerFactory())); + + builder.TestHost.AddDataConsumer(sequenceLoggerComposite); + builder.TestHost.AddTestSessionLifetimeHandler(sequenceLoggerComposite); } } diff --git a/src/Platform/Microsoft.Testing.Extensions.CrashDump/CrashDumpProcessLifetimeHandler.cs b/src/Platform/Microsoft.Testing.Extensions.CrashDump/CrashDumpProcessLifetimeHandler.cs index 6d1f7e378a..b43cf518ac 100644 --- a/src/Platform/Microsoft.Testing.Extensions.CrashDump/CrashDumpProcessLifetimeHandler.cs +++ b/src/Platform/Microsoft.Testing.Extensions.CrashDump/CrashDumpProcessLifetimeHandler.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using Microsoft.Testing.Extensions.Diagnostics.Resources; +using Microsoft.Testing.Platform; using Microsoft.Testing.Platform.CommandLine; using Microsoft.Testing.Platform.Extensions.Messages; using Microsoft.Testing.Platform.Extensions.OutputDevice; @@ -120,6 +121,11 @@ public async Task OnTestHostProcessExitedAsync(ITestHostProcessInformation testH if (testHostProcessInformation.HasExitedGracefully || (AppDomain.CurrentDomain.GetData("ProcessKilledByHangDump") is string processKilledByHangDump && processKilledByHangDump == "true")) { + // No crash → the sequence file (if any) has no diagnostic value. Delete it so the user's + // results directory is not polluted with stale "tests still running" logs after a clean + // run. Hang-dump kills are handled identically because HangDump already produces its own + // .log of in-progress tests via IPC. + TryDeleteSequenceFile(); return; } @@ -265,6 +271,117 @@ public async Task OnTestHostProcessExitedAsync(ITestHostProcessInformation testH } } } + + await TryPublishSequenceFileAsync(cancellationToken).ConfigureAwait(false); + } + + private async Task TryPublishSequenceFileAsync(CancellationToken cancellationToken) + { + string? sequenceFilePath = _netCoreCrashDumpGeneratorConfiguration.SequenceFileName; + if (RoslynString.IsNullOrEmpty(sequenceFilePath) || !File.Exists(sequenceFilePath)) + { + return; + } + + // Parse the journal to compute the set of tests that started but never ended. We can tolerate + // a partially-written final line because the testhost flushes after each whole record; any + // half-written tail line would simply fail to parse and be ignored. + var inFlight = new Dictionary(StringComparer.Ordinal); + DateTimeOffset latestSeen = DateTimeOffset.MinValue; + try + { + foreach (string line in File.ReadLines(sequenceFilePath)) + { + if (line.Length == 0 || line[0] == '#') + { + continue; + } + + // Format: \t\t\t + string[] parts = line.Split('\t'); + if (parts.Length < 4) + { + continue; + } + + if (!DateTimeOffset.TryParse(parts[1], CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out DateTimeOffset timestamp)) + { + continue; + } + + if (timestamp > latestSeen) + { + latestSeen = timestamp; + } + + string uid = parts[2]; + // Any field originally containing tabs was sanitized to spaces by the testhost, so + // a 4-element split is sufficient. Defensively re-join any extras to tolerate future + // schema changes. + string lastField = parts.Length == 4 ? parts[3] : string.Join("\t", parts, 3, parts.Length - 3); + + if (parts[0].Equals(CrashDumpSequenceLogger.StartedEvent, StringComparison.Ordinal)) + { + inFlight[uid] = (lastField, timestamp); + } + else if (parts[0].Equals(CrashDumpSequenceLogger.EndedEvent, StringComparison.Ordinal)) + { + inFlight.Remove(uid); + } + } + } + catch (IOException ex) + { + // Best-effort diagnostic. If we cannot read the sequence file we still publish it so the + // user can inspect it manually, but skip the friendly summary. + await _outputDisplay.DisplayAsync(this, new ErrorMessageOutputDeviceData(string.Format(CultureInfo.InvariantCulture, CrashDumpResources.CrashDumpSequenceFileReadError, sequenceFilePath, ex.Message)), cancellationToken).ConfigureAwait(false); + await _messageBus.PublishAsync(this, new FileArtifact(new FileInfo(sequenceFilePath), CrashDumpResources.CrashDumpSequenceArtifactDisplayName, CrashDumpResources.CrashDumpSequenceArtifactDescription)).ConfigureAwait(false); + return; + } + + if (inFlight.Count > 0) + { + await _outputDisplay.DisplayAsync(this, new ErrorMessageOutputDeviceData(CrashDumpResources.CrashDumpTestsRunningAtCrash), cancellationToken).ConfigureAwait(false); + DateTimeOffset anchor = latestSeen == DateTimeOffset.MinValue ? DateTimeOffset.UtcNow : latestSeen; + foreach (KeyValuePair entry in inFlight.OrderBy(static x => x.Value.StartedAt)) + { + TimeSpan elapsed = anchor - entry.Value.StartedAt; + if (elapsed < TimeSpan.Zero) + { + elapsed = TimeSpan.Zero; + } + + await _outputDisplay.DisplayAsync(this, new ErrorMessageOutputDeviceData($"[{elapsed}] {entry.Value.DisplayName}"), cancellationToken).ConfigureAwait(false); + } + } + + await _messageBus.PublishAsync(this, new FileArtifact(new FileInfo(sequenceFilePath), CrashDumpResources.CrashDumpSequenceArtifactDisplayName, CrashDumpResources.CrashDumpSequenceArtifactDescription)).ConfigureAwait(false); + } + + private void TryDeleteSequenceFile() + { + string? sequenceFilePath = _netCoreCrashDumpGeneratorConfiguration.SequenceFileName; + if (RoslynString.IsNullOrEmpty(sequenceFilePath)) + { + return; + } + + try + { + if (File.Exists(sequenceFilePath)) + { + File.Delete(sequenceFilePath); + } + } + catch (IOException) + { + // Best-effort cleanup; a leftover sequence file is harmless beyond cluttering the results + // directory. Avoid surfacing this as a user-visible error because the run itself succeeded. + } + catch (UnauthorizedAccessException) + { + // Same rationale as IOException. + } } internal static string GetDumpDirectory(string dumpFileNamePattern) diff --git a/src/Platform/Microsoft.Testing.Extensions.CrashDump/CrashDumpSequenceLogger.cs b/src/Platform/Microsoft.Testing.Extensions.CrashDump/CrashDumpSequenceLogger.cs new file mode 100644 index 0000000000..a7f0312ba6 --- /dev/null +++ b/src/Platform/Microsoft.Testing.Extensions.CrashDump/CrashDumpSequenceLogger.cs @@ -0,0 +1,251 @@ +// 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; +using Microsoft.Testing.Platform.Extensions; +using Microsoft.Testing.Platform.Extensions.Messages; +using Microsoft.Testing.Platform.Extensions.TestHost; +using Microsoft.Testing.Platform.Helpers; +using Microsoft.Testing.Platform.Logging; +using Microsoft.Testing.Platform.Services; + +namespace Microsoft.Testing.Extensions.Diagnostics; + +/// +/// Testhost-side extension that appends a line per test state transition to a "sequence file" +/// shared with the testhost controller. On a non-graceful exit (i.e. a crash), the controller +/// reads this file to surface the tests that were running at the time of the crash, mirroring +/// the per-test journaling that --hangdump performs over IPC (which is unavailable for +/// crashes because the testhost is dead). +/// +internal sealed class CrashDumpSequenceLogger : IDataConsumer, ITestSessionLifetimeHandler, +#if NETCOREAPP + IAsyncDisposable, +#endif + IDisposable +{ + // File schema: + // # MTP CrashDump test sequence v1 (format: \t\t\t) + // STARTED\t\t\t + // ENDED\t\t\t + // Tab is used as a separator so display names and states (which may contain spaces, '|', ':', + // etc.) survive a round-trip. Tabs themselves are extremely rare in test names; the controller + // tolerates extra trailing tabs by joining all remaining fields back together. + private const string FileHeader = "# MTP CrashDump test sequence v1 (format: \\t\\t\\t)"; + + internal const string StartedEvent = "STARTED"; + internal const string EndedEvent = "ENDED"; + + private readonly IEnvironment _environment; + private readonly IClock _clock; + private readonly ILogger _logger; + private readonly object _writeLock = new(); + + private string? _sequenceFilePath; + private StreamWriter? _writer; + private bool _disposed; + + public CrashDumpSequenceLogger( + IEnvironment environment, + IClock clock, + ILoggerFactory loggerFactory) + { + _environment = environment; + _clock = clock; + _logger = loggerFactory.CreateLogger(); + } + + public Type[] DataTypesConsumed => [typeof(TestNodeUpdateMessage)]; + + public string Uid => nameof(CrashDumpSequenceLogger); + + public string Version => ExtensionVersion.DefaultSemVer; + + public string DisplayName => nameof(CrashDumpSequenceLogger); + + public string Description => "Records the start and end of each test to a sequence file that can be used to identify the tests that were running at the time of a crash."; + + public Task IsEnabledAsync() + { + // The env var is set by the controller's CrashDumpEnvironmentVariableProvider only when the + // sequence feature is enabled, so its presence is the sole gate here. If it is missing, the + // testhost was either not launched via the crashdump controller or sequence logging was + // explicitly disabled with --crash-sequence off. + _sequenceFilePath = _environment.GetEnvironmentVariable(CrashDumpEnvironmentVariableProvider.SequenceFileEnvironmentVariableName); + return Task.FromResult(!RoslynString.IsNullOrEmpty(_sequenceFilePath)); + } + + public Task OnTestSessionStartingAsync(ITestSessionContext testSessionContext) + { + ApplicationStateGuard.Ensure(_sequenceFilePath is not null); + + try + { + // Ensure the destination directory exists. The controller chose a path under the configured + // results directory which is created elsewhere in the pipeline; this is a defensive call to + // cope with users who customize the path via --crashdump-filename to a subdirectory. + string? directory = Path.GetDirectoryName(_sequenceFilePath); + if (!RoslynString.IsNullOrEmpty(directory)) + { + Directory.CreateDirectory(directory); + } + + // Open in Create mode (overwrites any stale file from a previous run). AutoFlush flushes the + // StreamWriter buffer into the FileStream after each Write*, and FileStream then forwards + // those bytes to the OS — that is enough for a record to survive a process crash because + // the OS still owns the cache after the testhost dies. We do not call fsync (Flush(true)) + // because a sequence file is not required to survive an OS-level crash or power loss. + var fileStream = new FileStream(_sequenceFilePath, FileMode.Create, FileAccess.Write, FileShare.Read); + _writer = new StreamWriter(fileStream, Encoding.UTF8) { AutoFlush = true }; + _writer.WriteLine(FileHeader); + _writer.Flush(); + } + catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or ArgumentException or NotSupportedException or PathTooLongException or DirectoryNotFoundException or System.Security.SecurityException) + { + // The sequence file is a best-effort diagnostic. If we cannot open it (e.g. the disk is + // full, ACLs deny write, the path is invalid, or any other filesystem-level error), we + // trace the failure and behave as if the feature were disabled — failing the test run + // for this would be worse than missing the diagnostic. + _logger.LogWarning($"Failed to open crash sequence file '{_sequenceFilePath}': {ex.Message}"); + _writer?.Dispose(); + _writer = null; + } + + return Task.CompletedTask; + } + + public Task ConsumeAsync(IDataProducer dataProducer, IData value, CancellationToken cancellationToken) + { + if (_writer is null || value is not TestNodeUpdateMessage update) + { + return Task.CompletedTask; + } + + TestNodeStateProperty? state = update.TestNode.Properties.SingleOrDefault(); + if (state is null) + { + return Task.CompletedTask; + } + + string? line = state switch + { + InProgressTestNodeStateProperty => FormatLine(StartedEvent, _clock.UtcNow, update.TestNode.Uid, update.TestNode.DisplayName), +#pragma warning disable CS0618, MTP0001 // Type or member is obsolete - keep parity with HangDumpActivityIndicator's terminal-state set. + PassedTestNodeStateProperty => FormatLine(EndedEvent, _clock.UtcNow, update.TestNode.Uid, "Passed"), + FailedTestNodeStateProperty => FormatLine(EndedEvent, _clock.UtcNow, update.TestNode.Uid, "Failed"), + ErrorTestNodeStateProperty => FormatLine(EndedEvent, _clock.UtcNow, update.TestNode.Uid, "Error"), + SkippedTestNodeStateProperty => FormatLine(EndedEvent, _clock.UtcNow, update.TestNode.Uid, "Skipped"), + CancelledTestNodeStateProperty => FormatLine(EndedEvent, _clock.UtcNow, update.TestNode.Uid, "Cancelled"), + TimeoutTestNodeStateProperty => FormatLine(EndedEvent, _clock.UtcNow, update.TestNode.Uid, "Timeout"), +#pragma warning restore CS0618, MTP0001 + _ => null, + }; + + if (line is null) + { + return Task.CompletedTask; + } + + // ConsumeAsync may be invoked concurrently from multiple data producers/threads; serialize + // writes to keep the on-disk record consistent. Writes are tiny (one line each) so contention + // is negligible. + lock (_writeLock) + { + // Re-check under the lock to defend against a concurrent Dispose closing the writer + // between the null check above and the write here. + if (_writer is null) + { + return Task.CompletedTask; + } + + try + { + _writer.WriteLine(line); + } + catch (ObjectDisposedException) + { + // Writer was disposed between the check above and the write; nothing more to do. + } + catch (IOException ex) + { + // Best-effort logging only: dropping a single record is better than failing the test run. + _logger.LogWarning($"Failed to write to crash sequence file '{_sequenceFilePath}': {ex.Message}"); + } + } + + return Task.CompletedTask; + } + + public Task OnTestSessionFinishingAsync(ITestSessionContext testSessionContext) + { + // Flush remaining bytes; the controller-side handler decides whether to publish (on crash) or + // delete (on graceful exit) the file. We deliberately do not delete from here so that a crash + // *during* finishing still leaves the sequence file behind for the controller to inspect. + lock (_writeLock) + { + try + { + _writer?.Flush(); + } + catch (ObjectDisposedException) + { + // Ignore - already disposed. + } + catch (IOException) + { + // Best-effort - ignore final flush failure. + } + } + + return Task.CompletedTask; + } + + internal static string FormatLine(string eventName, DateTimeOffset timestamp, TestNodeUid uid, string lastField) + // Tab-separated. Replace tab/newline in user-controlled fields to keep the format unambiguous; + // this is a diagnostic file, not user content, so silent normalization is acceptable. + => string.Join( + "\t", + eventName, + timestamp.ToString("O", CultureInfo.InvariantCulture), + Sanitize(uid.Value), + Sanitize(lastField)); + + private static string Sanitize(string value) + => value.Replace('\t', ' ').Replace('\r', ' ').Replace('\n', ' '); + +#if NETCOREAPP + public ValueTask DisposeAsync() + { + Dispose(); + return ValueTask.CompletedTask; + } +#endif + + public void Dispose() + { + if (_disposed) + { + return; + } + + lock (_writeLock) + { + if (_disposed) + { + return; + } + + _disposed = true; + try + { + _writer?.Dispose(); + } + catch (IOException) + { + // Best-effort - ignore close failures. + } + + _writer = null; + } + } +} diff --git a/src/Platform/Microsoft.Testing.Extensions.CrashDump/Microsoft.Testing.Extensions.CrashDump.csproj b/src/Platform/Microsoft.Testing.Extensions.CrashDump/Microsoft.Testing.Extensions.CrashDump.csproj index 34863abd9a..6b734a548b 100644 --- a/src/Platform/Microsoft.Testing.Extensions.CrashDump/Microsoft.Testing.Extensions.CrashDump.csproj +++ b/src/Platform/Microsoft.Testing.Extensions.CrashDump/Microsoft.Testing.Extensions.CrashDump.csproj @@ -10,6 +10,7 @@ + diff --git a/src/Platform/Microsoft.Testing.Extensions.CrashDump/Resources/CrashDumpResources.resx b/src/Platform/Microsoft.Testing.Extensions.CrashDump/Resources/CrashDumpResources.resx index a4ccd77759..e004272a70 100644 --- a/src/Platform/Microsoft.Testing.Extensions.CrashDump/Resources/CrashDumpResources.resx +++ b/src/Platform/Microsoft.Testing.Extensions.CrashDump/Resources/CrashDumpResources.resx @@ -141,6 +141,26 @@ Environment variable '{0}' should have been set to '{1}' but value is '{2}' + + The list of tests that started and ended during the test session, used to identify the tests that were running at the time of the crash + + + Crash sequence file + + + Failed to read the crash sequence file '{0}': {1} + + + The following tests were still running when the test host crashed (format: [<time-elapsed-since-start>] <name>): + + + Control whether a sequence file listing the tests started and ended during the test session is generated alongside the crash dump or crash report. +The file makes it possible to identify the tests that were running at the time of the crash without having to inspect the dump. +Valid values are 'on' (default; also accepts 'true', 'enable', '1') or 'off' (also accepts 'false', 'disable', '0'). + + + --crash-sequence expects a single parameter with value 'on' (or 'true', 'enable', '1') or 'off' (or 'false', 'disable', '0'). + You specified one or more crash dump parameters but did not enable crash dumps. Add --crashdump or --crash-report to the command line. diff --git a/src/Platform/Microsoft.Testing.Extensions.CrashDump/Resources/xlf/CrashDumpResources.cs.xlf b/src/Platform/Microsoft.Testing.Extensions.CrashDump/Resources/xlf/CrashDumpResources.cs.xlf index 84b182a7bb..d1abcef3df 100644 --- a/src/Platform/Microsoft.Testing.Extensions.CrashDump/Resources/xlf/CrashDumpResources.cs.xlf +++ b/src/Platform/Microsoft.Testing.Extensions.CrashDump/Resources/xlf/CrashDumpResources.cs.xlf @@ -72,6 +72,26 @@ Došlo k chybovému ukončení hostitelského procesu testu s PID {0}. Byla vygenerována zpráva o chybovém ukončení. + + The list of tests that started and ended during the test session, used to identify the tests that were running at the time of the crash + The list of tests that started and ended during the test session, used to identify the tests that were running at the time of the crash + + + + Crash sequence file + Crash sequence file + + + + Failed to read the crash sequence file '{0}': {1} + Failed to read the crash sequence file '{0}': {1} + + + + The following tests were still running when the test host crashed (format: [<time-elapsed-since-start>] <name>): + The following tests were still running when the test host crashed (format: [<time-elapsed-since-start>] <name>): + + Specify the type of the dump. Valid values are 'Mini', 'Heap', 'Triage' or 'Full'. Default type is 'Full'. @@ -111,6 +131,20 @@ Další informace najdete na https://learn.microsoft.com/dotnet/core/diagnostics [Pouze Linux/macOS] Při chybovém ukončení testovacího procesu vygeneruje zprávu o chybovém ukončení ve formátu JSON. Pokud chcete vygenerovat také soubor výpisu paměti, zkombinujte s parametrem --crashdump. Pokud se používá samostatně, vyžaduje .NET 7+; při kombinaci s parametrem --crashdump vyžaduje .NET 6+. Tento požadavek na modul runtime není nástrojem vynucován: v případě nepodporovaných modulů runtime nebude vygenerována žádná zpráva o chybovém ukončení. Nepodporuje se ve Windows kvůli omezení modulu runtime .NET (dotnet/runtime#80191). + + Control whether a sequence file listing the tests started and ended during the test session is generated alongside the crash dump or crash report. +The file makes it possible to identify the tests that were running at the time of the crash without having to inspect the dump. +Valid values are 'on' (default; also accepts 'true', 'enable', '1') or 'off' (also accepts 'false', 'disable', '0'). + Control whether a sequence file listing the tests started and ended during the test session is generated alongside the crash dump or crash report. +The file makes it possible to identify the tests that were running at the time of the crash without having to inspect the dump. +Valid values are 'on' (default; also accepts 'true', 'enable', '1') or 'off' (also accepts 'false', 'disable', '0'). + + + + --crash-sequence expects a single parameter with value 'on' (or 'true', 'enable', '1') or 'off' (or 'false', 'disable', '0'). + --crash-sequence expects a single parameter with value 'on' (or 'true', 'enable', '1') or 'off' (or 'false', 'disable', '0'). + + You specified one or more crash dump parameters but did not enable crash dumps. Add --crashdump or --crash-report to the command line. You specified one or more crash dump parameters but did not enable crash dumps. Add --crashdump or --crash-report to the command line. diff --git a/src/Platform/Microsoft.Testing.Extensions.CrashDump/Resources/xlf/CrashDumpResources.de.xlf b/src/Platform/Microsoft.Testing.Extensions.CrashDump/Resources/xlf/CrashDumpResources.de.xlf index c9052305cf..d0c8dfe6b1 100644 --- a/src/Platform/Microsoft.Testing.Extensions.CrashDump/Resources/xlf/CrashDumpResources.de.xlf +++ b/src/Platform/Microsoft.Testing.Extensions.CrashDump/Resources/xlf/CrashDumpResources.de.xlf @@ -72,6 +72,26 @@ Der Testhostprozess mit PID „{0}“ ist abgestürzt, ein Absturzbericht wurde generiert. + + The list of tests that started and ended during the test session, used to identify the tests that were running at the time of the crash + The list of tests that started and ended during the test session, used to identify the tests that were running at the time of the crash + + + + Crash sequence file + Crash sequence file + + + + Failed to read the crash sequence file '{0}': {1} + Failed to read the crash sequence file '{0}': {1} + + + + The following tests were still running when the test host crashed (format: [<time-elapsed-since-start>] <name>): + The following tests were still running when the test host crashed (format: [<time-elapsed-since-start>] <name>): + + Specify the type of the dump. Valid values are 'Mini', 'Heap', 'Triage' or 'Full'. Default type is 'Full'. @@ -111,6 +131,20 @@ Weitere Informationen finden Sie unter https://learn.microsoft.com/dotnet/core/d [Nur Linux/macOS] Generieren Sie einen JSON-Absturzbericht, wenn der Testprozess abstürzt. Kombinieren Sie dies mit „--crashdump“, um auch eine Absturzabbilddatei zu generieren. Erfordert .NET 7 und höher, wenn es allein verwendet wird; .NET 6+ in Kombination mit „--crashdump“. Diese Runtimeanforderung wird vom Tool nicht erzwungen: Bei nicht unterstützten Runtimes wird kein Absturzbericht ausgegeben. Wird unter Windows aufgrund einer .NET-Runtimeeinschränkung nicht unterstützt (dotnet/runtime#80191). + + Control whether a sequence file listing the tests started and ended during the test session is generated alongside the crash dump or crash report. +The file makes it possible to identify the tests that were running at the time of the crash without having to inspect the dump. +Valid values are 'on' (default; also accepts 'true', 'enable', '1') or 'off' (also accepts 'false', 'disable', '0'). + Control whether a sequence file listing the tests started and ended during the test session is generated alongside the crash dump or crash report. +The file makes it possible to identify the tests that were running at the time of the crash without having to inspect the dump. +Valid values are 'on' (default; also accepts 'true', 'enable', '1') or 'off' (also accepts 'false', 'disable', '0'). + + + + --crash-sequence expects a single parameter with value 'on' (or 'true', 'enable', '1') or 'off' (or 'false', 'disable', '0'). + --crash-sequence expects a single parameter with value 'on' (or 'true', 'enable', '1') or 'off' (or 'false', 'disable', '0'). + + You specified one or more crash dump parameters but did not enable crash dumps. Add --crashdump or --crash-report to the command line. You specified one or more crash dump parameters but did not enable crash dumps. Add --crashdump or --crash-report to the command line. diff --git a/src/Platform/Microsoft.Testing.Extensions.CrashDump/Resources/xlf/CrashDumpResources.es.xlf b/src/Platform/Microsoft.Testing.Extensions.CrashDump/Resources/xlf/CrashDumpResources.es.xlf index 9bb1cce73d..7daf28cfac 100644 --- a/src/Platform/Microsoft.Testing.Extensions.CrashDump/Resources/xlf/CrashDumpResources.es.xlf +++ b/src/Platform/Microsoft.Testing.Extensions.CrashDump/Resources/xlf/CrashDumpResources.es.xlf @@ -72,6 +72,26 @@ Se bloqueó el proceso de host de prueba con PID "{0}"; se generó un informe de bloqueo + + The list of tests that started and ended during the test session, used to identify the tests that were running at the time of the crash + The list of tests that started and ended during the test session, used to identify the tests that were running at the time of the crash + + + + Crash sequence file + Crash sequence file + + + + Failed to read the crash sequence file '{0}': {1} + Failed to read the crash sequence file '{0}': {1} + + + + The following tests were still running when the test host crashed (format: [<time-elapsed-since-start>] <name>): + The following tests were still running when the test host crashed (format: [<time-elapsed-since-start>] <name>): + + Specify the type of the dump. Valid values are 'Mini', 'Heap', 'Triage' or 'Full'. Default type is 'Full'. @@ -111,6 +131,20 @@ Para obtener más información, visite https://learn.microsoft.com/dotnet/core/d [Solo Linux/macOS] Genere un informe de bloqueo JSON cuando el proceso de prueba se bloquee. Combine con "--crashdump" para generar también un archivo de volcado. Requiere .NET 7+ cuando se usa solo; .NET 6+ cuando se combina con "--crashdump". La herramienta no aplica este requisito de tiempo de ejecución: en los entornos de ejecución no admitidos, no se emitirá ningún informe de bloqueo. No se admite en Windows debido a una limitación del entorno de ejecución de .NET (dotnet/runtime#80191). + + Control whether a sequence file listing the tests started and ended during the test session is generated alongside the crash dump or crash report. +The file makes it possible to identify the tests that were running at the time of the crash without having to inspect the dump. +Valid values are 'on' (default; also accepts 'true', 'enable', '1') or 'off' (also accepts 'false', 'disable', '0'). + Control whether a sequence file listing the tests started and ended during the test session is generated alongside the crash dump or crash report. +The file makes it possible to identify the tests that were running at the time of the crash without having to inspect the dump. +Valid values are 'on' (default; also accepts 'true', 'enable', '1') or 'off' (also accepts 'false', 'disable', '0'). + + + + --crash-sequence expects a single parameter with value 'on' (or 'true', 'enable', '1') or 'off' (or 'false', 'disable', '0'). + --crash-sequence expects a single parameter with value 'on' (or 'true', 'enable', '1') or 'off' (or 'false', 'disable', '0'). + + You specified one or more crash dump parameters but did not enable crash dumps. Add --crashdump or --crash-report to the command line. You specified one or more crash dump parameters but did not enable crash dumps. Add --crashdump or --crash-report to the command line. diff --git a/src/Platform/Microsoft.Testing.Extensions.CrashDump/Resources/xlf/CrashDumpResources.fr.xlf b/src/Platform/Microsoft.Testing.Extensions.CrashDump/Resources/xlf/CrashDumpResources.fr.xlf index 4162947c65..052fcecbba 100644 --- a/src/Platform/Microsoft.Testing.Extensions.CrashDump/Resources/xlf/CrashDumpResources.fr.xlf +++ b/src/Platform/Microsoft.Testing.Extensions.CrashDump/Resources/xlf/CrashDumpResources.fr.xlf @@ -72,6 +72,26 @@ Le processus hôte de test avec le PID « {0} » s’est arrêté, un rapport d’incident a été généré + + The list of tests that started and ended during the test session, used to identify the tests that were running at the time of the crash + The list of tests that started and ended during the test session, used to identify the tests that were running at the time of the crash + + + + Crash sequence file + Crash sequence file + + + + Failed to read the crash sequence file '{0}': {1} + Failed to read the crash sequence file '{0}': {1} + + + + The following tests were still running when the test host crashed (format: [<time-elapsed-since-start>] <name>): + The following tests were still running when the test host crashed (format: [<time-elapsed-since-start>] <name>): + + Specify the type of the dump. Valid values are 'Mini', 'Heap', 'Triage' or 'Full'. Default type is 'Full'. @@ -111,6 +131,20 @@ Pour plus d’informations, visitez https://learn.microsoft.com/dotnet/core/diag [Linux/macOS uniquement] Générez un rapport d’incident JSON lorsque le processus de test se bloque. Combinez-le avec « --crashdump » pour générer également un fichier de vidage. Nécessite .NET 7+ lorsqu’il est utilisé seul ; .NET 6+ lorsqu’il est combiné avec « --crashdump ». L’outil n’applique pas cette exigence de runtime : aucun rapport d’incident ne sera généré sur les runtimes non pris en charge. Non pris en charge sur Windows en raison d’une limitation du runtime .NET (dotnet/runtime#80191). + + Control whether a sequence file listing the tests started and ended during the test session is generated alongside the crash dump or crash report. +The file makes it possible to identify the tests that were running at the time of the crash without having to inspect the dump. +Valid values are 'on' (default; also accepts 'true', 'enable', '1') or 'off' (also accepts 'false', 'disable', '0'). + Control whether a sequence file listing the tests started and ended during the test session is generated alongside the crash dump or crash report. +The file makes it possible to identify the tests that were running at the time of the crash without having to inspect the dump. +Valid values are 'on' (default; also accepts 'true', 'enable', '1') or 'off' (also accepts 'false', 'disable', '0'). + + + + --crash-sequence expects a single parameter with value 'on' (or 'true', 'enable', '1') or 'off' (or 'false', 'disable', '0'). + --crash-sequence expects a single parameter with value 'on' (or 'true', 'enable', '1') or 'off' (or 'false', 'disable', '0'). + + You specified one or more crash dump parameters but did not enable crash dumps. Add --crashdump or --crash-report to the command line. You specified one or more crash dump parameters but did not enable crash dumps. Add --crashdump or --crash-report to the command line. diff --git a/src/Platform/Microsoft.Testing.Extensions.CrashDump/Resources/xlf/CrashDumpResources.it.xlf b/src/Platform/Microsoft.Testing.Extensions.CrashDump/Resources/xlf/CrashDumpResources.it.xlf index dbab5ad895..d150c79afd 100644 --- a/src/Platform/Microsoft.Testing.Extensions.CrashDump/Resources/xlf/CrashDumpResources.it.xlf +++ b/src/Platform/Microsoft.Testing.Extensions.CrashDump/Resources/xlf/CrashDumpResources.it.xlf @@ -72,6 +72,26 @@ Processo host di test con arresto anomalo del PID ''{0}''. È stato generato un report di arresto anomalo + + The list of tests that started and ended during the test session, used to identify the tests that were running at the time of the crash + The list of tests that started and ended during the test session, used to identify the tests that were running at the time of the crash + + + + Crash sequence file + Crash sequence file + + + + Failed to read the crash sequence file '{0}': {1} + Failed to read the crash sequence file '{0}': {1} + + + + The following tests were still running when the test host crashed (format: [<time-elapsed-since-start>] <name>): + The following tests were still running when the test host crashed (format: [<time-elapsed-since-start>] <name>): + + Specify the type of the dump. Valid values are 'Mini', 'Heap', 'Triage' or 'Full'. Default type is 'Full'. @@ -111,6 +131,20 @@ Per altre informazioni, visitare https://learn.microsoft.com/dotnet/core/diagnos [Solo Linux/macOS] Generare un report di arresto anomalo JSON quando il processo di test si arresta in modo anomalo. Combinare con ''--crashdump'' per generare anche un file di dump. Richiede .NET 7+ se usato da solo; .NET 6+ in combinazione con ''--crashdump''. Questo requisito di runtime non viene applicato dallo strumento: nei runtime non supportati non verrà generato alcun rapporto di arresto anomalo. Non supportato in Windows a causa di una limitazione del runtime .NET (dotnet/runtime#80191). + + Control whether a sequence file listing the tests started and ended during the test session is generated alongside the crash dump or crash report. +The file makes it possible to identify the tests that were running at the time of the crash without having to inspect the dump. +Valid values are 'on' (default; also accepts 'true', 'enable', '1') or 'off' (also accepts 'false', 'disable', '0'). + Control whether a sequence file listing the tests started and ended during the test session is generated alongside the crash dump or crash report. +The file makes it possible to identify the tests that were running at the time of the crash without having to inspect the dump. +Valid values are 'on' (default; also accepts 'true', 'enable', '1') or 'off' (also accepts 'false', 'disable', '0'). + + + + --crash-sequence expects a single parameter with value 'on' (or 'true', 'enable', '1') or 'off' (or 'false', 'disable', '0'). + --crash-sequence expects a single parameter with value 'on' (or 'true', 'enable', '1') or 'off' (or 'false', 'disable', '0'). + + You specified one or more crash dump parameters but did not enable crash dumps. Add --crashdump or --crash-report to the command line. You specified one or more crash dump parameters but did not enable crash dumps. Add --crashdump or --crash-report to the command line. diff --git a/src/Platform/Microsoft.Testing.Extensions.CrashDump/Resources/xlf/CrashDumpResources.ja.xlf b/src/Platform/Microsoft.Testing.Extensions.CrashDump/Resources/xlf/CrashDumpResources.ja.xlf index 34ad568361..008dfbcbaa 100644 --- a/src/Platform/Microsoft.Testing.Extensions.CrashDump/Resources/xlf/CrashDumpResources.ja.xlf +++ b/src/Platform/Microsoft.Testing.Extensions.CrashDump/Resources/xlf/CrashDumpResources.ja.xlf @@ -72,6 +72,26 @@ PID '{0}' のテスト ホスト プロセスがクラッシュしました。クラッシュ レポートが生成されました + + The list of tests that started and ended during the test session, used to identify the tests that were running at the time of the crash + The list of tests that started and ended during the test session, used to identify the tests that were running at the time of the crash + + + + Crash sequence file + Crash sequence file + + + + Failed to read the crash sequence file '{0}': {1} + Failed to read the crash sequence file '{0}': {1} + + + + The following tests were still running when the test host crashed (format: [<time-elapsed-since-start>] <name>): + The following tests were still running when the test host crashed (format: [<time-elapsed-since-start>] <name>): + + Specify the type of the dump. Valid values are 'Mini', 'Heap', 'Triage' or 'Full'. Default type is 'Full'. @@ -111,6 +131,20 @@ For more information visit https://learn.microsoft.com/dotnet/core/diagnostics/c [Linux/macOS のみ] テスト プロセスがクラッシュしたときに JSON クラッシュ レポートを生成します。'--crashdump' と組み合わせて、ダンプ ファイルも生成できます。単独で使用する場合は .NET 7 以降、'--crashdump' と組み合わせる場合は .NET 6 以降が必要です。このランタイム要件はツールでは強制されません。サポートされていないランタイムではクラッシュ レポートは出力されません。.NET ランタイムの制限により、Windows ではサポートされません (dotnet/runtime#80191)。 + + Control whether a sequence file listing the tests started and ended during the test session is generated alongside the crash dump or crash report. +The file makes it possible to identify the tests that were running at the time of the crash without having to inspect the dump. +Valid values are 'on' (default; also accepts 'true', 'enable', '1') or 'off' (also accepts 'false', 'disable', '0'). + Control whether a sequence file listing the tests started and ended during the test session is generated alongside the crash dump or crash report. +The file makes it possible to identify the tests that were running at the time of the crash without having to inspect the dump. +Valid values are 'on' (default; also accepts 'true', 'enable', '1') or 'off' (also accepts 'false', 'disable', '0'). + + + + --crash-sequence expects a single parameter with value 'on' (or 'true', 'enable', '1') or 'off' (or 'false', 'disable', '0'). + --crash-sequence expects a single parameter with value 'on' (or 'true', 'enable', '1') or 'off' (or 'false', 'disable', '0'). + + You specified one or more crash dump parameters but did not enable crash dumps. Add --crashdump or --crash-report to the command line. You specified one or more crash dump parameters but did not enable crash dumps. Add --crashdump or --crash-report to the command line. diff --git a/src/Platform/Microsoft.Testing.Extensions.CrashDump/Resources/xlf/CrashDumpResources.ko.xlf b/src/Platform/Microsoft.Testing.Extensions.CrashDump/Resources/xlf/CrashDumpResources.ko.xlf index 0afd48b958..1aeac69e45 100644 --- a/src/Platform/Microsoft.Testing.Extensions.CrashDump/Resources/xlf/CrashDumpResources.ko.xlf +++ b/src/Platform/Microsoft.Testing.Extensions.CrashDump/Resources/xlf/CrashDumpResources.ko.xlf @@ -72,6 +72,26 @@ PID가 '{0}'인 테스트 호스트 프로세스가 충돌했습니다. 덤프 파일이 생성되었습니다. + + The list of tests that started and ended during the test session, used to identify the tests that were running at the time of the crash + The list of tests that started and ended during the test session, used to identify the tests that were running at the time of the crash + + + + Crash sequence file + Crash sequence file + + + + Failed to read the crash sequence file '{0}': {1} + Failed to read the crash sequence file '{0}': {1} + + + + The following tests were still running when the test host crashed (format: [<time-elapsed-since-start>] <name>): + The following tests were still running when the test host crashed (format: [<time-elapsed-since-start>] <name>): + + Specify the type of the dump. Valid values are 'Mini', 'Heap', 'Triage' or 'Full'. Default type is 'Full'. @@ -111,6 +131,20 @@ For more information visit https://learn.microsoft.com/dotnet/core/diagnostics/c [Linux/macOS만 해당] 테스트 프로세스가 충돌할 때 JSON 크래시 보고서를 생성합니다. '--crashdump'와 함께 사용하면 덤프 파일도 생성합니다. 단독으로 사용할 때는 .NET 7+가 필요합니다. '--crashdump'와 함께 사용할 때는 .NET 6+가 필요합니다. 이 런타임 요구 사항은 도구에서 강제하지 않습니다. 지원되지 않는 런타임에서는 크래시 보고서가 생성되지 않습니다. .NET 런타임 제한(dotnet/runtime#80191)으로 인해 Windows에서는 지원되지 않습니다. + + Control whether a sequence file listing the tests started and ended during the test session is generated alongside the crash dump or crash report. +The file makes it possible to identify the tests that were running at the time of the crash without having to inspect the dump. +Valid values are 'on' (default; also accepts 'true', 'enable', '1') or 'off' (also accepts 'false', 'disable', '0'). + Control whether a sequence file listing the tests started and ended during the test session is generated alongside the crash dump or crash report. +The file makes it possible to identify the tests that were running at the time of the crash without having to inspect the dump. +Valid values are 'on' (default; also accepts 'true', 'enable', '1') or 'off' (also accepts 'false', 'disable', '0'). + + + + --crash-sequence expects a single parameter with value 'on' (or 'true', 'enable', '1') or 'off' (or 'false', 'disable', '0'). + --crash-sequence expects a single parameter with value 'on' (or 'true', 'enable', '1') or 'off' (or 'false', 'disable', '0'). + + You specified one or more crash dump parameters but did not enable crash dumps. Add --crashdump or --crash-report to the command line. You specified one or more crash dump parameters but did not enable crash dumps. Add --crashdump or --crash-report to the command line. diff --git a/src/Platform/Microsoft.Testing.Extensions.CrashDump/Resources/xlf/CrashDumpResources.pl.xlf b/src/Platform/Microsoft.Testing.Extensions.CrashDump/Resources/xlf/CrashDumpResources.pl.xlf index b9a1eee4e4..6cb741ec6f 100644 --- a/src/Platform/Microsoft.Testing.Extensions.CrashDump/Resources/xlf/CrashDumpResources.pl.xlf +++ b/src/Platform/Microsoft.Testing.Extensions.CrashDump/Resources/xlf/CrashDumpResources.pl.xlf @@ -72,6 +72,26 @@ Proces hosta testowego o identyfikatorze PID „{0}” uległ awarii, wygenerowano raport o awarii + + The list of tests that started and ended during the test session, used to identify the tests that were running at the time of the crash + The list of tests that started and ended during the test session, used to identify the tests that were running at the time of the crash + + + + Crash sequence file + Crash sequence file + + + + Failed to read the crash sequence file '{0}': {1} + Failed to read the crash sequence file '{0}': {1} + + + + The following tests were still running when the test host crashed (format: [<time-elapsed-since-start>] <name>): + The following tests were still running when the test host crashed (format: [<time-elapsed-since-start>] <name>): + + Specify the type of the dump. Valid values are 'Mini', 'Heap', 'Triage' or 'Full'. Default type is 'Full'. @@ -111,6 +131,20 @@ Aby uzyskać więcej informacji, odwiedź stronę https://learn.microsoft.com/do [Tylko Linux/macOS] Generuj raport JSON o awarii, gdy proces testowy ulegnie awarii. Połącz to z elementem „--crashdump”, aby też wygenerować plik zrzutu. Gdy używasz tego samodzielnie, wymaga to platformy .NET 7+; gdy używasz razem z elementem „--crashdump” — platformy .NET 6+. To wymaganie dotyczące środowiska uruchomieniowego nie jest wymuszane przez narzędzie: w nieobsługiwanych środowiskach uruchomieniowych nie będzie emitowany żaden raport o awarii. Nieobsługiwane w systemie Windows z powodu ograniczenia środowiska uruchomieniowego platformy .NET (dotnet/runtime#80191). + + Control whether a sequence file listing the tests started and ended during the test session is generated alongside the crash dump or crash report. +The file makes it possible to identify the tests that were running at the time of the crash without having to inspect the dump. +Valid values are 'on' (default; also accepts 'true', 'enable', '1') or 'off' (also accepts 'false', 'disable', '0'). + Control whether a sequence file listing the tests started and ended during the test session is generated alongside the crash dump or crash report. +The file makes it possible to identify the tests that were running at the time of the crash without having to inspect the dump. +Valid values are 'on' (default; also accepts 'true', 'enable', '1') or 'off' (also accepts 'false', 'disable', '0'). + + + + --crash-sequence expects a single parameter with value 'on' (or 'true', 'enable', '1') or 'off' (or 'false', 'disable', '0'). + --crash-sequence expects a single parameter with value 'on' (or 'true', 'enable', '1') or 'off' (or 'false', 'disable', '0'). + + You specified one or more crash dump parameters but did not enable crash dumps. Add --crashdump or --crash-report to the command line. You specified one or more crash dump parameters but did not enable crash dumps. Add --crashdump or --crash-report to the command line. diff --git a/src/Platform/Microsoft.Testing.Extensions.CrashDump/Resources/xlf/CrashDumpResources.pt-BR.xlf b/src/Platform/Microsoft.Testing.Extensions.CrashDump/Resources/xlf/CrashDumpResources.pt-BR.xlf index 346eb56b6a..11c2d80c9d 100644 --- a/src/Platform/Microsoft.Testing.Extensions.CrashDump/Resources/xlf/CrashDumpResources.pt-BR.xlf +++ b/src/Platform/Microsoft.Testing.Extensions.CrashDump/Resources/xlf/CrashDumpResources.pt-BR.xlf @@ -72,6 +72,26 @@ O processo de host de teste com o PID '{0}' falhou, um relatório de falha foi gerado + + The list of tests that started and ended during the test session, used to identify the tests that were running at the time of the crash + The list of tests that started and ended during the test session, used to identify the tests that were running at the time of the crash + + + + Crash sequence file + Crash sequence file + + + + Failed to read the crash sequence file '{0}': {1} + Failed to read the crash sequence file '{0}': {1} + + + + The following tests were still running when the test host crashed (format: [<time-elapsed-since-start>] <name>): + The following tests were still running when the test host crashed (format: [<time-elapsed-since-start>] <name>): + + Specify the type of the dump. Valid values are 'Mini', 'Heap', 'Triage' or 'Full'. Default type is 'Full'. @@ -111,6 +131,20 @@ Para obter mais informações, visite https://learn.microsoft.com/dotnet/core/di [Somente Linux/macOS] Gere um relatório de falha em JSON quando o processo de teste travar. Combine com '--crashdump' para também gerar um arquivo de despejo. Requer o .NET 7+ quando usado sozinho; .NET 6+ quando combinado com '--crashdump'. Esse requisito de runtime não é imposto pela ferramenta: em runtimes sem suporte. Nenhum relatório de falha será emitido. Sem suporte no Windows devido a uma limitação de runtime do .NET (dotnet/runtime#80191). + + Control whether a sequence file listing the tests started and ended during the test session is generated alongside the crash dump or crash report. +The file makes it possible to identify the tests that were running at the time of the crash without having to inspect the dump. +Valid values are 'on' (default; also accepts 'true', 'enable', '1') or 'off' (also accepts 'false', 'disable', '0'). + Control whether a sequence file listing the tests started and ended during the test session is generated alongside the crash dump or crash report. +The file makes it possible to identify the tests that were running at the time of the crash without having to inspect the dump. +Valid values are 'on' (default; also accepts 'true', 'enable', '1') or 'off' (also accepts 'false', 'disable', '0'). + + + + --crash-sequence expects a single parameter with value 'on' (or 'true', 'enable', '1') or 'off' (or 'false', 'disable', '0'). + --crash-sequence expects a single parameter with value 'on' (or 'true', 'enable', '1') or 'off' (or 'false', 'disable', '0'). + + You specified one or more crash dump parameters but did not enable crash dumps. Add --crashdump or --crash-report to the command line. You specified one or more crash dump parameters but did not enable crash dumps. Add --crashdump or --crash-report to the command line. diff --git a/src/Platform/Microsoft.Testing.Extensions.CrashDump/Resources/xlf/CrashDumpResources.ru.xlf b/src/Platform/Microsoft.Testing.Extensions.CrashDump/Resources/xlf/CrashDumpResources.ru.xlf index a6587dd1c1..a575a548a1 100644 --- a/src/Platform/Microsoft.Testing.Extensions.CrashDump/Resources/xlf/CrashDumpResources.ru.xlf +++ b/src/Platform/Microsoft.Testing.Extensions.CrashDump/Resources/xlf/CrashDumpResources.ru.xlf @@ -72,6 +72,26 @@ Произошло аварийное завершение тестового хост-процесса с идентификатором процесса "{0}". Создан отчет об аварийном завершении + + The list of tests that started and ended during the test session, used to identify the tests that were running at the time of the crash + The list of tests that started and ended during the test session, used to identify the tests that were running at the time of the crash + + + + Crash sequence file + Crash sequence file + + + + Failed to read the crash sequence file '{0}': {1} + Failed to read the crash sequence file '{0}': {1} + + + + The following tests were still running when the test host crashed (format: [<time-elapsed-since-start>] <name>): + The following tests were still running when the test host crashed (format: [<time-elapsed-since-start>] <name>): + + Specify the type of the dump. Valid values are 'Mini', 'Heap', 'Triage' or 'Full'. Default type is 'Full'. @@ -111,6 +131,20 @@ For more information visit https://learn.microsoft.com/dotnet/core/diagnostics/c [Только для Linux/macOS] Создать JSON-отчет об аварийном завершении, если процесс тестирования завершается аварийно. Чтобы также создать файл дампа, используйте вместе с --crashdump. При отдельном использовании требуется .NET 7+; в сочетании с --crashdump — .NET 6+. Это требование среды выполнения не применяется принудительно средством: в неподдерживаемых средах выполнения отчет об аварийном завершении не создается. Не поддерживается в Windows из-за ограничения среды выполнения .NET (dotnet/runtime#80191). + + Control whether a sequence file listing the tests started and ended during the test session is generated alongside the crash dump or crash report. +The file makes it possible to identify the tests that were running at the time of the crash without having to inspect the dump. +Valid values are 'on' (default; also accepts 'true', 'enable', '1') or 'off' (also accepts 'false', 'disable', '0'). + Control whether a sequence file listing the tests started and ended during the test session is generated alongside the crash dump or crash report. +The file makes it possible to identify the tests that were running at the time of the crash without having to inspect the dump. +Valid values are 'on' (default; also accepts 'true', 'enable', '1') or 'off' (also accepts 'false', 'disable', '0'). + + + + --crash-sequence expects a single parameter with value 'on' (or 'true', 'enable', '1') or 'off' (or 'false', 'disable', '0'). + --crash-sequence expects a single parameter with value 'on' (or 'true', 'enable', '1') or 'off' (or 'false', 'disable', '0'). + + You specified one or more crash dump parameters but did not enable crash dumps. Add --crashdump or --crash-report to the command line. You specified one or more crash dump parameters but did not enable crash dumps. Add --crashdump or --crash-report to the command line. diff --git a/src/Platform/Microsoft.Testing.Extensions.CrashDump/Resources/xlf/CrashDumpResources.tr.xlf b/src/Platform/Microsoft.Testing.Extensions.CrashDump/Resources/xlf/CrashDumpResources.tr.xlf index 381bd4d84e..c8baf0984b 100644 --- a/src/Platform/Microsoft.Testing.Extensions.CrashDump/Resources/xlf/CrashDumpResources.tr.xlf +++ b/src/Platform/Microsoft.Testing.Extensions.CrashDump/Resources/xlf/CrashDumpResources.tr.xlf @@ -72,6 +72,26 @@ PID'li test ana işlemi '{0}' kilitlendi, bir kilitlenme raporu oluşturuldu + + The list of tests that started and ended during the test session, used to identify the tests that were running at the time of the crash + The list of tests that started and ended during the test session, used to identify the tests that were running at the time of the crash + + + + Crash sequence file + Crash sequence file + + + + Failed to read the crash sequence file '{0}': {1} + Failed to read the crash sequence file '{0}': {1} + + + + The following tests were still running when the test host crashed (format: [<time-elapsed-since-start>] <name>): + The following tests were still running when the test host crashed (format: [<time-elapsed-since-start>] <name>): + + Specify the type of the dump. Valid values are 'Mini', 'Heap', 'Triage' or 'Full'. Default type is 'Full'. @@ -111,6 +131,20 @@ Daha fazla bilgi için https://learn.microsoft.com/dotnet/core/diagnostics/colle `[Yalnızca Linux/macOS] Test işlemi kilitlendiğinde bir JSON kilitlenme raporu oluşturun. Ayrıca bir döküm dosyası oluşturmak için '--crashdump' ile birlikte kullanın. Tek başına kullanıldığında .NET 7+ gerektirir. '--crashdump' ile birlikte kullanıldığında .NET 6+ gerektirir. Bu çalışma zamanı gereksinimi araç tarafından zorunlu kılınmaz. Desteklenmeyen çalışma zamanlarında kilitlenme raporu oluşturulmaz. .NET çalışma zamanı sınırlaması nedeniyle Windows'ta desteklenmez (dotnet/runtime#80191). + + Control whether a sequence file listing the tests started and ended during the test session is generated alongside the crash dump or crash report. +The file makes it possible to identify the tests that were running at the time of the crash without having to inspect the dump. +Valid values are 'on' (default; also accepts 'true', 'enable', '1') or 'off' (also accepts 'false', 'disable', '0'). + Control whether a sequence file listing the tests started and ended during the test session is generated alongside the crash dump or crash report. +The file makes it possible to identify the tests that were running at the time of the crash without having to inspect the dump. +Valid values are 'on' (default; also accepts 'true', 'enable', '1') or 'off' (also accepts 'false', 'disable', '0'). + + + + --crash-sequence expects a single parameter with value 'on' (or 'true', 'enable', '1') or 'off' (or 'false', 'disable', '0'). + --crash-sequence expects a single parameter with value 'on' (or 'true', 'enable', '1') or 'off' (or 'false', 'disable', '0'). + + You specified one or more crash dump parameters but did not enable crash dumps. Add --crashdump or --crash-report to the command line. You specified one or more crash dump parameters but did not enable crash dumps. Add --crashdump or --crash-report to the command line. diff --git a/src/Platform/Microsoft.Testing.Extensions.CrashDump/Resources/xlf/CrashDumpResources.zh-Hans.xlf b/src/Platform/Microsoft.Testing.Extensions.CrashDump/Resources/xlf/CrashDumpResources.zh-Hans.xlf index 12208794a2..639c817aff 100644 --- a/src/Platform/Microsoft.Testing.Extensions.CrashDump/Resources/xlf/CrashDumpResources.zh-Hans.xlf +++ b/src/Platform/Microsoft.Testing.Extensions.CrashDump/Resources/xlf/CrashDumpResources.zh-Hans.xlf @@ -72,6 +72,26 @@ PID 为“{0}”的测试主机进程故障,已生成故障报表 + + The list of tests that started and ended during the test session, used to identify the tests that were running at the time of the crash + The list of tests that started and ended during the test session, used to identify the tests that were running at the time of the crash + + + + Crash sequence file + Crash sequence file + + + + Failed to read the crash sequence file '{0}': {1} + Failed to read the crash sequence file '{0}': {1} + + + + The following tests were still running when the test host crashed (format: [<time-elapsed-since-start>] <name>): + The following tests were still running when the test host crashed (format: [<time-elapsed-since-start>] <name>): + + Specify the type of the dump. Valid values are 'Mini', 'Heap', 'Triage' or 'Full'. Default type is 'Full'. @@ -111,6 +131,20 @@ For more information visit https://learn.microsoft.com/dotnet/core/diagnostics/c [仅 Linux/macOS] 测试进程故障时生成 JSON 故障报表。与 "--crashdump" 结合使用时,还会生成转储文件。单独使用时需要 .NET 7+;与 "--crashdump" 结合使用时需要 .NET 6+。此工具不强制执行此运行时要求: 在不受支持的运行时上,不会生成故障报表。由于 .NET 运行时限制(dotnet/runtime#80191),在 Windows 上不受支持。 + + Control whether a sequence file listing the tests started and ended during the test session is generated alongside the crash dump or crash report. +The file makes it possible to identify the tests that were running at the time of the crash without having to inspect the dump. +Valid values are 'on' (default; also accepts 'true', 'enable', '1') or 'off' (also accepts 'false', 'disable', '0'). + Control whether a sequence file listing the tests started and ended during the test session is generated alongside the crash dump or crash report. +The file makes it possible to identify the tests that were running at the time of the crash without having to inspect the dump. +Valid values are 'on' (default; also accepts 'true', 'enable', '1') or 'off' (also accepts 'false', 'disable', '0'). + + + + --crash-sequence expects a single parameter with value 'on' (or 'true', 'enable', '1') or 'off' (or 'false', 'disable', '0'). + --crash-sequence expects a single parameter with value 'on' (or 'true', 'enable', '1') or 'off' (or 'false', 'disable', '0'). + + You specified one or more crash dump parameters but did not enable crash dumps. Add --crashdump or --crash-report to the command line. You specified one or more crash dump parameters but did not enable crash dumps. Add --crashdump or --crash-report to the command line. diff --git a/src/Platform/Microsoft.Testing.Extensions.CrashDump/Resources/xlf/CrashDumpResources.zh-Hant.xlf b/src/Platform/Microsoft.Testing.Extensions.CrashDump/Resources/xlf/CrashDumpResources.zh-Hant.xlf index 428ad4e4fc..bb21fb49cd 100644 --- a/src/Platform/Microsoft.Testing.Extensions.CrashDump/Resources/xlf/CrashDumpResources.zh-Hant.xlf +++ b/src/Platform/Microsoft.Testing.Extensions.CrashDump/Resources/xlf/CrashDumpResources.zh-Hant.xlf @@ -72,6 +72,26 @@ PID '{0}' 損毀的測試主機處理常式,已產生損毀報告 + + The list of tests that started and ended during the test session, used to identify the tests that were running at the time of the crash + The list of tests that started and ended during the test session, used to identify the tests that were running at the time of the crash + + + + Crash sequence file + Crash sequence file + + + + Failed to read the crash sequence file '{0}': {1} + Failed to read the crash sequence file '{0}': {1} + + + + The following tests were still running when the test host crashed (format: [<time-elapsed-since-start>] <name>): + The following tests were still running when the test host crashed (format: [<time-elapsed-since-start>] <name>): + + Specify the type of the dump. Valid values are 'Mini', 'Heap', 'Triage' or 'Full'. Default type is 'Full'. @@ -111,6 +131,20 @@ For more information visit https://learn.microsoft.com/dotnet/core/diagnostics/c [僅限 Linux/macOS] 當測試處理序損毀時,產生 JSON 損毀報告。將此選項與 '--crashdump' 一起使用,也可產生傾印檔案。單獨使用時需要 .NET 7 以上版本;與 '--crashdump' 搭配使用時需要 .NET 6 以上版本。工具不會強制檢查此執行階段需求: 在不支援的執行階段上,不會輸出損毀報告。由於 .NET 執行階段的限制 (dotnet/runtime#80191),Windows 不支援此功能。 + + Control whether a sequence file listing the tests started and ended during the test session is generated alongside the crash dump or crash report. +The file makes it possible to identify the tests that were running at the time of the crash without having to inspect the dump. +Valid values are 'on' (default; also accepts 'true', 'enable', '1') or 'off' (also accepts 'false', 'disable', '0'). + Control whether a sequence file listing the tests started and ended during the test session is generated alongside the crash dump or crash report. +The file makes it possible to identify the tests that were running at the time of the crash without having to inspect the dump. +Valid values are 'on' (default; also accepts 'true', 'enable', '1') or 'off' (also accepts 'false', 'disable', '0'). + + + + --crash-sequence expects a single parameter with value 'on' (or 'true', 'enable', '1') or 'off' (or 'false', 'disable', '0'). + --crash-sequence expects a single parameter with value 'on' (or 'true', 'enable', '1') or 'off' (or 'false', 'disable', '0'). + + You specified one or more crash dump parameters but did not enable crash dumps. Add --crashdump or --crash-report to the command line. You specified one or more crash dump parameters but did not enable crash dumps. Add --crashdump or --crash-report to the command line. diff --git a/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/CrashDumpTests.cs b/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/CrashDumpTests.cs index bfabf14439..aeb6937d64 100644 --- a/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/CrashDumpTests.cs +++ b/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/CrashDumpTests.cs @@ -153,6 +153,101 @@ public async Task CrashDump_InvalidFormat_ShouldFail() testHostResult.AssertOutputContains("Option '--crashdump-type' has invalid arguments: 'invalid' is not a valid dump type. Valid options are 'Mini', 'Heap', 'Triage' and 'Full'"); } + [DynamicData(nameof(TargetFrameworks.NetForDynamicData), typeof(TargetFrameworks))] + [TestMethod] + public async Task CrashDump_DefaultSetting_GeneratesSequenceFileListingRunningTests(string tfm) + { + string resultDirectory = Path.Combine(AssetFixture.TargetAssetPath, Guid.NewGuid().ToString("N")); + var testHost = TestInfrastructure.TestHost.LocateFrom(AssetFixture.TargetAssetPath, "CrashDump", tfm); + TestHostResult testHostResult = await testHost.ExecuteAsync( + $"--crashdump --results-directory {resultDirectory}", + new Dictionary + { + { "CRASHDUMP_PUBLISH_INPROGRESS_TESTS", "MyTest1;MyTest2" }, + }, + cancellationToken: TestContext.CancellationToken); + testHostResult.AssertExitCodeIs(ExitCode.TestHostProcessExitedNonGracefully); + + string sequenceFile = Assert.ContainsSingle( + Directory.GetFiles(resultDirectory, "*.sequence.log", SearchOption.AllDirectories), + $"Crash sequence file not found '{tfm}'\n{testHostResult}"); + string content = await File.ReadAllTextAsync(sequenceFile, TestContext.CancellationToken); + Assert.Contains("STARTED", content); + Assert.Contains("MyTest1", content); + Assert.Contains("MyTest2", content); + + // The friendly summary printed by the controller lists each running test by display name. + testHostResult.AssertOutputContains("The following tests were still running when the test host crashed"); + testHostResult.AssertOutputContains("MyTest1"); + testHostResult.AssertOutputContains("MyTest2"); + } + + [TestMethod] + public async Task CrashDump_SequenceOff_DoesNotGenerateSequenceFile() + { + string resultDirectory = Path.Combine(AssetFixture.TargetAssetPath, Guid.NewGuid().ToString("N")); + var testHost = TestInfrastructure.TestHost.LocateFrom(AssetFixture.TargetAssetPath, "CrashDump", TargetFrameworks.NetCurrent); + TestHostResult testHostResult = await testHost.ExecuteAsync( + $"--crashdump --crash-sequence off --results-directory {resultDirectory}", + new Dictionary + { + { "CRASHDUMP_PUBLISH_INPROGRESS_TESTS", "MyTest1" }, + }, + cancellationToken: TestContext.CancellationToken); + testHostResult.AssertExitCodeIs(ExitCode.TestHostProcessExitedNonGracefully); + + Assert.IsEmpty( + Directory.GetFiles(resultDirectory, "*.sequence.log", SearchOption.AllDirectories), + $"No sequence file expected when --crash-sequence off is set\n{testHostResult}"); + testHostResult.AssertOutputDoesNotContain("The following tests were still running when the test host crashed"); + } + + [DynamicData(nameof(TargetFrameworks.NetForDynamicData), typeof(TargetFrameworks))] + [TestMethod] + public async Task CrashDump_GracefulExit_DeletesSequenceFile(string tfm) + { + string resultDirectory = Path.Combine(AssetFixture.TargetAssetPath, Guid.NewGuid().ToString("N")); + var testHost = TestInfrastructure.TestHost.LocateFrom(AssetFixture.TargetAssetPath, "CrashDump", tfm); + TestHostResult testHostResult = await testHost.ExecuteAsync( + $"--crashdump --results-directory {resultDirectory}", + new Dictionary + { + { "CRASHDUMP_PUBLISH_INPROGRESS_TESTS", "MyTest1" }, + { "CRASHDUMP_EXIT_GRACEFULLY", "1" }, + }, + cancellationToken: TestContext.CancellationToken); + + // Even though tests were "started", the testhost exited cleanly so the sequence file has no + // diagnostic value and must be cleaned up to avoid polluting the results directory. + Assert.IsEmpty( + Directory.GetFiles(resultDirectory, "*.sequence.log", SearchOption.AllDirectories), + $"No sequence file expected on graceful exit '{tfm}'\n{testHostResult}"); + } + + [TestMethod] + public async Task CrashSequence_InvalidArgument_ShouldFail() + { + string resultDirectory = Path.Combine(AssetFixture.TargetAssetPath, Guid.NewGuid().ToString("N")); + var testHost = TestInfrastructure.TestHost.LocateFrom(AssetFixture.TargetAssetPath, "CrashDump", TargetFrameworks.NetCurrent); + TestHostResult testHostResult = await testHost.ExecuteAsync( + $"--crashdump --crash-sequence maybe --results-directory {resultDirectory}", + cancellationToken: TestContext.CancellationToken); + testHostResult.AssertExitCodeIs(ExitCode.InvalidCommandLine); + testHostResult.AssertOutputContains("--crash-sequence expects a single parameter"); + } + + [TestMethod] + public async Task CrashSequence_WithoutCrashDumpOrCrashReport_ShouldFail() + { + string resultDirectory = Path.Combine(AssetFixture.TargetAssetPath, Guid.NewGuid().ToString("N")); + var testHost = TestInfrastructure.TestHost.LocateFrom(AssetFixture.TargetAssetPath, "CrashDump", TargetFrameworks.NetCurrent); + TestHostResult testHostResult = await testHost.ExecuteAsync( + $"--crash-sequence on --results-directory {resultDirectory}", + cancellationToken: TestContext.CancellationToken); + testHostResult.AssertExitCodeIs(ExitCode.InvalidCommandLine); + testHostResult.AssertOutputContains("Add --crashdump or --crash-report"); + } + public sealed class TestAssetFixture() : TestAssetFixtureBase() { private const string AssetName = "CrashDumpFixture"; @@ -187,6 +282,7 @@ public override (string ID, string Name, string Code) GetAssetsToGenerate() => ( using System.Threading.Tasks; using System.Globalization; using Microsoft.Testing.Platform; +using Microsoft.Testing.Platform.Extensions.Messages; using Microsoft.Testing.Platform.Extensions.TestFramework; using Microsoft.Testing.Platform.Builder; using Microsoft.Testing.Platform.Capabilities.TestFramework; @@ -220,7 +316,7 @@ public static async Task Main(string[] args) } } -public class DummyTestFramework : ITestFramework +public class DummyTestFramework : ITestFramework, IDataProducer { public string Uid => nameof(DummyTestFramework); @@ -230,6 +326,8 @@ public class DummyTestFramework : ITestFramework public string Description => nameof(DummyTestFramework); + public Type[] DataTypesProduced => new[] { typeof(TestNodeUpdateMessage) }; + public Task IsEnabledAsync() => Task.FromResult(true); public Task CreateTestSessionAsync(CreateTestSessionContext context) @@ -238,8 +336,34 @@ public Task CreateTestSessionAsync(CreateTestSessionCon public Task CloseTestSessionAsync(CloseTestSessionContext context) => Task.FromResult(new CloseTestSessionResult() { IsSuccess = true }); - public Task ExecuteRequestAsync(ExecuteRequestContext context) + public async Task ExecuteRequestAsync(ExecuteRequestContext context) { + // Optionally publish a fake "in-progress" test node before crashing so the crash-sequence + // extension has something to record. We do not publish a terminal state (passed/failed/...) + // for these so the controller-side handler can verify them as "tests running at the time + // of the crash". + string? tests = Environment.GetEnvironmentVariable("CRASHDUMP_PUBLISH_INPROGRESS_TESTS"); + if (!string.IsNullOrEmpty(tests) && context.Request is Microsoft.Testing.Platform.Requests.RunTestExecutionRequest runRequest) + { + foreach (string testName in tests.Split(';', StringSplitOptions.RemoveEmptyEntries)) + { + var node = new TestNode + { + Uid = new TestNodeUid(testName), + DisplayName = testName, + }; + node.Properties.Add(InProgressTestNodeStateProperty.CachedInstance); + await context.MessageBus.PublishAsync(this, new TestNodeUpdateMessage(runRequest.Session.SessionUid, node)); + } + + // MessageBus.PublishAsync only enqueues; the async consumer pipeline may not have run + // yet. Give the CrashDumpSequenceLogger a brief opportunity to drain and flush before + // we crash so the sequence file actually contains the STARTED entries this test asserts + // on. This is a test-asset workaround for what is, in production, a best-effort feature + // (the message bus is intentionally asynchronous for throughput reasons). + await Task.Delay(2000); + } + // Optionally spawn a child process that also crashes (and produces its own dump) so we can // exercise the crashdump extension's ability to collect dumps from child processes. if (Environment.GetEnvironmentVariable("CRASHDUMP_SPAWN_CHILD_THAT_CRASHES") == "1") @@ -311,9 +435,16 @@ public Task ExecuteRequestAsync(ExecuteRequestContext context) } } + // Optionally exit gracefully instead of crashing so we can assert that the sequence file is + // cleaned up when no crash occurs. + if (Environment.GetEnvironmentVariable("CRASHDUMP_EXIT_GRACEFULLY") == "1") + { + context.Complete(); + return; + } + Environment.FailFast("CrashDump"); context.Complete(); - return Task.CompletedTask; } } """; diff --git a/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/HelpInfoAllExtensionsTests.cs b/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/HelpInfoAllExtensionsTests.cs index 58f22d91e8..12ed76a93f 100644 --- a/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/HelpInfoAllExtensionsTests.cs +++ b/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/HelpInfoAllExtensionsTests.cs @@ -78,6 +78,10 @@ Control whether ANSI escape characters are emitted. When both --ansi and --no-ansi are provided, --ansi wins. --crash-report [Linux/macOS only] Generate a JSON crash report when the test process crashes. Combine with '--crashdump' to also generate a dump file. Requires .NET 7+ when used alone; .NET 6+ when combined with '--crashdump'. This runtime requirement is not enforced by the tool: on unsupported runtimes no crash report will be emitted. Not supported on Windows due to a .NET runtime limitation (dotnet/runtime#80191). + --crash-sequence + Control whether a sequence file listing the tests started and ended during the test session is generated alongside the crash dump or crash report. + The file makes it possible to identify the tests that were running at the time of the crash without having to inspect the dump. + Valid values are 'on' (default; also accepts 'true', 'enable', '1') or 'off' (also accepts 'false', 'disable', '0'). --crashdump [net6.0+ only] Generate a dump file if the test process crashes --crashdump-filename @@ -358,6 +362,12 @@ Takes one argument as string in the format [h|m|s] where 'value' is float Arity: 0 Hidden: False Description: [Linux/macOS only] Generate a JSON crash report when the test process crashes. Combine with '--crashdump' to also generate a dump file. Requires .NET 7+ when used alone; .NET 6+ when combined with '--crashdump'. This runtime requirement is not enforced by the tool: on unsupported runtimes no crash report will be emitted. Not supported on Windows due to a .NET runtime limitation (dotnet/runtime#80191). + --crash-sequence + Arity: 1 + Hidden: False + Description: Control whether a sequence file listing the tests started and ended during the test session is generated alongside the crash dump or crash report. + The file makes it possible to identify the tests that were running at the time of the crash without having to inspect the dump. + Valid values are 'on' (default; also accepts 'true', 'enable', '1') or 'off' (also accepts 'false', 'disable', '0'). --crashdump Arity: 0 Hidden: False diff --git a/test/UnitTests/Microsoft.Testing.Extensions.UnitTests/CrashDumpTests.cs b/test/UnitTests/Microsoft.Testing.Extensions.UnitTests/CrashDumpTests.cs index d40074c777..9e747ade5b 100644 --- a/test/UnitTests/Microsoft.Testing.Extensions.UnitTests/CrashDumpTests.cs +++ b/test/UnitTests/Microsoft.Testing.Extensions.UnitTests/CrashDumpTests.cs @@ -55,6 +55,7 @@ public async Task CrashDump_CommandLineOptions_Are_Valid_ByDefault() [TestMethod] [DataRow(CrashDumpCommandLineOptions.CrashDumpFileNameOptionName)] [DataRow(CrashDumpCommandLineOptions.CrashDumpTypeOptionName)] + [DataRow(CrashDumpCommandLineOptions.CrashSequenceOptionName)] public async Task Missing_CrashDumpMainOption_ShouldReturn_IsInvalid(string crashDumpArgument) { var provider = new CrashDumpCommandLineProvider(); @@ -71,6 +72,7 @@ public async Task Missing_CrashDumpMainOption_ShouldReturn_IsInvalid(string cras [TestMethod] [DataRow(CrashDumpCommandLineOptions.CrashDumpFileNameOptionName)] [DataRow(CrashDumpCommandLineOptions.CrashDumpTypeOptionName)] + [DataRow(CrashDumpCommandLineOptions.CrashSequenceOptionName)] public async Task If_CrashDumpMainOption_IsSpecified_ShouldReturn_IsValid(string crashDumpArgument) { var provider = new CrashDumpCommandLineProvider(); @@ -85,6 +87,36 @@ public async Task If_CrashDumpMainOption_IsSpecified_ShouldReturn_IsValid(string Assert.IsTrue(string.IsNullOrEmpty(validateOptionsResult.ErrorMessage)); } + [TestMethod] + [DataRow("on")] + [DataRow("off")] + [DataRow("true")] + [DataRow("false")] + [DataRow("enable")] + [DataRow("disable")] + [DataRow("1")] + [DataRow("0")] + public async Task IsValid_If_CrashSequence_Has_CorrectValue(string value) + { + var provider = new CrashDumpCommandLineProvider(); + CommandLineOption option = provider.GetCommandLineOptions().First(x => x.Name == CrashDumpCommandLineOptions.CrashSequenceOptionName); + + ValidationResult validateOptionsResult = await provider.ValidateOptionArgumentsAsync(option, [value]).ConfigureAwait(false); + Assert.IsTrue(validateOptionsResult.IsValid); + Assert.IsTrue(string.IsNullOrEmpty(validateOptionsResult.ErrorMessage)); + } + + [TestMethod] + public async Task IsInvalid_If_CrashSequence_Has_IncorrectValue() + { + var provider = new CrashDumpCommandLineProvider(); + CommandLineOption option = provider.GetCommandLineOptions().First(x => x.Name == CrashDumpCommandLineOptions.CrashSequenceOptionName); + + ValidationResult validateOptionsResult = await provider.ValidateOptionArgumentsAsync(option, ["maybe"]).ConfigureAwait(false); + Assert.IsFalse(validateOptionsResult.IsValid); + Assert.AreEqual(CrashDumpResources.CrashSequenceOptionInvalidArgument, validateOptionsResult.ErrorMessage); + } + [TestMethod] [OSCondition(ConditionMode.Exclude, OperatingSystems.Windows, IgnoreMessage = "Crash report is not supported on Windows (dotnet/runtime#80191)")] [DataRow(CrashDumpCommandLineOptions.CrashDumpFileNameOptionName)] From 39ab3ae8bb3a2ef2943b6e8acba6a867878bbf13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Sat, 23 May 2026 18:18:58 +0200 Subject: [PATCH 2/2] Address VSTHRD103, IDE0330, CA1416 analyzer findings from full pack - Convert sync StreamWriter calls to async (WriteLineAsync/FlushAsync/DisposeAsync) to satisfy VSTHRD103. - Replace plain `lock` with SemaphoreSlim so we can await inside the critical section (also avoids IDE0330 push to System.Threading.Lock which would require multi-TFM gymnastics). - Suppress CA1416 narrowly on the sync Dispose path's SemaphoreSlim.Wait(), explaining that the path is unreachable on browser because the controller-side env var provider requires NETCOREAPP. - Fix CrashDump test asset to use the netfx-compatible char[] overload of String.Split. End-to-end validation: full `build.cmd -pack` now succeeds on origin/main; 19 CrashDumpTests acceptance tests pass (3 Windows-only skips for --crash-report); 9 HelpInfoAllExtensionsTests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../CrashDumpSequenceLogger.cs | 139 +++++++++++++----- .../CrashDumpTests.cs | 2 +- 2 files changed, 102 insertions(+), 39 deletions(-) diff --git a/src/Platform/Microsoft.Testing.Extensions.CrashDump/CrashDumpSequenceLogger.cs b/src/Platform/Microsoft.Testing.Extensions.CrashDump/CrashDumpSequenceLogger.cs index a7f0312ba6..b38f2ed2b1 100644 --- a/src/Platform/Microsoft.Testing.Extensions.CrashDump/CrashDumpSequenceLogger.cs +++ b/src/Platform/Microsoft.Testing.Extensions.CrashDump/CrashDumpSequenceLogger.cs @@ -39,7 +39,10 @@ internal sealed class CrashDumpSequenceLogger : IDataConsumer, ITestSessionLifet private readonly IEnvironment _environment; private readonly IClock _clock; private readonly ILogger _logger; - private readonly object _writeLock = new(); + + // SemaphoreSlim instead of a plain `lock` so we can `await` the write/flush calls inside the + // critical section without blocking a thread on synchronous I/O. + private readonly SemaphoreSlim _writeSemaphore = new(1, 1); private string? _sequenceFilePath; private StreamWriter? _writer; @@ -75,7 +78,7 @@ public Task IsEnabledAsync() return Task.FromResult(!RoslynString.IsNullOrEmpty(_sequenceFilePath)); } - public Task OnTestSessionStartingAsync(ITestSessionContext testSessionContext) + public async Task OnTestSessionStartingAsync(ITestSessionContext testSessionContext) { ApplicationStateGuard.Ensure(_sequenceFilePath is not null); @@ -97,8 +100,8 @@ public Task OnTestSessionStartingAsync(ITestSessionContext testSessionContext) // because a sequence file is not required to survive an OS-level crash or power loss. var fileStream = new FileStream(_sequenceFilePath, FileMode.Create, FileAccess.Write, FileShare.Read); _writer = new StreamWriter(fileStream, Encoding.UTF8) { AutoFlush = true }; - _writer.WriteLine(FileHeader); - _writer.Flush(); + await _writer.WriteLineAsync(FileHeader).ConfigureAwait(false); + await _writer.FlushAsync().ConfigureAwait(false); } catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or ArgumentException or NotSupportedException or PathTooLongException or DirectoryNotFoundException or System.Security.SecurityException) { @@ -106,25 +109,30 @@ public Task OnTestSessionStartingAsync(ITestSessionContext testSessionContext) // full, ACLs deny write, the path is invalid, or any other filesystem-level error), we // trace the failure and behave as if the feature were disabled — failing the test run // for this would be worse than missing the diagnostic. - _logger.LogWarning($"Failed to open crash sequence file '{_sequenceFilePath}': {ex.Message}"); - _writer?.Dispose(); - _writer = null; + await _logger.LogWarningAsync($"Failed to open crash sequence file '{_sequenceFilePath}': {ex.Message}").ConfigureAwait(false); + if (_writer is not null) + { +#if NETCOREAPP + await _writer.DisposeAsync().ConfigureAwait(false); +#else + _writer.Dispose(); +#endif + _writer = null; + } } - - return Task.CompletedTask; } - public Task ConsumeAsync(IDataProducer dataProducer, IData value, CancellationToken cancellationToken) + public async Task ConsumeAsync(IDataProducer dataProducer, IData value, CancellationToken cancellationToken) { if (_writer is null || value is not TestNodeUpdateMessage update) { - return Task.CompletedTask; + return; } TestNodeStateProperty? state = update.TestNode.Properties.SingleOrDefault(); if (state is null) { - return Task.CompletedTask; + return; } string? line = state switch @@ -143,24 +151,25 @@ public Task ConsumeAsync(IDataProducer dataProducer, IData value, CancellationTo if (line is null) { - return Task.CompletedTask; + return; } // ConsumeAsync may be invoked concurrently from multiple data producers/threads; serialize // writes to keep the on-disk record consistent. Writes are tiny (one line each) so contention // is negligible. - lock (_writeLock) + await _writeSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); + try { - // Re-check under the lock to defend against a concurrent Dispose closing the writer + // Re-check under the semaphore to defend against a concurrent Dispose closing the writer // between the null check above and the write here. if (_writer is null) { - return Task.CompletedTask; + return; } try { - _writer.WriteLine(line); + await _writer.WriteLineAsync(line).ConfigureAwait(false); } catch (ObjectDisposedException) { @@ -169,35 +178,43 @@ public Task ConsumeAsync(IDataProducer dataProducer, IData value, CancellationTo catch (IOException ex) { // Best-effort logging only: dropping a single record is better than failing the test run. - _logger.LogWarning($"Failed to write to crash sequence file '{_sequenceFilePath}': {ex.Message}"); + await _logger.LogWarningAsync($"Failed to write to crash sequence file '{_sequenceFilePath}': {ex.Message}").ConfigureAwait(false); } } - - return Task.CompletedTask; + finally + { + _writeSemaphore.Release(); + } } - public Task OnTestSessionFinishingAsync(ITestSessionContext testSessionContext) + public async Task OnTestSessionFinishingAsync(ITestSessionContext testSessionContext) { // Flush remaining bytes; the controller-side handler decides whether to publish (on crash) or // delete (on graceful exit) the file. We deliberately do not delete from here so that a crash // *during* finishing still leaves the sequence file behind for the controller to inspect. - lock (_writeLock) + await _writeSemaphore.WaitAsync(testSessionContext.CancellationToken).ConfigureAwait(false); + try { - try - { - _writer?.Flush(); - } - catch (ObjectDisposedException) - { - // Ignore - already disposed. - } - catch (IOException) + if (_writer is not null) { - // Best-effort - ignore final flush failure. + try + { + await _writer.FlushAsync().ConfigureAwait(false); + } + catch (ObjectDisposedException) + { + // Ignore - already disposed. + } + catch (IOException) + { + // Best-effort - ignore final flush failure. + } } } - - return Task.CompletedTask; + finally + { + _writeSemaphore.Release(); + } } internal static string FormatLine(string eventName, DateTimeOffset timestamp, TestNodeUid uid, string lastField) @@ -214,10 +231,41 @@ private static string Sanitize(string value) => value.Replace('\t', ' ').Replace('\r', ' ').Replace('\n', ' '); #if NETCOREAPP - public ValueTask DisposeAsync() + public async ValueTask DisposeAsync() { - Dispose(); - return ValueTask.CompletedTask; + if (_disposed) + { + return; + } + + await _writeSemaphore.WaitAsync().ConfigureAwait(false); + try + { + if (_disposed) + { + return; + } + + _disposed = true; + if (_writer is not null) + { + try + { + await _writer.DisposeAsync().ConfigureAwait(false); + } + catch (IOException) + { + // Best-effort - ignore close failures. + } + + _writer = null; + } + } + finally + { + _writeSemaphore.Release(); + _writeSemaphore.Dispose(); + } } #endif @@ -228,7 +276,17 @@ public void Dispose() return; } - lock (_writeLock) + // Synchronous fallback for non-NETCOREAPP targets and for callers that don't observe + // IAsyncDisposable. We use the synchronous Wait() / Dispose() pair here intentionally: + // there is no async context to await on, and the wait is bounded by the brief duration of + // any concurrent ConsumeAsync write. + // CA1416: SemaphoreSlim.Wait() is unsupported on 'browser'; this code path is unreachable on + // browser because IsEnabledAsync returns false there (the controller-side env var provider + // requires a NETCOREAPP runtime which excludes browser). +#pragma warning disable CA1416 + _writeSemaphore.Wait(); +#pragma warning restore CA1416 + try { if (_disposed) { @@ -247,5 +305,10 @@ public void Dispose() _writer = null; } + finally + { + _writeSemaphore.Release(); + _writeSemaphore.Dispose(); + } } } diff --git a/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/CrashDumpTests.cs b/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/CrashDumpTests.cs index aeb6937d64..6f3372c118 100644 --- a/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/CrashDumpTests.cs +++ b/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/CrashDumpTests.cs @@ -345,7 +345,7 @@ public async Task ExecuteRequestAsync(ExecuteRequestContext context) string? tests = Environment.GetEnvironmentVariable("CRASHDUMP_PUBLISH_INPROGRESS_TESTS"); if (!string.IsNullOrEmpty(tests) && context.Request is Microsoft.Testing.Platform.Requests.RunTestExecutionRequest runRequest) { - foreach (string testName in tests.Split(';', StringSplitOptions.RemoveEmptyEntries)) + foreach (string testName in tests.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries)) { var node = new TestNode {