diff --git a/src/Microsoft.TestPlatform.CrossPlatEngine/Execution/BaseRunTests.cs b/src/Microsoft.TestPlatform.CrossPlatEngine/Execution/BaseRunTests.cs index 99a570dbf3..9e640745d7 100644 --- a/src/Microsoft.TestPlatform.CrossPlatEngine/Execution/BaseRunTests.cs +++ b/src/Microsoft.TestPlatform.CrossPlatEngine/Execution/BaseRunTests.cs @@ -9,6 +9,7 @@ using System.Globalization; using System.IO; using System.Linq; +using System.Reflection; using System.Threading.Tasks; using Microsoft.VisualStudio.TestPlatform.Common.ExtensionDecorators; @@ -218,7 +219,14 @@ public void RunTests() { EqtTrace.Error("BaseRunTests.RunTests: Failed to run the tests. Reason: {0}.", ex); - exception = new Exception(ex.Message, ex.InnerException); + // RunTestsInternal can surface a TargetInvocationException when a test extension is + // instantiated via reflection and its constructor throws. Unwrap that wrapper to the + // real exception so callers don't see the reflection noise. Any other exception is + // preserved as-is so its concrete type and stack trace are not lost on the way out. + Exception realException = ex is TargetInvocationException tie && tie.InnerException is not null + ? tie.InnerException + : ex; + exception = new Exception(realException.Message, realException); isAborted = true; } finally diff --git a/test/Microsoft.TestPlatform.CrossPlatEngine.UnitTests/Execution/BaseRunTestsTests.cs b/test/Microsoft.TestPlatform.CrossPlatEngine.UnitTests/Execution/BaseRunTestsTests.cs index b72c2abb67..bf6a959eac 100644 --- a/test/Microsoft.TestPlatform.CrossPlatEngine.UnitTests/Execution/BaseRunTestsTests.cs +++ b/test/Microsoft.TestPlatform.CrossPlatEngine.UnitTests/Execution/BaseRunTestsTests.cs @@ -7,6 +7,7 @@ using System.Globalization; using System.IO; using System.Linq; +using System.Reflection; using Microsoft.TestPlatform.TestUtilities; using Microsoft.VisualStudio.TestPlatform.Common.ExtensionFramework; @@ -159,6 +160,72 @@ public void RunTestsShouldRaiseTestRunCompleteWithAbortedAsTrueOnException() Assert.IsTrue(receivedCompleteArgs.IsAborted); } + [TestMethod] + public void RunTestsShouldPreserveOriginalExceptionAsInnerException() + { + TestRunCompleteEventArgs? receivedCompleteArgs = null; + var originalException = new NotImplementedException("original message"); + + // Setup mocks. + _runTestsInstance.GetExecutorUriExtensionMapCallback = (fh, rc) => throw originalException; + _mockTestRunEventsHandler.Setup( + treh => + treh.HandleTestRunComplete( + It.IsAny(), + It.IsAny(), + It.IsAny>(), + It.IsAny>())) + .Callback( + ( + TestRunCompleteEventArgs complete, + TestRunChangedEventArgs stats, + ICollection attachments, + ICollection executorUris) => receivedCompleteArgs = complete); + + _runTestsInstance.RunTests(); + + Assert.IsNotNull(receivedCompleteArgs); + Assert.IsTrue(receivedCompleteArgs.IsAborted); + Assert.IsNotNull(receivedCompleteArgs.Error); + Assert.AreSame(originalException, receivedCompleteArgs.Error!.InnerException, + "The original exception should be preserved as the inner exception of the wrapper."); + } + + [TestMethod] + public void RunTestsShouldUnwrapTargetInvocationExceptionToTheRealException() + { + TestRunCompleteEventArgs? receivedCompleteArgs = null; + var realException = new NotImplementedException("real message"); + + // A TargetInvocationException is what reflection-based extension instantiation throws when + // a constructor fails; the wrapper itself is noise, the real exception is the inner one. + var reflectionWrapper = new TargetInvocationException(realException); + + // Setup mocks. + _runTestsInstance.GetExecutorUriExtensionMapCallback = (fh, rc) => throw reflectionWrapper; + _mockTestRunEventsHandler.Setup( + treh => + treh.HandleTestRunComplete( + It.IsAny(), + It.IsAny(), + It.IsAny>(), + It.IsAny>())) + .Callback( + ( + TestRunCompleteEventArgs complete, + TestRunChangedEventArgs stats, + ICollection attachments, + ICollection executorUris) => receivedCompleteArgs = complete); + + _runTestsInstance.RunTests(); + + Assert.IsNotNull(receivedCompleteArgs); + Assert.IsTrue(receivedCompleteArgs.IsAborted); + Assert.IsNotNull(receivedCompleteArgs.Error); + Assert.AreSame(realException, receivedCompleteArgs.Error!.InnerException, + "A TargetInvocationException should be unwrapped to its real inner exception, not surfaced as-is."); + } + [TestMethod] public void RunTestsShouldNotThrowIfExceptionIsAFileNotFoundException() {