Skip to content

Commit

Permalink
Merge pull request nunit#4922 from nunit/Issue4909_PropertiesWriter
Browse files Browse the repository at this point in the history
DelayedConstraint shows full result of underlying constraint.
  • Loading branch information
manfred-brands authored Jan 12, 2025
2 parents cca65aa + 8b91299 commit 6ed875d
Show file tree
Hide file tree
Showing 12 changed files with 195 additions and 60 deletions.
10 changes: 9 additions & 1 deletion .github/workflows/NUnit.CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ jobs:
uses: actions/setup-dotnet@v4
with:
global-json-file: global.json
dotnet-version: |
6.0.x
8.0.x
- name: 🛠️ Install dotnet tools
run: dotnet tool restore
Expand Down Expand Up @@ -68,6 +71,9 @@ jobs:
uses: actions/setup-dotnet@v4
with:
global-json-file: global.json
dotnet-version: |
6.0.x
8.0.x
- name: 🛠️ Install F#
run: sudo apt-get install fsharp
Expand Down Expand Up @@ -100,7 +106,9 @@ jobs:
uses: actions/setup-dotnet@v4
with:
global-json-file: global.json
dotnet-version: 6.x
dotnet-version: |
6.0.x
8.0.x
- name: 🛠️ Install dotnet tools
run: dotnet tool restore
Expand Down
16 changes: 10 additions & 6 deletions src/NUnitFramework/framework/Assert.Exceptions.Async.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,17 @@ public partial class Assert
public static Exception? ThrowsAsync(IResolveConstraint expression, AsyncTestDelegate code, string message, params object?[]? args)
{
Exception? caughtException = null;
try
{
AsyncToSyncAdapter.Await(TestExecutionContext.CurrentContext, code.Invoke);
}
catch (Exception e)

using (new TestExecutionContext.IsolatedContext())
{
caughtException = e;
try
{
AsyncToSyncAdapter.Await(TestExecutionContext.CurrentContext, code.Invoke);
}
catch (Exception e)
{
caughtException = e;
}
}

Assert.That(caughtException, expression, () => ConvertMessageWithArgs(message, args));
Expand Down
10 changes: 6 additions & 4 deletions src/NUnitFramework/framework/Constraints/DelayedConstraint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ private DelayedConstraint(IConstraint baseConstraint, Interval delayInterval, In
/// <summary>
/// Gets text describing a constraint
/// </summary>
public override string Description => $"{BaseConstraint.Description} after {DelayInterval} delay";
public override string Description => $"After {DelayInterval} delay";

/// <summary>
/// Test whether the constraint is satisfied by a given value
Expand Down Expand Up @@ -401,9 +401,11 @@ public DelegatingConstraintResult(IConstraint constraint, ConstraintResult inner
_innerResult = innerResult;
}

public override void WriteActualValueTo(MessageWriter writer) => _innerResult.WriteActualValueTo(writer);

public override void WriteAdditionalLinesTo(MessageWriter writer) => _innerResult.WriteAdditionalLinesTo(writer);
public override void WriteMessageTo(MessageWriter writer)
{
writer.WriteMessageLine(Description);
_innerResult.WriteMessageTo(writer);
}
}
}
}
4 changes: 4 additions & 0 deletions src/NUnitFramework/framework/Constraints/EqualConstraint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,10 @@ public EqualConstraint UsingPropertiesComparer()
/// <returns>True for success, false for failure</returns>
public override ConstraintResult ApplyTo<TActual>(TActual actual)
{
// Reset the comparer before each use, e.g. for DelayedConstraint
if (_comparer.HasFailurePoints)
_comparer.FailurePoints.Clear();

AdjustArgumentIfNeeded(ref actual);
return new EqualConstraintResult(this, actual, _comparer.AreEqual(_expected, actual, ref _tolerance));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Collections;
using System.Collections.Generic;
using System.IO;
using NUnit.Framework.Internal;

namespace NUnit.Framework.Constraints
{
Expand Down Expand Up @@ -123,7 +124,7 @@ private void DisplayDifferences(MessageWriter writer, object? expected, object?
else if (expected is Stream expectedStream && actual is Stream actualStream)
DisplayStreamDifferences(writer, expectedStream, actualStream, depth);
else if (_comparingProperties && IsPropertyFailurePoint(depth))
DisplayPropertyDifferences(writer, depth);
DisplayPropertyDifferences(writer, expected, actual, depth);
else if (_tolerance is not null)
writer.DisplayDifferences(expected, actual, _tolerance);
else
Expand Down Expand Up @@ -220,8 +221,8 @@ private void DisplayCollectionDifferenceWithFailurePoint(MessageWriter writer, I
writer.WriteMessageLine(StringsDiffer_1, expectedString.Length, mismatchExpected);
else
writer.WriteMessageLine(StringsDiffer_2, expectedString.Length, actualString.Length, mismatchExpected);
writer.WriteLine($" Expected: {MsgUtils.FormatCollection(expected)}");
writer.WriteLine($" But was: {MsgUtils.FormatCollection(actual)}");
writer.WriteLine($"{TextMessageWriter.Pfx_Expected}{MsgUtils.FormatCollection(expected)}");
writer.WriteLine($"{TextMessageWriter.Pfx_Actual}{MsgUtils.FormatCollection(actual)}");
writer.WriteLine($" First non-matching item at index [{failurePoint.Position}]: \"{failurePoint.ExpectedValue}\"");
return;
}
Expand Down Expand Up @@ -346,13 +347,15 @@ private void DisplayEnumerableDifferences(MessageWriter writer, IEnumerable expe

#region DisplayPropertyDifferences

private void DisplayPropertyDifferences(MessageWriter writer, int depth)
private void DisplayPropertyDifferences(MessageWriter writer, object? expected, object? actual, int depth)
{
if (_failurePoints.Count > depth)
{
NUnitEqualityComparer.FailurePoint failurePoint = _failurePoints[depth];

writer.WriteMessageLine($"Values differ at property {failurePoint.PropertyName}");
writer.WriteLine($"{TextMessageWriter.Pfx_Expected}{MsgUtils.FormatValueProperties(expected)}");
writer.WriteLine($"{TextMessageWriter.Pfx_Actual}{MsgUtils.FormatValueProperties(actual)}");
writer.WriteLine($" Values differ at property {failurePoint.PropertyName}:");
DisplayDifferences(writer, failurePoint.ExpectedValue, failurePoint.ActualValue, ++depth);
}
}
Expand Down
51 changes: 51 additions & 0 deletions src/NUnitFramework/framework/Constraints/MsgUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Text;
using NUnit.Framework.Internal;

Expand Down Expand Up @@ -94,6 +96,55 @@ static MsgUtils()
AddFormatter(next => val => val is Array valArray ? FormatArray(valArray) : next(val));
}

/// <summary>
/// Try to format the properties of an object as a string.
/// </summary>
/// <param name="val">The object to format.</param>
/// <returns>Formatted string for all properties.</returns>
public static string FormatValueProperties(object? val)
{
if (val is null)
return "null";

Type valueType = val.GetType();

// If the type is a low level type, call our default formatter.
if (valueType.IsPrimitive || valueType == typeof(string) || valueType == typeof(decimal))
return FormatValue(val);

// If the type implements its own ToString() method, call that.
MethodInfo? toStringMethod = valueType.GetMethod(nameof(ToString), Type.EmptyTypes);
if (toStringMethod?.DeclaringType == valueType)
return FormatValueWithoutThrowing(val);

PropertyInfo[] properties = valueType.GetProperties(BindingFlags.Instance | BindingFlags.Public);
if (properties.Length == 0 || properties.Any(p => p.GetIndexParameters().Length > 0))
{
// We can't print if there are no properties.
// We also can't deal with indexer properties as we don't know the range of valid values.
return FormatValue(val);
}

var sb = new StringBuilder();
sb.Append(valueType.Name);
sb.Append(" { ");

bool firstProperty = true;
foreach (var property in properties)
{
if (!firstProperty)
sb.Append(", ");
else
firstProperty = false;

sb.Append(property.Name);
sb.Append(" = ");
sb.Append(FormatValue(property.GetValue(val, null)));
}
sb.Append(" }");
return sb.ToString();
}

#if NETFRAMEWORK
[System.Runtime.ExceptionServices.HandleProcessCorruptedStateExceptions]
#endif
Expand Down
15 changes: 9 additions & 6 deletions src/NUnitFramework/framework/Internal/ExceptionHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -199,13 +199,16 @@ private static List<Exception> FlattenExceptionHierarchy(Exception exception)
{
Guard.ArgumentNotNull(parameterlessDelegate, parameterName);

try
{
await parameterlessDelegate();
}
catch (Exception e)
using (new TestExecutionContext.IsolatedContext())
{
return e;
try
{
await parameterlessDelegate();
}
catch (Exception e)
{
return e;
}
}

return null;
Expand Down
8 changes: 1 addition & 7 deletions src/NUnitFramework/tests/Assertions/AssertThatAsyncTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -159,13 +159,7 @@ public async Task AssertionFails_WhenDelegateThrowsEvenWithRetry()
private static async Task AssertAssertionFailsAsync(Func<Task> assertion)
{
await Assert.ThatAsync(
async () =>
{
using (new TestExecutionContext.IsolatedContext())
{
await assertion();
}
},
async () => await assertion(),
Throws.InstanceOf<AssertionException>());
}
}
Expand Down
59 changes: 35 additions & 24 deletions src/NUnitFramework/tests/Assertions/AssertThatTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -551,37 +551,53 @@ public void AssertThatEqualsWithStructWithSomeToleranceAwareMembers()
[Test]
public void AssertThatEqualsWithStructMemberDifferences()
{
var instance = new StructWithSomeToleranceAwareMembers(1, 1.1, "1.1", SomeEnum.One);
var instance = new StructWithSomeToleranceAwareMembers(1, 0.123, "1.1", SomeEnum.One);

Assert.That(() =>
Assert.That(new StructWithSomeToleranceAwareMembers(2, 1.1, "1.1", SomeEnum.One), Is.EqualTo(instance).UsingPropertiesComparer()),
Throws.InstanceOf<AssertionException>().With.Message.Contains("at property StructWithSomeToleranceAwareMembers.ValueA")
.And.Message.Contains("Expected: 1"));
Assert.That(new StructWithSomeToleranceAwareMembers(2, 0.123, "1.1", SomeEnum.One), Is.EqualTo(instance).UsingPropertiesComparer()),
Throws.InstanceOf<AssertionException>().With
.Message.Contains("Expected: StructWithSomeToleranceAwareMembers { ValueA = 1, ValueB = 0.123d, ValueC = \"1.1\", ValueD = One }").And
.Message.Contains("But was: StructWithSomeToleranceAwareMembers { ValueA = 2, ValueB = 0.123d, ValueC = \"1.1\", ValueD = One }").And
.Message.Contains("at property StructWithSomeToleranceAwareMembers.ValueA").And
.Message.Contains("Expected: 1").And
.Message.Contains("But was: 2"));
Assert.That(() =>
Assert.That(new StructWithSomeToleranceAwareMembers(1, 1.2, "1.1", SomeEnum.One), Is.EqualTo(instance).UsingPropertiesComparer()),
Throws.InstanceOf<AssertionException>().With.Message.Contains("at property StructWithSomeToleranceAwareMembers.ValueB")
.And.Message.Contains("Expected: 1.1"));
Assert.That(new StructWithSomeToleranceAwareMembers(1, 0.246, "1.1", SomeEnum.One), Is.EqualTo(instance).UsingPropertiesComparer()),
Throws.InstanceOf<AssertionException>().With
.Message.Contains("Expected: StructWithSomeToleranceAwareMembers { ValueA = 1, ValueB = 0.123d, ValueC = \"1.1\", ValueD = One }").And
.Message.Contains("But was: StructWithSomeToleranceAwareMembers { ValueA = 1, ValueB = 0.246d, ValueC = \"1.1\", ValueD = One }").And
.Message.Contains("at property StructWithSomeToleranceAwareMembers.ValueB").And
.Message.Contains("Expected: 0.123d").And
.Message.Contains("But was: 0.246d"));
Assert.That(() =>
Assert.That(new StructWithSomeToleranceAwareMembers(1, 1.1, "1.2", SomeEnum.One), Is.EqualTo(instance).UsingPropertiesComparer()),
Throws.InstanceOf<AssertionException>().With.Message.Contains("at property StructWithSomeToleranceAwareMembers.ValueC")
.And.Message.Contains("Expected: \"1.1\""));
Assert.That(new StructWithSomeToleranceAwareMembers(1, 0.123, "2.2", SomeEnum.One), Is.EqualTo(instance).UsingPropertiesComparer()),
Throws.InstanceOf<AssertionException>().With
.Message.Contains("Expected: StructWithSomeToleranceAwareMembers { ValueA = 1, ValueB = 0.123d, ValueC = \"1.1\", ValueD = One }").And
.Message.Contains("But was: StructWithSomeToleranceAwareMembers { ValueA = 1, ValueB = 0.123d, ValueC = \"2.2\", ValueD = One }").And
.Message.Contains("at property StructWithSomeToleranceAwareMembers.ValueC").And
.Message.Contains("Expected: \"1.1\"").And
.Message.Contains("But was: \"2.2\""));
Assert.That(() =>
Assert.That(new StructWithSomeToleranceAwareMembers(1, 1.1, "1.1", SomeEnum.Two), Is.EqualTo(instance).UsingPropertiesComparer()),
Throws.InstanceOf<AssertionException>().With.Message.Contains("at property StructWithSomeToleranceAwareMembers.ValueD")
.And.Message.Contains("Expected: One"));
Assert.That(new StructWithSomeToleranceAwareMembers(1, 0.123, "1.1", SomeEnum.Two), Is.EqualTo(instance).UsingPropertiesComparer()),
Throws.InstanceOf<AssertionException>().With
.Message.Contains("Expected: StructWithSomeToleranceAwareMembers { ValueA = 1, ValueB = 0.123d, ValueC = \"1.1\", ValueD = One }").And
.Message.Contains("But was: StructWithSomeToleranceAwareMembers { ValueA = 1, ValueB = 0.123d, ValueC = \"1.1\", ValueD = Two }").And
.Message.Contains("at property StructWithSomeToleranceAwareMembers.ValueD").And
.Message.Contains("Expected: One").And
.Message.Contains("But was: Two"));

/*
* Uncomment this block to see the actual exception messages. Test will fail.
*
Assert.Multiple(() =>
{
Assert.That(new StructWithSomeToleranceAwareMembers(2, 1.1, "1.1", SomeEnum.One), Is.EqualTo(instance).UsingPropertiesComparer());
Assert.That(new StructWithSomeToleranceAwareMembers(1, 1.1, "1.1", SomeEnum.One), Is.EqualTo(instance).UsingPropertiesComparer());
Assert.That(new StructWithSomeToleranceAwareMembers(1, 1.2, "1.1", SomeEnum.One), Is.EqualTo(instance).UsingPropertiesComparer());
Assert.That(new StructWithSomeToleranceAwareMembers(1, 1.1, "1.2", SomeEnum.One), Is.EqualTo(instance).UsingPropertiesComparer());
Assert.That(new StructWithSomeToleranceAwareMembers(1, 1.1, "1.1", SomeEnum.Two), Is.EqualTo(instance).UsingPropertiesComparer());
Assert.That(new StructWithSomeToleranceAwareMembers(2, 0.123, "1.1", SomeEnum.One), Is.EqualTo(instance).UsingPropertiesComparer());
Assert.That(new StructWithSomeToleranceAwareMembers(1, 0.246, "1.1", SomeEnum.One), Is.EqualTo(instance).UsingPropertiesComparer());
Assert.That(new StructWithSomeToleranceAwareMembers(1, 0.123, "1.1", SomeEnum.One), Is.EqualTo(instance).UsingPropertiesComparer());
Assert.That(new StructWithSomeToleranceAwareMembers(1, 0.123, "1.2", SomeEnum.One), Is.EqualTo(instance).UsingPropertiesComparer());
Assert.That(new StructWithSomeToleranceAwareMembers(1, 0.123, "1.1", SomeEnum.Two), Is.EqualTo(instance).UsingPropertiesComparer());
});
*/
*/
}

private enum SomeEnum
Expand All @@ -604,11 +620,6 @@ public StructWithSomeToleranceAwareMembers(int valueA, double valueB, string val
public double ValueB { get; }
public string ValueC { get; }
public SomeEnum ValueD { get; }

public override string ToString()
{
return $"{ValueA} {ValueB} '{ValueC}' {ValueD}";
}
}

[Test]
Expand Down
7 changes: 7 additions & 0 deletions src/NUnitFramework/tests/Assertions/AssertThrowsAsyncTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,13 @@ public void ThrowsConstraintReturnsCorrectException()
CheckForSpuriousAssertionResults();
}

[Test]
public void ThrowsAsyncIsNotAffectedByAssertionsInDelegate()
{
Assert.ThrowsAsync<AssertionException>(
() => Assert.ThatAsync(AsyncTestDelegates.ThrowsArgumentExceptionAsync, Throws.InvalidOperationException));
}

[Test]
public void CorrectExceptionIsReturnedToMethod()
{
Expand Down
6 changes: 1 addition & 5 deletions src/NUnitFramework/tests/Constraints/ConstraintTestBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,16 +49,12 @@ public void SucceedsWithGoodValues(object value)
[Test, TestCaseSource(nameof(FailureData))]
public void FailsWithBadValues(object badValue, string message)
{
string nl = Environment.NewLine;

var constraintResult = TheConstraint.ApplyTo(badValue);
Assert.That(constraintResult.IsSuccess, Is.False);

TextMessageWriter writer = new TextMessageWriter();
constraintResult.WriteMessageTo(writer);
Assert.That(writer.ToString(), Is.EqualTo(
TextMessageWriter.Pfx_Expected + ExpectedDescription + nl +
TextMessageWriter.Pfx_Actual + message + nl));
Assert.That(writer.ToString(), Does.Contain(ExpectedDescription).And.Contains(message));
}
}
}
Loading

0 comments on commit 6ed875d

Please sign in to comment.