diff --git a/src/Logging/Logging.Testing/src/CollectDumpAttribute.cs b/src/Logging/Logging.Testing/src/CollectDumpAttribute.cs new file mode 100644 index 0000000000..8a6aa84bac --- /dev/null +++ b/src/Logging/Logging.Testing/src/CollectDumpAttribute.cs @@ -0,0 +1,18 @@ +// Copyright(c) .NET Foundation.All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.Extensions.Logging.Testing +{ + /// + /// Capture the memory dump upon test failure. + /// + /// + /// This currently only works in Windows environments + /// + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] + public class CollectDumpAttribute : Attribute + { + } +} diff --git a/src/Logging/Logging.Testing/src/DumpCollector/DumpCollector.Windows.cs b/src/Logging/Logging.Testing/src/DumpCollector/DumpCollector.Windows.cs new file mode 100644 index 0000000000..8d4168c20c --- /dev/null +++ b/src/Logging/Logging.Testing/src/DumpCollector/DumpCollector.Windows.cs @@ -0,0 +1,76 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; +using Microsoft.Win32.SafeHandles; + +namespace Microsoft.Extensions.Logging.Testing +{ + public static partial class DumpCollector + { + private static class Windows + { + internal static void Collect(Process process, string outputFile) + { + // Open the file for writing + using (var stream = new FileStream(outputFile, FileMode.Create, FileAccess.ReadWrite, FileShare.None)) + { + // Dump the process! + var exceptionInfo = new NativeMethods.MINIDUMP_EXCEPTION_INFORMATION(); + if (!NativeMethods.MiniDumpWriteDump(process.Handle, (uint)process.Id, stream.SafeFileHandle, NativeMethods.MINIDUMP_TYPE.MiniDumpWithFullMemory, ref exceptionInfo, IntPtr.Zero, IntPtr.Zero)) + { + var err = Marshal.GetHRForLastWin32Error(); + Marshal.ThrowExceptionForHR(err); + } + } + } + + private static class NativeMethods + { + [DllImport("Dbghelp.dll")] + public static extern bool MiniDumpWriteDump(IntPtr hProcess, uint ProcessId, SafeFileHandle hFile, MINIDUMP_TYPE DumpType, ref MINIDUMP_EXCEPTION_INFORMATION ExceptionParam, IntPtr UserStreamParam, IntPtr CallbackParam); + + [StructLayout(LayoutKind.Sequential, Pack = 4)] + public struct MINIDUMP_EXCEPTION_INFORMATION + { + public uint ThreadId; + public IntPtr ExceptionPointers; + public int ClientPointers; + } + + [Flags] + public enum MINIDUMP_TYPE : uint + { + MiniDumpNormal = 0, + MiniDumpWithDataSegs = 1 << 0, + MiniDumpWithFullMemory = 1 << 1, + MiniDumpWithHandleData = 1 << 2, + MiniDumpFilterMemory = 1 << 3, + MiniDumpScanMemory = 1 << 4, + MiniDumpWithUnloadedModules = 1 << 5, + MiniDumpWithIndirectlyReferencedMemory = 1 << 6, + MiniDumpFilterModulePaths = 1 << 7, + MiniDumpWithProcessThreadData = 1 << 8, + MiniDumpWithPrivateReadWriteMemory = 1 << 9, + MiniDumpWithoutOptionalData = 1 << 10, + MiniDumpWithFullMemoryInfo = 1 << 11, + MiniDumpWithThreadInfo = 1 << 12, + MiniDumpWithCodeSegs = 1 << 13, + MiniDumpWithoutAuxiliaryState = 1 << 14, + MiniDumpWithFullAuxiliaryState = 1 << 15, + MiniDumpWithPrivateWriteCopyMemory = 1 << 16, + MiniDumpIgnoreInaccessibleMemory = 1 << 17, + MiniDumpWithTokenInformation = 1 << 18, + MiniDumpWithModuleHeaders = 1 << 19, + MiniDumpFilterTriage = 1 << 20, + MiniDumpWithAvxXStateContext = 1 << 21, + MiniDumpWithIptTrace = 1 << 22, + MiniDumpValidTypeFlags = (-1) ^ ((~1) << 22) + } + } + } + } +} diff --git a/src/Logging/Logging.Testing/src/DumpCollector/DumpCollector.cs b/src/Logging/Logging.Testing/src/DumpCollector/DumpCollector.cs new file mode 100644 index 0000000000..67043ed827 --- /dev/null +++ b/src/Logging/Logging.Testing/src/DumpCollector/DumpCollector.cs @@ -0,0 +1,20 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace Microsoft.Extensions.Logging.Testing +{ + public static partial class DumpCollector + { + public static void Collect(Process process, string fileName) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + Windows.Collect(process, fileName); + } + // No implementations yet for macOS and Linux + } + } +} diff --git a/src/Logging/Logging.Testing/src/Xunit/LoggedTestInvoker.cs b/src/Logging/Logging.Testing/src/Xunit/LoggedTestInvoker.cs index 708db37105..788c38b306 100644 --- a/src/Logging/Logging.Testing/src/Xunit/LoggedTestInvoker.cs +++ b/src/Logging/Logging.Testing/src/Xunit/LoggedTestInvoker.cs @@ -1,8 +1,10 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.Collections.Generic; +using System.Diagnostics; +using System.IO; using System.Linq; using System.Reflection; using System.Threading; @@ -15,6 +17,7 @@ namespace Microsoft.Extensions.Logging.Testing { private readonly ITestOutputHelper _output; private readonly RetryContext _retryContext; + private readonly bool _collectDumpOnFailure; public LoggedTestInvoker( ITest test, @@ -27,11 +30,13 @@ namespace Microsoft.Extensions.Logging.Testing ExceptionAggregator aggregator, CancellationTokenSource cancellationTokenSource, ITestOutputHelper output, - RetryContext retryContext) + RetryContext retryContext, + bool collectDumpOnFailure) : base(test, messageBus, testClass, constructorArguments, testMethod, testMethodArguments, beforeAfterAttributes, aggregator, cancellationTokenSource) { _output = output; _retryContext = retryContext; + _collectDumpOnFailure = collectDumpOnFailure; } protected override object CreateTestClass() @@ -63,5 +68,25 @@ namespace Microsoft.Extensions.Logging.Testing return testClass; } + + protected override object CallTestMethod(object testClassInstance) + { + try + { + return base.CallTestMethod(testClassInstance); + } + catch + { + if (_collectDumpOnFailure && testClassInstance is LoggedTestBase loggedTestBase) + { + var path = Path.Combine(loggedTestBase.ResolvedLogOutputDirectory, loggedTestBase.ResolvedTestMethodName + ".dmp"); + var process = Process.GetCurrentProcess(); + + DumpCollector.Collect(process, path); + } + + throw; + } + } } } diff --git a/src/Logging/Logging.Testing/src/Xunit/LoggedTestRunner.cs b/src/Logging/Logging.Testing/src/Xunit/LoggedTestRunner.cs index 7dc847a113..11537eb679 100644 --- a/src/Logging/Logging.Testing/src/Xunit/LoggedTestRunner.cs +++ b/src/Logging/Logging.Testing/src/Xunit/LoggedTestRunner.cs @@ -50,9 +50,11 @@ namespace Microsoft.Extensions.Logging.Testing private async Task InvokeTestMethodAsync(ExceptionAggregator aggregator, ITestOutputHelper output) { var retryAttribute = GetRetryAttribute(TestMethod); + var collectDump = TestMethod.GetCustomAttribute() != null; + if (!typeof(LoggedTestBase).IsAssignableFrom(TestClass) || retryAttribute == null) { - return await new LoggedTestInvoker(Test, MessageBus, TestClass, ConstructorArguments, TestMethod, TestMethodArguments, BeforeAfterAttributes, aggregator, CancellationTokenSource, output, null).RunAsync(); + return await new LoggedTestInvoker(Test, MessageBus, TestClass, ConstructorArguments, TestMethod, TestMethodArguments, BeforeAfterAttributes, aggregator, CancellationTokenSource, output, null, collectDump).RunAsync(); } var retryPredicateMethodName = retryAttribute.RetryPredicateName; @@ -75,7 +77,7 @@ namespace Microsoft.Extensions.Logging.Testing }; var retryAggregator = new ExceptionAggregator(); - var loggedTestInvoker = new LoggedTestInvoker(Test, MessageBus, TestClass, ConstructorArguments, TestMethod, TestMethodArguments, BeforeAfterAttributes, retryAggregator, CancellationTokenSource, output, retryContext); + var loggedTestInvoker = new LoggedTestInvoker(Test, MessageBus, TestClass, ConstructorArguments, TestMethod, TestMethodArguments, BeforeAfterAttributes, retryAggregator, CancellationTokenSource, output, retryContext, collectDump); var totalTime = 0.0M; do @@ -95,7 +97,6 @@ namespace Microsoft.Extensions.Logging.Testing return totalTime; } - private RetryTestAttribute GetRetryAttribute(MethodInfo methodInfo) { var os = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? OperatingSystems.MacOSX