Skip to content

Commit 6ed875d

Browse files
Merge pull request nunit#4922 from nunit/Issue4909_PropertiesWriter
DelayedConstraint shows full result of underlying constraint.
2 parents cca65aa + 8b91299 commit 6ed875d

File tree

12 files changed

+195
-60
lines changed

12 files changed

+195
-60
lines changed

.github/workflows/NUnit.CI.yml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ jobs:
2727
uses: actions/setup-dotnet@v4
2828
with:
2929
global-json-file: global.json
30+
dotnet-version: |
31+
6.0.x
32+
8.0.x
3033
3134
- name: 🛠️ Install dotnet tools
3235
run: dotnet tool restore
@@ -68,6 +71,9 @@ jobs:
6871
uses: actions/setup-dotnet@v4
6972
with:
7073
global-json-file: global.json
74+
dotnet-version: |
75+
6.0.x
76+
8.0.x
7177
7278
- name: 🛠️ Install F#
7379
run: sudo apt-get install fsharp
@@ -100,7 +106,9 @@ jobs:
100106
uses: actions/setup-dotnet@v4
101107
with:
102108
global-json-file: global.json
103-
dotnet-version: 6.x
109+
dotnet-version: |
110+
6.0.x
111+
8.0.x
104112
105113
- name: 🛠️ Install dotnet tools
106114
run: dotnet tool restore

src/NUnitFramework/framework/Assert.Exceptions.Async.cs

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,17 @@ public partial class Assert
2121
public static Exception? ThrowsAsync(IResolveConstraint expression, AsyncTestDelegate code, string message, params object?[]? args)
2222
{
2323
Exception? caughtException = null;
24-
try
25-
{
26-
AsyncToSyncAdapter.Await(TestExecutionContext.CurrentContext, code.Invoke);
27-
}
28-
catch (Exception e)
24+
25+
using (new TestExecutionContext.IsolatedContext())
2926
{
30-
caughtException = e;
27+
try
28+
{
29+
AsyncToSyncAdapter.Await(TestExecutionContext.CurrentContext, code.Invoke);
30+
}
31+
catch (Exception e)
32+
{
33+
caughtException = e;
34+
}
3135
}
3236

3337
Assert.That(caughtException, expression, () => ConvertMessageWithArgs(message, args));

src/NUnitFramework/framework/Constraints/DelayedConstraint.cs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ private DelayedConstraint(IConstraint baseConstraint, Interval delayInterval, In
220220
/// <summary>
221221
/// Gets text describing a constraint
222222
/// </summary>
223-
public override string Description => $"{BaseConstraint.Description} after {DelayInterval} delay";
223+
public override string Description => $"After {DelayInterval} delay";
224224

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

404-
public override void WriteActualValueTo(MessageWriter writer) => _innerResult.WriteActualValueTo(writer);
405-
406-
public override void WriteAdditionalLinesTo(MessageWriter writer) => _innerResult.WriteAdditionalLinesTo(writer);
404+
public override void WriteMessageTo(MessageWriter writer)
405+
{
406+
writer.WriteMessageLine(Description);
407+
_innerResult.WriteMessageTo(writer);
408+
}
407409
}
408410
}
409411
}

src/NUnitFramework/framework/Constraints/EqualConstraint.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,10 @@ public EqualConstraint UsingPropertiesComparer()
404404
/// <returns>True for success, false for failure</returns>
405405
public override ConstraintResult ApplyTo<TActual>(TActual actual)
406406
{
407+
// Reset the comparer before each use, e.g. for DelayedConstraint
408+
if (_comparer.HasFailurePoints)
409+
_comparer.FailurePoints.Clear();
410+
407411
AdjustArgumentIfNeeded(ref actual);
408412
return new EqualConstraintResult(this, actual, _comparer.AreEqual(_expected, actual, ref _tolerance));
409413
}

src/NUnitFramework/framework/Constraints/EqualConstraintResult.cs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.Collections;
55
using System.Collections.Generic;
66
using System.IO;
7+
using NUnit.Framework.Internal;
78

89
namespace NUnit.Framework.Constraints
910
{
@@ -123,7 +124,7 @@ private void DisplayDifferences(MessageWriter writer, object? expected, object?
123124
else if (expected is Stream expectedStream && actual is Stream actualStream)
124125
DisplayStreamDifferences(writer, expectedStream, actualStream, depth);
125126
else if (_comparingProperties && IsPropertyFailurePoint(depth))
126-
DisplayPropertyDifferences(writer, depth);
127+
DisplayPropertyDifferences(writer, expected, actual, depth);
127128
else if (_tolerance is not null)
128129
writer.DisplayDifferences(expected, actual, _tolerance);
129130
else
@@ -220,8 +221,8 @@ private void DisplayCollectionDifferenceWithFailurePoint(MessageWriter writer, I
220221
writer.WriteMessageLine(StringsDiffer_1, expectedString.Length, mismatchExpected);
221222
else
222223
writer.WriteMessageLine(StringsDiffer_2, expectedString.Length, actualString.Length, mismatchExpected);
223-
writer.WriteLine($" Expected: {MsgUtils.FormatCollection(expected)}");
224-
writer.WriteLine($" But was: {MsgUtils.FormatCollection(actual)}");
224+
writer.WriteLine($"{TextMessageWriter.Pfx_Expected}{MsgUtils.FormatCollection(expected)}");
225+
writer.WriteLine($"{TextMessageWriter.Pfx_Actual}{MsgUtils.FormatCollection(actual)}");
225226
writer.WriteLine($" First non-matching item at index [{failurePoint.Position}]: \"{failurePoint.ExpectedValue}\"");
226227
return;
227228
}
@@ -346,13 +347,15 @@ private void DisplayEnumerableDifferences(MessageWriter writer, IEnumerable expe
346347

347348
#region DisplayPropertyDifferences
348349

349-
private void DisplayPropertyDifferences(MessageWriter writer, int depth)
350+
private void DisplayPropertyDifferences(MessageWriter writer, object? expected, object? actual, int depth)
350351
{
351352
if (_failurePoints.Count > depth)
352353
{
353354
NUnitEqualityComparer.FailurePoint failurePoint = _failurePoints[depth];
354355

355-
writer.WriteMessageLine($"Values differ at property {failurePoint.PropertyName}");
356+
writer.WriteLine($"{TextMessageWriter.Pfx_Expected}{MsgUtils.FormatValueProperties(expected)}");
357+
writer.WriteLine($"{TextMessageWriter.Pfx_Actual}{MsgUtils.FormatValueProperties(actual)}");
358+
writer.WriteLine($" Values differ at property {failurePoint.PropertyName}:");
356359
DisplayDifferences(writer, failurePoint.ExpectedValue, failurePoint.ActualValue, ++depth);
357360
}
358361
}

src/NUnitFramework/framework/Constraints/MsgUtils.cs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
using System.Collections.Generic;
66
using System.Diagnostics.CodeAnalysis;
77
using System.Globalization;
8+
using System.Linq;
9+
using System.Reflection;
810
using System.Text;
911
using NUnit.Framework.Internal;
1012

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

99+
/// <summary>
100+
/// Try to format the properties of an object as a string.
101+
/// </summary>
102+
/// <param name="val">The object to format.</param>
103+
/// <returns>Formatted string for all properties.</returns>
104+
public static string FormatValueProperties(object? val)
105+
{
106+
if (val is null)
107+
return "null";
108+
109+
Type valueType = val.GetType();
110+
111+
// If the type is a low level type, call our default formatter.
112+
if (valueType.IsPrimitive || valueType == typeof(string) || valueType == typeof(decimal))
113+
return FormatValue(val);
114+
115+
// If the type implements its own ToString() method, call that.
116+
MethodInfo? toStringMethod = valueType.GetMethod(nameof(ToString), Type.EmptyTypes);
117+
if (toStringMethod?.DeclaringType == valueType)
118+
return FormatValueWithoutThrowing(val);
119+
120+
PropertyInfo[] properties = valueType.GetProperties(BindingFlags.Instance | BindingFlags.Public);
121+
if (properties.Length == 0 || properties.Any(p => p.GetIndexParameters().Length > 0))
122+
{
123+
// We can't print if there are no properties.
124+
// We also can't deal with indexer properties as we don't know the range of valid values.
125+
return FormatValue(val);
126+
}
127+
128+
var sb = new StringBuilder();
129+
sb.Append(valueType.Name);
130+
sb.Append(" { ");
131+
132+
bool firstProperty = true;
133+
foreach (var property in properties)
134+
{
135+
if (!firstProperty)
136+
sb.Append(", ");
137+
else
138+
firstProperty = false;
139+
140+
sb.Append(property.Name);
141+
sb.Append(" = ");
142+
sb.Append(FormatValue(property.GetValue(val, null)));
143+
}
144+
sb.Append(" }");
145+
return sb.ToString();
146+
}
147+
97148
#if NETFRAMEWORK
98149
[System.Runtime.ExceptionServices.HandleProcessCorruptedStateExceptions]
99150
#endif

src/NUnitFramework/framework/Internal/ExceptionHelper.cs

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -199,13 +199,16 @@ private static List<Exception> FlattenExceptionHierarchy(Exception exception)
199199
{
200200
Guard.ArgumentNotNull(parameterlessDelegate, parameterName);
201201

202-
try
203-
{
204-
await parameterlessDelegate();
205-
}
206-
catch (Exception e)
202+
using (new TestExecutionContext.IsolatedContext())
207203
{
208-
return e;
204+
try
205+
{
206+
await parameterlessDelegate();
207+
}
208+
catch (Exception e)
209+
{
210+
return e;
211+
}
209212
}
210213

211214
return null;

src/NUnitFramework/tests/Assertions/AssertThatAsyncTests.cs

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -159,13 +159,7 @@ public async Task AssertionFails_WhenDelegateThrowsEvenWithRetry()
159159
private static async Task AssertAssertionFailsAsync(Func<Task> assertion)
160160
{
161161
await Assert.ThatAsync(
162-
async () =>
163-
{
164-
using (new TestExecutionContext.IsolatedContext())
165-
{
166-
await assertion();
167-
}
168-
},
162+
async () => await assertion(),
169163
Throws.InstanceOf<AssertionException>());
170164
}
171165
}

src/NUnitFramework/tests/Assertions/AssertThatTests.cs

Lines changed: 35 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -551,37 +551,53 @@ public void AssertThatEqualsWithStructWithSomeToleranceAwareMembers()
551551
[Test]
552552
public void AssertThatEqualsWithStructMemberDifferences()
553553
{
554-
var instance = new StructWithSomeToleranceAwareMembers(1, 1.1, "1.1", SomeEnum.One);
554+
var instance = new StructWithSomeToleranceAwareMembers(1, 0.123, "1.1", SomeEnum.One);
555555

556556
Assert.That(() =>
557-
Assert.That(new StructWithSomeToleranceAwareMembers(2, 1.1, "1.1", SomeEnum.One), Is.EqualTo(instance).UsingPropertiesComparer()),
558-
Throws.InstanceOf<AssertionException>().With.Message.Contains("at property StructWithSomeToleranceAwareMembers.ValueA")
559-
.And.Message.Contains("Expected: 1"));
557+
Assert.That(new StructWithSomeToleranceAwareMembers(2, 0.123, "1.1", SomeEnum.One), Is.EqualTo(instance).UsingPropertiesComparer()),
558+
Throws.InstanceOf<AssertionException>().With
559+
.Message.Contains("Expected: StructWithSomeToleranceAwareMembers { ValueA = 1, ValueB = 0.123d, ValueC = \"1.1\", ValueD = One }").And
560+
.Message.Contains("But was: StructWithSomeToleranceAwareMembers { ValueA = 2, ValueB = 0.123d, ValueC = \"1.1\", ValueD = One }").And
561+
.Message.Contains("at property StructWithSomeToleranceAwareMembers.ValueA").And
562+
.Message.Contains("Expected: 1").And
563+
.Message.Contains("But was: 2"));
560564
Assert.That(() =>
561-
Assert.That(new StructWithSomeToleranceAwareMembers(1, 1.2, "1.1", SomeEnum.One), Is.EqualTo(instance).UsingPropertiesComparer()),
562-
Throws.InstanceOf<AssertionException>().With.Message.Contains("at property StructWithSomeToleranceAwareMembers.ValueB")
563-
.And.Message.Contains("Expected: 1.1"));
565+
Assert.That(new StructWithSomeToleranceAwareMembers(1, 0.246, "1.1", SomeEnum.One), Is.EqualTo(instance).UsingPropertiesComparer()),
566+
Throws.InstanceOf<AssertionException>().With
567+
.Message.Contains("Expected: StructWithSomeToleranceAwareMembers { ValueA = 1, ValueB = 0.123d, ValueC = \"1.1\", ValueD = One }").And
568+
.Message.Contains("But was: StructWithSomeToleranceAwareMembers { ValueA = 1, ValueB = 0.246d, ValueC = \"1.1\", ValueD = One }").And
569+
.Message.Contains("at property StructWithSomeToleranceAwareMembers.ValueB").And
570+
.Message.Contains("Expected: 0.123d").And
571+
.Message.Contains("But was: 0.246d"));
564572
Assert.That(() =>
565-
Assert.That(new StructWithSomeToleranceAwareMembers(1, 1.1, "1.2", SomeEnum.One), Is.EqualTo(instance).UsingPropertiesComparer()),
566-
Throws.InstanceOf<AssertionException>().With.Message.Contains("at property StructWithSomeToleranceAwareMembers.ValueC")
567-
.And.Message.Contains("Expected: \"1.1\""));
573+
Assert.That(new StructWithSomeToleranceAwareMembers(1, 0.123, "2.2", SomeEnum.One), Is.EqualTo(instance).UsingPropertiesComparer()),
574+
Throws.InstanceOf<AssertionException>().With
575+
.Message.Contains("Expected: StructWithSomeToleranceAwareMembers { ValueA = 1, ValueB = 0.123d, ValueC = \"1.1\", ValueD = One }").And
576+
.Message.Contains("But was: StructWithSomeToleranceAwareMembers { ValueA = 1, ValueB = 0.123d, ValueC = \"2.2\", ValueD = One }").And
577+
.Message.Contains("at property StructWithSomeToleranceAwareMembers.ValueC").And
578+
.Message.Contains("Expected: \"1.1\"").And
579+
.Message.Contains("But was: \"2.2\""));
568580
Assert.That(() =>
569-
Assert.That(new StructWithSomeToleranceAwareMembers(1, 1.1, "1.1", SomeEnum.Two), Is.EqualTo(instance).UsingPropertiesComparer()),
570-
Throws.InstanceOf<AssertionException>().With.Message.Contains("at property StructWithSomeToleranceAwareMembers.ValueD")
571-
.And.Message.Contains("Expected: One"));
581+
Assert.That(new StructWithSomeToleranceAwareMembers(1, 0.123, "1.1", SomeEnum.Two), Is.EqualTo(instance).UsingPropertiesComparer()),
582+
Throws.InstanceOf<AssertionException>().With
583+
.Message.Contains("Expected: StructWithSomeToleranceAwareMembers { ValueA = 1, ValueB = 0.123d, ValueC = \"1.1\", ValueD = One }").And
584+
.Message.Contains("But was: StructWithSomeToleranceAwareMembers { ValueA = 1, ValueB = 0.123d, ValueC = \"1.1\", ValueD = Two }").And
585+
.Message.Contains("at property StructWithSomeToleranceAwareMembers.ValueD").And
586+
.Message.Contains("Expected: One").And
587+
.Message.Contains("But was: Two"));
572588

573589
/*
574590
* Uncomment this block to see the actual exception messages. Test will fail.
575591
*
576592
Assert.Multiple(() =>
577593
{
578-
Assert.That(new StructWithSomeToleranceAwareMembers(2, 1.1, "1.1", SomeEnum.One), Is.EqualTo(instance).UsingPropertiesComparer());
579-
Assert.That(new StructWithSomeToleranceAwareMembers(1, 1.1, "1.1", SomeEnum.One), Is.EqualTo(instance).UsingPropertiesComparer());
580-
Assert.That(new StructWithSomeToleranceAwareMembers(1, 1.2, "1.1", SomeEnum.One), Is.EqualTo(instance).UsingPropertiesComparer());
581-
Assert.That(new StructWithSomeToleranceAwareMembers(1, 1.1, "1.2", SomeEnum.One), Is.EqualTo(instance).UsingPropertiesComparer());
582-
Assert.That(new StructWithSomeToleranceAwareMembers(1, 1.1, "1.1", SomeEnum.Two), Is.EqualTo(instance).UsingPropertiesComparer());
594+
Assert.That(new StructWithSomeToleranceAwareMembers(2, 0.123, "1.1", SomeEnum.One), Is.EqualTo(instance).UsingPropertiesComparer());
595+
Assert.That(new StructWithSomeToleranceAwareMembers(1, 0.246, "1.1", SomeEnum.One), Is.EqualTo(instance).UsingPropertiesComparer());
596+
Assert.That(new StructWithSomeToleranceAwareMembers(1, 0.123, "1.1", SomeEnum.One), Is.EqualTo(instance).UsingPropertiesComparer());
597+
Assert.That(new StructWithSomeToleranceAwareMembers(1, 0.123, "1.2", SomeEnum.One), Is.EqualTo(instance).UsingPropertiesComparer());
598+
Assert.That(new StructWithSomeToleranceAwareMembers(1, 0.123, "1.1", SomeEnum.Two), Is.EqualTo(instance).UsingPropertiesComparer());
583599
});
584-
*/
600+
*/
585601
}
586602

587603
private enum SomeEnum
@@ -604,11 +620,6 @@ public StructWithSomeToleranceAwareMembers(int valueA, double valueB, string val
604620
public double ValueB { get; }
605621
public string ValueC { get; }
606622
public SomeEnum ValueD { get; }
607-
608-
public override string ToString()
609-
{
610-
return $"{ValueA} {ValueB} '{ValueC}' {ValueD}";
611-
}
612623
}
613624

614625
[Test]

src/NUnitFramework/tests/Assertions/AssertThrowsAsyncTests.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,13 @@ public void ThrowsConstraintReturnsCorrectException()
7373
CheckForSpuriousAssertionResults();
7474
}
7575

76+
[Test]
77+
public void ThrowsAsyncIsNotAffectedByAssertionsInDelegate()
78+
{
79+
Assert.ThrowsAsync<AssertionException>(
80+
() => Assert.ThatAsync(AsyncTestDelegates.ThrowsArgumentExceptionAsync, Throws.InvalidOperationException));
81+
}
82+
7683
[Test]
7784
public void CorrectExceptionIsReturnedToMethod()
7885
{

src/NUnitFramework/tests/Constraints/ConstraintTestBase.cs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,16 +49,12 @@ public void SucceedsWithGoodValues(object value)
4949
[Test, TestCaseSource(nameof(FailureData))]
5050
public void FailsWithBadValues(object badValue, string message)
5151
{
52-
string nl = Environment.NewLine;
53-
5452
var constraintResult = TheConstraint.ApplyTo(badValue);
5553
Assert.That(constraintResult.IsSuccess, Is.False);
5654

5755
TextMessageWriter writer = new TextMessageWriter();
5856
constraintResult.WriteMessageTo(writer);
59-
Assert.That(writer.ToString(), Is.EqualTo(
60-
TextMessageWriter.Pfx_Expected + ExpectedDescription + nl +
61-
TextMessageWriter.Pfx_Actual + message + nl));
57+
Assert.That(writer.ToString(), Does.Contain(ExpectedDescription).And.Contains(message));
6258
}
6359
}
6460
}

0 commit comments

Comments
 (0)