Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Debug.Assert in test method crashes full process #3955

Open
Evangelink opened this issue Oct 21, 2024 · 6 comments
Open

Debug.Assert in test method crashes full process #3955

Evangelink opened this issue Oct 21, 2024 · 6 comments

Comments

@Evangelink
Copy link
Member

Describe the bug

Debug.Assert in test method crashes full process

Steps To Reproduce

[TestClass]
    public sealed class Test1
    {
        [TestMethod]
        public void TM()
        {
            Debug.Assert(false);
        }
    }

Output:

MSTest v3.6.1 (UTC 03/10/2024) [win-x64 - .NET 9.0.0-rc.2.24473.5]
Process terminated. Assertion Failed
false
   at TestProject8.Test1.TM() in C:\src\temp\TestProject8\TestProject8\Test1.cs:line 11
   at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor)
   at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr)
   at System.Reflection.RuntimeMethodInfo.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
   at System.Reflection.MethodBase.Invoke(Object obj, Object[] parameters)
   at Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Extensions.MethodInfoExtensions.InvokeAsSynchronousTask(MethodInfo methodInfo, Object classInstance, Object[] arguments) in /_/src/Adapter/MSTest.TestAdapter/Extensions/MethodInfoExtensions.cs:line 181
   at Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Execution.TestMethodInfo.ExecuteInternal(Object[] arguments) in /_/src/Adapter/MSTest.TestAdapter/Execution/TestMethodInfo.cs:line 252
   at Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Execution.TestMethodInfo.Invoke(Object[] arguments) in /_/src/Adapter/MSTest.TestAdapter/Execution/TestMethodInfo.cs:line 115
   at Microsoft.VisualStudio.TestTools.UnitTesting.TestMethodAttribute.Execute(ITestMethod testMethod) in /_/src/TestFramework/TestFramework/Attributes/TestMethod/TestMethodAttribute.cs:line 39
   at Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Execution.TestMethodRunner.ExecuteTest(TestMethodInfo testMethodInfo) in /_/src/Adapter/MSTest.TestAdapter/Execution/TestMethodRunner.cs:line 418
   at Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Execution.TestMethodRunner.RunTestMethod() in /_/src/Adapter/MSTest.TestAdapter/Execution/TestMethodRunner.cs:line 185
   at Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Execution.TestMethodRunner.<>c__DisplayClass4_0.<Execute>g__SafeRunTestMethod|0(String initializationLogs, String initializationErrorLogs, String initializationTrace, String initializationTestContextMessages) in /_/src/Adapter/MSTest.TestAdapter/Execution/TestMethodRunner.cs:line 120
   at Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Execution.TestMethodRunner.Execute(String initializationLogs, String initializationErrorLogs, String initializationTrace, String initializationTestContextMessages) in /_/src/Adapter/MSTest.TestAdapter/Execution/TestMethodRunner.cs:line 110
   at Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Execution.UnitTestRunner.RunSingleTest(TestMethod testMethod, IDictionary`2 testContextProperties) in /_/src/Adapter/MSTest.TestAdapter/Execution/UnitTestRunner.cs:line 180
   at Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Execution.TestExecutionManager.ExecuteTestsWithTestRunner(IEnumerable`1 tests, ITestExecutionRecorder testExecutionRecorder, String source, IDictionary`2 sourceLevelParameters, UnitTestRunner testRunner) in /_/src/Adapter/MSTest.TestAdapter/Execution/TestExecutionManager.cs:line 438
   at Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Execution.TestExecutionManager.ExecuteTestsInSource(IEnumerable`1 tests, IRunContext runContext, IFrameworkHandle frameworkHandle, String source, Boolean isDeploymentDone) in /_/src/Adapter/MSTest.TestAdapter/Execution/TestExecutionManager.cs:line 394
   at Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Execution.TestExecutionManager.ExecuteTests(IEnumerable`1 tests, IRunContext runContext, IFrameworkHandle frameworkHandle, Boolean isDeploymentDone) in /_/src/Adapter/MSTest.TestAdapter/Execution/TestExecutionManager.cs:line 173
   at Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Execution.TestExecutionManager.RunTests(IEnumerable`1 sources, IRunContext runContext, IFrameworkHandle frameworkHandle, TestRunCancellationToken cancellationToken) in /_/src/Adapter/MSTest.TestAdapter/Execution/TestExecutionManager.cs:line 149
   at Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.MSTestExecutor.<>c__DisplayClass9_0.<RunTests>b__0(TestRunCancellationToken testRunToken) in /_/src/Adapter/MSTest.TestAdapter/VSTestAdapter/MSTestExecutor.cs:line 72
   at Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.MSTestExecutor.<>c__DisplayClass11_0.<RunTestsFromRightContext>g__DoRunTests|0() in /_/src/Adapter/MSTest.TestAdapter/VSTestAdapter/MSTestExecutor.cs:line 126
   at Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.MSTestExecutor.RunTestsFromRightContext(IFrameworkHandle frameworkHandle, Action`1 runTestsAction) in /_/src/Adapter/MSTest.TestAdapter/VSTestAdapter/MSTestExecutor.cs:line 115
   at Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.MSTestExecutor.RunTests(IEnumerable`1 sources, IRunContext runContext, IFrameworkHandle frameworkHandle) in /_/src/Adapter/MSTest.TestAdapter/VSTestAdapter/MSTestExecutor.cs:line 72
   at Microsoft.VisualStudio.TestTools.UnitTesting.MSTestBridgedTestFramework.SynchronizedRunTestsAsync(VSTestRunTestExecutionRequest request, IMessageBus messageBus, CancellationToken cancellationToken) in /_/src/Adapter/MSTest.TestAdapter/TestingPlatformAdapter/MSTestBridgedTestFramework.cs:line 56
   at Microsoft.Testing.Extensions.VSTestBridge.SynchronizedSingleSessionVSTestBridgedTestFramework.<>c__DisplayClass22_0.<<ExecuteRequestAsync>b__0>d.MoveNext() in /_/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/SynchronizedSingleSessionVSTestAndTestAnywhereAdapter.cs:line 124
   at System.Runtime.CompilerServices.AsyncMethodBuilderCore.Start[TStateMachine](TStateMachine& stateMachine)
   at Microsoft.Testing.Extensions.VSTestBridge.SynchronizedSingleSessionVSTestBridgedTestFramework.<>c__DisplayClass22_0.<ExecuteRequestAsync>b__0()
   at Microsoft.Testing.Extensions.VSTestBridge.SynchronizedSingleSessionVSTestBridgedTestFramework.ExecuteRequestWithRequestCountGuardAsync(Func`1 asyncFunc) in /_/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/SynchronizedSingleSessionVSTestAndTestAnywhereAdapter.cs:line 145
   at System.Runtime.CompilerServices.AsyncMethodBuilderCore.Start[TStateMachine](TStateMachine& stateMachine)
   at Microsoft.Testing.Extensions.VSTestBridge.SynchronizedSingleSessionVSTestBridgedTestFramework.ExecuteRequestWithRequestCountGuardAsync(Func`1 asyncFunc)
   at Microsoft.Testing.Extensions.VSTestBridge.SynchronizedSingleSessionVSTestBridgedTestFramework.ExecuteRequestAsync(TestExecutionRequest request, IMessageBus messageBus, CancellationToken cancellationToken) in /_/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/SynchronizedSingleSessionVSTestAndTestAnywhereAdapter.cs:line 108
   at Microsoft.Testing.Extensions.VSTestBridge.VSTestBridgedTestFrameworkBase.ExecuteRequestAsync(ExecuteRequestContext context) in /_/src/Platform/Microsoft.Testing.Extensions.VSTestBridge/VSTestBridgedTestFrameworkBase.cs:line 72
   at System.Runtime.CompilerServices.AsyncMethodBuilderCore.Start[TStateMachine](TStateMachine& stateMachine)
   at Microsoft.Testing.Extensions.VSTestBridge.VSTestBridgedTestFrameworkBase.ExecuteRequestAsync(ExecuteRequestContext context)
   at Microsoft.Testing.Platform.Requests.TestHostTestFrameworkInvoker.ExecuteRequestAsync(ITestFramework testFramework, TestExecutionRequest request, IMessageBus messageBus, CancellationToken cancellationToken) in /_/src/Platform/Microsoft.Testing.Platform/Requests/TestHostTestFrameworkInvoker.cs:line 73
   at System.Runtime.CompilerServices.AsyncMethodBuilderCore.Start[TStateMachine](TStateMachine& stateMachine)
   at Microsoft.Testing.Platform.Requests.TestHostTestFrameworkInvoker.ExecuteRequestAsync(ITestFramework testFramework, TestExecutionRequest request, IMessageBus messageBus, CancellationToken cancellationToken)
   at Microsoft.Testing.Platform.Requests.TestHostTestFrameworkInvoker.ExecuteAsync(ITestFramework testFramework, ClientInfo client, CancellationToken cancellationToken) in /_/src/Platform/Microsoft.Testing.Platform/Requests/TestHostTestFrameworkInvoker.cs:line 62
   at System.Runtime.CompilerServices.AsyncMethodBuilderCore.Start[TStateMachine](TStateMachine& stateMachine)
   at Microsoft.Testing.Platform.Requests.TestHostTestFrameworkInvoker.ExecuteAsync(ITestFramework testFramework, ClientInfo client, CancellationToken cancellationToken)
   at Microsoft.Testing.Platform.Hosts.CommonTestHost.ExecuteRequestAsync(IPlatformOutputDevice outputDevice, ITestSessionContext testSessionInfo, ServiceProvider serviceProvider, BaseMessageBus baseMessageBus, ITestFramework testFramework, ClientInfo client) in /_/src/Platform/Microsoft.Testing.Platform/Hosts/CommonTestHost.cs:line 111
   at System.Runtime.CompilerServices.AsyncMethodBuilderCore.Start[TStateMachine](TStateMachine& stateMachine)
   at Microsoft.Testing.Platform.Hosts.CommonTestHost.ExecuteRequestAsync(IPlatformOutputDevice outputDevice, ITestSessionContext testSessionInfo, ServiceProvider serviceProvider, BaseMessageBus baseMessageBus, ITestFramework testFramework, ClientInfo client)
   at Microsoft.Testing.Platform.Hosts.ConsoleTestHost.InternalRunAsync() in /_/src/Platform/Microsoft.Testing.Platform/Hosts/ConsoleTestHost.cs:line 84
   at System.Runtime.CompilerServices.AsyncMethodBuilderCore.Start[TStateMachine](TStateMachine& stateMachine)
   at Microsoft.Testing.Platform.Hosts.ConsoleTestHost.InternalRunAsync()
   at Microsoft.Testing.Platform.Hosts.CommonTestHost.RunTestAppAsync(CancellationToken testApplicationCancellationToken) in /_/src/Platform/Microsoft.Testing.Platform/Hosts/CommonTestHost.cs:line 85
   at System.Runtime.CompilerServices.AsyncMethodBuilderCore.Start[TStateMachine](TStateMachine& stateMachine)
   at Microsoft.Testing.Platform.Hosts.CommonTestHost.RunTestAppAsync(CancellationToken testApplicationCancellationToken)
   at Microsoft.Testing.Platform.Hosts.CommonTestHost.RunAsync() in /_/src/Platform/Microsoft.Testing.Platform/Hosts/CommonTestHost.cs:line 33
   at System.Runtime.CompilerServices.AsyncMethodBuilderCore.Start[TStateMachine](TStateMachine& stateMachine)
   at Microsoft.Testing.Platform.Hosts.CommonTestHost.RunAsync()
   at Microsoft.Testing.Platform.Builder.TestApplication.RunAsync() in /_/src/Platform/Microsoft.Testing.Platform/Builder/TestApplication.cs:line 240
   at System.Runtime.CompilerServices.AsyncMethodBuilderCore.Start[TStateMachine](TStateMachine& stateMachine)
   at Microsoft.Testing.Platform.Builder.TestApplication.RunAsync()
   at TestingPlatformEntryPoint.Main(String[] args) in C:\src\temp\TestProject8\TestProject8\obj\Debug\net9.0\TestPlatformEntryPoint.cs:line 16
   at System.Runtime.CompilerServices.AsyncMethodBuilderCore.Start[TStateMachine](TStateMachine& stateMachine)
   at TestingPlatformEntryPoint.Main(String[] args)
   at TestingPlatformEntryPoint.<Main>(String[] args)

Expected behavior

No issue

Actual behavior

Process crashes

@nohwnd
Copy link
Member

nohwnd commented Oct 21, 2024

This is by design, Debug.Assert behaves like this, if you are not debugging. I don't think it is great experience (especially for tests) because either:

  • you are in interactive session and you get a pop-up (especially annoying if you run in CI with interactive mode where you block tests forever), or if 1000 tests fail on the assertion and you get 1000 pop-ups
  • you are in non-interactive mode => crash
  • you are debugging => condition is skipped

To prevent this you need to register a trace listener and handle the error there. I did that in VSTest previously, but admittedly my solution is not great because it will translate the assert to exception that can be handled.

Instead, it would be better, if the listener threw exception, but also told the test that it must fail, even if the underlying code handles the exception.

E.g. using async local storage to pick up the state of the current test.

@Evangelink
Copy link
Member Author

Just a quick note that I simplified the customer problem for the sake of the repro. In their case, the Debug.Assert call is part of the main/production code so they cannot use assertions instead.

@Liamdoult
Copy link

We use Debug.Assert in our code base to validate that the desired state was reached. If not, this Debug.Assert is thrown. This is used in both tests and when running code outside of tests.

But when our debug code throws this exception during tests, the test host is killed and our tests cannot continue. In NUnit (where we are migrating from), the test would be marked as failed and other tests continue.

@nohwnd
Copy link
Member

nohwnd commented Oct 22, 2024

I don't see any mention of NUnit handling the Assert calls, do you happen to know where they do it?

Running via nunit3-console it crashes the child dotnet.exe process as well.

Image

I think you might be referring to running NUnit tests with VSTest (e.g. with dotnet test, in VS, or with vstest.console). In those cases the solution described in the comment above is used, which will prevent the process from crashing.

Image

But it also does not prevent the assertion exception from being handled, which I think is incorrect. The test below will pass, but it should fail.

[Test]
public void Test1()
{
    try
    {
        Debug.Fail("OOOOOOOOOOOOOOOOh!");
    }
    catch
    {

    }
}

@nohwnd
Copy link
Member

nohwnd commented Oct 22, 2024

@Evangelink I think this needs more design to solve properly, would you be willing to mark it as feature and move it to 3.7?

To solve this properly we need to establish some context for the test that is accessible globally (or based on async local storage) and put the data about failure there. If this is done, such storage is useful for other things as well, e.g. for soft assertions.

@nohwnd
Copy link
Member

nohwnd commented Oct 22, 2024

@Liamdoult if this is a blocker for you, you can simply grab the implementation from vstest, and just use Assembly initialize to put it in place. VSTest does just that, but built-in:

// file Test1.cs
using Microsoft.VisualStudio.TestPlatform.TestHost;
using System.Diagnostics;

namespace TestProject93
{
    [TestClass]
    public sealed class Test1
    {
        [TestMethod]
        public void TestMethod1()
        {
            Debug.Fail("OOOOOh");
        }

        [AssemblyInitialize]
        public static void Setup(TestContext _)
        {
           TestHostTraceListener.Setup();
        }
    }
}
// file DebugAssertListener.cs
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.


namespace Microsoft.VisualStudio.TestPlatform.TestHost
{
#if NETCOREAPP
    using System;
    using System.Diagnostics;
    using System.Linq;
    using System.Reflection;

    internal class TestHostTraceListener : DefaultTraceListener
    {
        public static void Setup()
        {
            // in the majority of cases there will be only a single DefaultTraceListener in this collection
            // and we will replace that with our listener, in case there are listeners of different types we keep
            // them as is
            for (var i = 0; i < Trace.Listeners.Count; i++)
            {
                var listener = Trace.Listeners[i];
                if (listener is DefaultTraceListener)
                {
                    Trace.Listeners[i] = new TestHostTraceListener(); 
                }
            }
        }

        public override void Fail(string message)
        {
            throw GetException(message);
        }

        public override void Fail(string message, string detailMessage)
        {
            throw GetException((message + Environment.NewLine + detailMessage));
        }

        public static void ShowDialog(string stackTrace, string message, string detailMessage, string _)
        {
            var text = !string.IsNullOrEmpty(message)
                ? !string.IsNullOrEmpty(detailMessage)
                    ? (message + Environment.NewLine + detailMessage)
                    : message
                : null;
            throw GetException(text);
        }

        private static DebugAssertException GetException(string message)
        {
            var debugTypes = new Type[] { typeof(Debug), typeof(Trace) };
            var stack = new StackTrace(true);

            var debugMethodFound = false;
            var frameCount = 0;
            MethodBase method = null;
            foreach (var f in stack.GetFrames())
            {
                var m = f.GetMethod();
                var declaringType = m?.DeclaringType;
                if (!debugMethodFound && (declaringType == typeof(Debug) || declaringType == typeof(Trace)))
                {
                    method = m;
                    debugMethodFound = true;
                }

                if (debugMethodFound)
                {
                    frameCount++;
                }
            }

            var stackTrace = string.Join(Environment.NewLine, stack.ToString().Split(Environment.NewLine).TakeLast(frameCount));
            var methodName = method != null ? $"{method.DeclaringType.Name}.{method.Name}" : "<method>";
            var wholeMessage = $"Method {methodName} failed with '{message}', and was translated to {typeof(DebugAssertException).FullName} to avoid terminating the process hosting the test.";

            return new DebugAssertException(wholeMessage, stackTrace);
        }
    }

    internal sealed class DebugAssertException : Exception
    {
        public DebugAssertException(string message, string stackTrace) : base(message)
        {
            StackTrace = stackTrace;
        }

        public override string StackTrace { get; }
    }

#endif
}

TestProject93.zip

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants