Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions samples/Playground/Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ public class TestClass
[TestMethod]
public void Test1()
{
Thread.Sleep(5000);
Assert.IsPositive(1);
// string commonPart = new('a', 1000);
// string expected = commonPart + "expected";
// string actual = commonPart + "actual";
// Assert.AreEqual(expected, actual, ignoreCase: true);
}
}
6 changes: 6 additions & 0 deletions src/Adapter/MSTestAdapter.PlatformServices/EngineConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@ internal static class EngineConstants

internal static readonly TestProperty InnerResultsCountProperty = TestProperty.Register("InnerResultsCount", InnerResultsCountLabel, typeof(int), TestPropertyAttributes.Hidden, typeof(TestResult));

internal static readonly TestProperty AssertActualProperty = TestProperty.Register("AssertActual", AssertActualLabel, typeof(string), TestPropertyAttributes.Hidden, typeof(TestResult));

internal static readonly TestProperty AssertExpectedProperty = TestProperty.Register("AssertExpected", AssertExpectedLabel, typeof(string), TestPropertyAttributes.Hidden, typeof(TestResult));

internal static readonly TestProperty TestRunIdProperty = TestProperty.Register(TestRunId, TestRunId, typeof(int), TestPropertyAttributes.Hidden, typeof(TestCase));

internal static readonly TestProperty TestPlanIdProperty = TestProperty.Register(TestPlanId, TestPlanId, typeof(int), TestPropertyAttributes.Hidden, typeof(TestCase));
Expand Down Expand Up @@ -144,6 +148,8 @@ internal static class EngineConstants
private const string ExecutionIdLabel = "ExecutionId";
private const string ParentExecIdLabel = "ParentExecId";
private const string InnerResultsCountLabel = "InnerResultsCount";
private const string AssertActualLabel = "AssertActual";
private const string AssertExpectedLabel = "AssertExpected";
private const string WorkItemIdsLabel = "WorkItemIds";

private const string TestRunId = "__Tfs_TestRunId__";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,16 @@ internal static VSTestTestResult ToTestResult(this TestResult frameworkTestResul
testResult.SetPropertyValue(EngineConstants.ParentExecIdProperty, frameworkTestResult.ParentExecId);
testResult.SetPropertyValue(EngineConstants.InnerResultsCountProperty, frameworkTestResult.InnerResultsCount);

if (frameworkTestResult.ExceptionAssertActual is not null)
{
testResult.SetPropertyValue(EngineConstants.AssertActualProperty, frameworkTestResult.ExceptionAssertActual);
}

if (frameworkTestResult.ExceptionAssertExpected is not null)
{
testResult.SetPropertyValue(EngineConstants.AssertExpectedProperty, frameworkTestResult.ExceptionAssertExpected);
}

if (!StringEx.IsNullOrEmpty(frameworkTestResult.LogOutput))
{
VSTestTestResultMessage message = new(VSTestTestResultMessage.StandardOutCategory, frameworkTestResult.LogOutput);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,15 @@ public TestFailedException(UTF.UnitTestOutcome outcome, string errorMessage, Sta

Outcome = outcome;
StackTraceInformation = stackTraceInformation;

// Copy Data from the real exception to preserve assertion metadata
if (realException?.Data is not null)
{
foreach (object key in realException.Data.Keys)
{
Data[key] = realException.Data[key];
}
}
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,18 @@ internal static class ObjectModelConverters
valueType: typeof(string),
owner: typeof(TestCase));

private static readonly TestProperty AssertActualProperty = TestProperty.Register(
id: "AssertActual",
label: "AssertActual",
valueType: typeof(string),
owner: typeof(TestResult));

private static readonly TestProperty AssertExpectedProperty = TestProperty.Register(
id: "AssertExpected",
label: "AssertExpected",
valueType: typeof(string),
owner: typeof(TestResult));

private static readonly Uri ExecutorUri = new(Constants.ExecutorUri);

/// <summary>
Expand Down Expand Up @@ -229,11 +241,21 @@ private static void AddOutcome(this TestNode testNode, TestResult testResult)
break;

case TestOutcome.NotFound:
testNode.Properties.Add(new ErrorTestNodeStateProperty(new VSTestException(testResult.ErrorMessage ?? "Not found", testResult.ErrorStackTrace)));
{
VSTestException exception = new(testResult.ErrorMessage ?? "Not found", testResult.ErrorStackTrace);
AddAssertDataToException(exception, testResult);
testNode.Properties.Add(new ErrorTestNodeStateProperty(exception));
}

break;

case TestOutcome.Failed:
testNode.Properties.Add(new FailedTestNodeStateProperty(new VSTestException(testResult.ErrorMessage, testResult.ErrorStackTrace)));
{
VSTestException exception = new(testResult.ErrorMessage, testResult.ErrorStackTrace);
AddAssertDataToException(exception, testResult);
testNode.Properties.Add(new FailedTestNodeStateProperty(exception));
}

break;

// It seems that NUnit inconclusive tests are reported as None which should be considered as Skipped.
Expand All @@ -249,6 +271,21 @@ private static void AddOutcome(this TestNode testNode, TestResult testResult)
}
}

private static void AddAssertDataToException(VSTestException exception, TestResult testResult)
{
string? assertActual = testResult.GetPropertyValue<string>(AssertActualProperty, defaultValue: null);
if (assertActual is not null)
{
exception.Data["assert.actual"] = assertActual;
}

string? assertExpected = testResult.GetPropertyValue<string>(AssertExpectedProperty, defaultValue: null);
if (assertExpected is not null)
{
exception.Data["assert.expected"] = assertExpected;
}
}

internal static void FixUpTestCase(this TestCase testCase, string? testAssemblyPath = null)
{
// To help framework authors using code generator, we replace the Source property of the test case with the
Expand Down
6 changes: 3 additions & 3 deletions src/TestFramework/TestFramework/Assertions/Assert.AreEqual.cs
Original file line number Diff line number Diff line change
Expand Up @@ -643,7 +643,7 @@ private static string FormatStringDifferenceMessage(string expected, string actu
}

[DoesNotReturn]
private static void ThrowAssertAreEqualFailed(object? expected, object? actual, string userMessage)
private static void ThrowAssertAreEqualFailed<T>(T? expected, T? actual, string userMessage)
{
string finalMessage = actual != null && expected != null && !actual.GetType().Equals(expected.GetType())
? string.Format(
Expand All @@ -662,7 +662,7 @@ private static void ThrowAssertAreEqualFailed(object? expected, object? actual,
userMessage,
ReplaceNulls(expected),
ReplaceNulls(actual));
ThrowAssertFailed("Assert.AreEqual", finalMessage);
ThrowAssertFailed("Assert.AreEqual", finalMessage, expected, actual);
}

[DoesNotReturn]
Expand Down Expand Up @@ -700,7 +700,7 @@ private static void ThrowAssertAreEqualFailed(string? expected, string? actual,
finalMessage = FormatStringComparisonMessage(expected, actual, userMessage);
}

ThrowAssertFailed("Assert.AreEqual", finalMessage);
ThrowAssertFailed("Assert.AreEqual", finalMessage, expected, actual);
}

/// <summary>
Expand Down
64 changes: 64 additions & 0 deletions src/TestFramework/TestFramework/Assertions/Assert.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,70 @@ internal static void ThrowAssertFailed(string assertionName, string? message)
=> throw new AssertFailedException(
string.Format(CultureInfo.CurrentCulture, FrameworkMessages.AssertionFailed, assertionName, message));

/// <summary>
/// Helper function that creates and throws an AssertionFailedException with expected and actual values.
/// </summary>
/// <typeparam name="T">
/// The type of the expected and actual values.
/// </typeparam>
/// <param name="assertionName">
/// name of the assertion throwing an exception.
/// </param>
/// <param name="message">
/// The assertion failure message.
/// </param>
/// <param name="expected">
/// Expected value to store in exception data.
/// </param>
/// <param name="actual">
/// Actual value to store in exception data.
/// </param>
[DoesNotReturn]
[StackTraceHidden]
internal static void ThrowAssertFailed<T>(string assertionName, string? message, T? expected, T? actual)
{
AssertFailedException exception = new(
string.Format(CultureInfo.CurrentCulture, FrameworkMessages.AssertionFailed, assertionName, message));

// Store expected and actual values in exception Data for types with known good ToString implementations
if (HasKnownGoodToString(expected))
{
exception.Data["assert.expected"] = expected;
}

if (HasKnownGoodToString(actual))
{
exception.Data["assert.actual"] = actual;
}

throw exception;
}

private static bool HasKnownGoodToString([NotNullWhen(true)] object? value)
{
if (value is null)
{
return false;
}

Type type = value.GetType();

// Primitive types and string
if (type.IsPrimitive || type == typeof(string))
{
return true;
}

// Common types with good ToString implementations
return type == typeof(decimal)
|| type == typeof(DateTime)
|| type == typeof(DateTimeOffset)
|| type == typeof(TimeSpan)
|| type == typeof(Guid)
|| type == typeof(Uri)
|| type.IsEnum;
}

/// <summary>
/// Builds the formatted message using the given user format message and parameters.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,27 @@ public Exception? TestFailureException

ExceptionMessage = field.Message;
ExceptionStackTrace = field.StackTrace;

if (field.Data.Contains("assert.actual"))
{
ExceptionAssertActual = field.Data["assert.actual"]?.ToString();
}

if (field.Data.Contains("assert.expected"))
{
ExceptionAssertExpected = field.Data["assert.expected"]?.ToString();
}
}
}

internal string? ExceptionMessage { get; set; }

internal string? ExceptionStackTrace { get; set; }

internal string? ExceptionAssertActual { get; set; }

internal string? ExceptionAssertExpected { get; set; }

/// <summary>
/// Gets or sets the output of the message logged by test code.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,23 @@ public async Task TestMethodInfoInvokeShouldSetStackTraceInformationIfSetTestCon
" at Microsoft.VisualStudio.TestPlatform.MSTestAdapter.UnitTests.Execution.TestMethodInfoTests.<>c.<TestMethodInfoInvokeShouldSetStackTraceInformationIfSetTestContextThrows>b__", StringComparison.Ordinal).Should().BeTrue();
}

public async Task TestMethodInfoInvokeShouldPreserveExceptionDataWhenWrappingException()
{
var innerException = new UTF.AssertFailedException("Assert failed");
innerException.Data["assert.expected"] = "expectedValue";
innerException.Data["assert.actual"] = "actualValue";
innerException.Data["custom.key"] = "customValue";

DummyTestClass.TestMethodBody = instance => throw innerException;

var exception = (await _testMethodInfo.InvokeAsync(null)).TestFailureException as TestFailedException;

exception.Should().NotBeNull();
exception.Data["assert.expected"].Should().Be("expectedValue");
exception.Data["assert.actual"].Should().Be("actualValue");
exception.Data["custom.key"].Should().Be("customValue");
}

public async Task TestMethodInfoInvoke_WhenCtorHasOneParameterOfTypeTestContextAndTestContextProperty_InitializeBothTestContexts()
{
ConstructorInfo ctorInfo = typeof(DummyTestClass).GetConstructor([typeof(TestContext)])!;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,4 +184,59 @@ public void ToUnitTestResultsShouldHaveResultsFileProvidedToTestResult()
var convertedResult = result.ToTestResult(new(), default, default, string.Empty, new());
convertedResult.Attachments[0].Attachments[0].Description.Should().Be(resultFile);
}

public void ToUnitTestResultsShouldSetAssertActualWhenProvided()
{
var exception = new Exception("Test failed");
exception.Data["assert.actual"] = "actualValue";

var result = new TestResult { TestFailureException = exception };
var convertedResult = result.ToTestResult(new(), default, default, string.Empty, new());

((string)convertedResult.GetPropertyValue(EngineConstants.AssertActualProperty)!).Should().Be("actualValue");
}

public void ToUnitTestResultsShouldSetAssertExpectedWhenProvided()
{
var exception = new Exception("Test failed");
exception.Data["assert.expected"] = "expectedValue";

var result = new TestResult { TestFailureException = exception };
var convertedResult = result.ToTestResult(new(), default, default, string.Empty, new());

((string)convertedResult.GetPropertyValue(EngineConstants.AssertExpectedProperty)!).Should().Be("expectedValue");
}

public void ToUnitTestResultsShouldSetBothAssertActualAndExpectedWhenProvided()
{
var exception = new Exception("Test failed");
exception.Data["assert.actual"] = "actualValue";
exception.Data["assert.expected"] = "expectedValue";

var result = new TestResult { TestFailureException = exception };
var convertedResult = result.ToTestResult(new(), default, default, string.Empty, new());

((string)convertedResult.GetPropertyValue(EngineConstants.AssertActualProperty)!).Should().Be("actualValue");
((string)convertedResult.GetPropertyValue(EngineConstants.AssertExpectedProperty)!).Should().Be("expectedValue");
}

public void ToUnitTestResultsShouldNotSetAssertActualWhenNotProvided()
{
var exception = new Exception("Test failed");

var result = new TestResult { TestFailureException = exception };
var convertedResult = result.ToTestResult(new(), default, default, string.Empty, new());

convertedResult.GetPropertyValue(EngineConstants.AssertActualProperty).Should().BeNull();
}

public void ToUnitTestResultsShouldNotSetAssertExpectedWhenNotProvided()
{
var exception = new Exception("Test failed");

var result = new TestResult { TestFailureException = exception };
var convertedResult = result.ToTestResult(new(), default, default, string.Empty, new());

convertedResult.GetPropertyValue(EngineConstants.AssertExpectedProperty).Should().BeNull();
}
}
Loading
Loading