From 713452aefe7414ea9499cec24ebddb0d6f7f892e Mon Sep 17 00:00:00 2001 From: Manfred Brands Date: Sat, 7 Sep 2024 17:10:02 +0800 Subject: [PATCH 1/5] Added an EqualStringConstraint --- .../Constraints/ConstraintExpression.cs | 8 + .../Constraints/EqualConstraintResult.cs | 15 + .../Constraints/EqualStringConstraint.cs | 258 ++++++++++++++++++ .../framework/Constraints/PrefixConstraint.cs | 2 +- src/NUnitFramework/framework/Is.cs | 8 + .../tests/Constraints/EqualConstraintTests.cs | 16 +- .../tests/Constraints/EqualTest.cs | 8 +- .../tests/Constraints/NotConstraintTests.cs | 2 +- .../Constraints/ThrowsConstraintTests.cs | 4 +- .../tests/Constraints/ToStringTests.cs | 4 +- .../tests/Syntax/EqualityTests.cs | 2 +- .../tests/Syntax/ThrowsTests.cs | 8 +- 12 files changed, 313 insertions(+), 22 deletions(-) create mode 100644 src/NUnitFramework/framework/Constraints/EqualStringConstraint.cs diff --git a/src/NUnitFramework/framework/Constraints/ConstraintExpression.cs b/src/NUnitFramework/framework/Constraints/ConstraintExpression.cs index 536d89fe5a..d0630250b6 100644 --- a/src/NUnitFramework/framework/Constraints/ConstraintExpression.cs +++ b/src/NUnitFramework/framework/Constraints/ConstraintExpression.cs @@ -427,6 +427,14 @@ public EqualConstraint EqualTo(object? expected) return Append(new EqualConstraint(expected)); } + /// + /// Returns a constraint that tests two strings for equality + /// + public EqualStringConstraint EqualTo(string? expected) + { + return Append(new EqualStringConstraint(expected)); + } + #endregion #region SameAs diff --git a/src/NUnitFramework/framework/Constraints/EqualConstraintResult.cs b/src/NUnitFramework/framework/Constraints/EqualConstraintResult.cs index e6d6dd41d8..fe7594bcd5 100644 --- a/src/NUnitFramework/framework/Constraints/EqualConstraintResult.cs +++ b/src/NUnitFramework/framework/Constraints/EqualConstraintResult.cs @@ -57,6 +57,21 @@ public EqualConstraintResult(EqualConstraint constraint, object? actual, bool ha _failurePoints = constraint.HasFailurePoints ? constraint.FailurePoints : Array.Empty(); } + /// + /// Construct an EqualConstraintResult + /// + public EqualConstraintResult(EqualStringConstraint constraint, object? actual, bool caseInsensitive, bool ignoringWhiteSpace, bool clipStrings, bool hasSucceeded) + : base(constraint, actual, hasSucceeded) + { + _expectedValue = constraint.Arguments[0]; + _tolerance = Tolerance.Exact; + _comparingProperties = false; + _caseInsensitive = caseInsensitive; + _ignoringWhiteSpace = ignoringWhiteSpace; + _clipStrings = clipStrings; + _failurePoints = Array.Empty(); + } + /// /// Write a failure message. Overridden to provide custom /// failure messages for EqualConstraint. diff --git a/src/NUnitFramework/framework/Constraints/EqualStringConstraint.cs b/src/NUnitFramework/framework/Constraints/EqualStringConstraint.cs new file mode 100644 index 0000000000..3f11ab76d7 --- /dev/null +++ b/src/NUnitFramework/framework/Constraints/EqualStringConstraint.cs @@ -0,0 +1,258 @@ +// Copyright (c) Charlie Poole, Rob Prouse and Contributors. MIT License - see LICENSE.txt + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Text; +using NUnit.Framework.Constraints.Comparers; + +namespace NUnit.Framework.Constraints +{ + /// + /// EqualConstraint is able to compare an actual value with the + /// expected value provided in its constructor. Two objects are + /// considered equal if both are null, or if both have the same + /// value. NUnit has special semantics for some object types. + /// + public class EqualStringConstraint : Constraint + { + #region Static and Instance Fields + + private readonly string? _expected; + + private Func _comparer; + private Func? _nonTypedComparer; + + private bool _caseInsensitive; + private bool _ignoringWhiteSpace; + private bool _clipStrings; + + #endregion + + #region Constructor + + /// + /// Initializes a new instance of the class. + /// + /// The expected value. + public EqualStringConstraint(string? expected) + : base(expected) + { + _expected = expected; + _clipStrings = true; + + _comparer = (x, y) => StringsComparer.Equals(x, y, _caseInsensitive, _ignoringWhiteSpace); + } + + #endregion + + /// + /// Gets the expected value. + /// + public string? Expected => _expected; + + #region Constraint Modifiers + + /// + /// Flag the constraint to ignore case and return self. + /// + public EqualStringConstraint IgnoreCase + { + get + { + _caseInsensitive = true; + return this; + } + } + + /// + /// Flag the constraint to ignore white space and return self. + /// + public EqualStringConstraint IgnoreWhiteSpace + { + get + { + _ignoringWhiteSpace = true; + return this; + } + } + + /// + /// Flag the constraint to suppress string clipping + /// and return self. + /// + public EqualStringConstraint NoClip + { + get + { + _clipStrings = false; + return this; + } + } + + /// + /// Flag the constraint to use the supplied IEqualityComparer object. + /// + /// The IComparer object to use. + /// Self. + public EqualStringConstraint Using(IEqualityComparer comparer) + { + _comparer = (x, y) => comparer.Equals(x, y); + return this; + } + + /// + /// Flag the constraint to use the supplied IEqualityComparer object. + /// + /// The IComparer object to use. + /// Self. + public EqualStringConstraint Using(Func comparer) + { + _comparer = comparer; + return this; + } + + /// + /// Flag the constraint to use the supplied IComparer object. + /// + /// The IComparer object to use. + /// Self. + public EqualStringConstraint Using(IComparer comparer) + { + _comparer = (x, y) => comparer.Compare(x, y) == 0; + return this; + } + + /// + /// Flag the constraint to use the supplied Comparison object. + /// + /// The IComparer object to use. + /// Self. + public EqualStringConstraint Using(Comparison comparer) + { + _comparer = (x, y) => comparer.Invoke(x, y) == 0; + return this; + } + + /// + /// Flag the constraint to use the supplied IEqualityComparer object. + /// + /// The IComparer object to use. + /// Self. + public EqualStringConstraint Using(IEqualityComparer comparer) + { + _nonTypedComparer = (x, y) => comparer.Equals(x, y); + return this; + } + + /// + /// Flag the constraint to use the supplied IEqualityComparer object. + /// + /// The IComparer object to use. + /// Self. + public EqualStringConstraint Using(IComparer comparer) + { + _nonTypedComparer = (x, y) => comparer.Compare(x, y) == 0; + return this; + } + + /// + /// Flag the constraint to use the supplied IComparer object. + /// + /// The IComparer object to use. + /// Self. + public EqualStringConstraint Using(IComparer comparer) + { + _nonTypedComparer = (x, y) => comparer.Compare((TOther)x, (TOther)y) == 0; + return this; + } + + #endregion + + #region Public Methods + + /// + /// Test whether the constraint is satisfied by a given value + /// + /// The value to be tested + /// True for success, false for failure + public ConstraintResult ApplyTo(string? actual) + { + bool hasSucceeded; + + if (actual is null) + { + hasSucceeded = _expected is null; + } + else if (_expected is null) + { + hasSucceeded = false; + } + else if (_nonTypedComparer is not null) + { + hasSucceeded = _nonTypedComparer.Invoke(_expected, actual); + } + else + { + hasSucceeded = _comparer.Invoke(_expected, actual); + } + + return ConstraintResult(actual, hasSucceeded); + } + + /// + /// + /// I wish we could hide this method, but it is public in the base class. + /// + public sealed override ConstraintResult ApplyTo(TActual actual) + { + bool hasSucceeded; + + if (actual is null) + { + hasSucceeded = _expected is null; + } + else if (_expected is null) + { + hasSucceeded = false; + } + else if (_nonTypedComparer is not null) + { + hasSucceeded = _nonTypedComparer.Invoke(_expected, actual); + } + else + { + return ApplyTo(actual as string); + } + + return ConstraintResult(actual, hasSucceeded); + } + + private ConstraintResult ConstraintResult(T actual, bool hasSucceeded) + { + return new EqualConstraintResult(this, actual, _caseInsensitive, _ignoringWhiteSpace, _clipStrings, hasSucceeded); + } + + /// + /// The Description of what this constraint tests, for + /// use in messages and in the ConstraintResult. + /// + public override string Description + { + get + { + var sb = new StringBuilder(MsgUtils.FormatValue(_expected)); + + if (_caseInsensitive) + sb.Append(", ignoring case"); + + if (_ignoringWhiteSpace) + sb.Append(", ignoring white-space"); + + return sb.ToString(); + } + } + + #endregion + } +} diff --git a/src/NUnitFramework/framework/Constraints/PrefixConstraint.cs b/src/NUnitFramework/framework/Constraints/PrefixConstraint.cs index 6de627d1c7..ac2ad6abb8 100644 --- a/src/NUnitFramework/framework/Constraints/PrefixConstraint.cs +++ b/src/NUnitFramework/framework/Constraints/PrefixConstraint.cs @@ -43,7 +43,7 @@ protected PrefixConstraint(IResolveConstraint baseConstraint, string description internal static string FormatDescription(string descriptionPrefix, IConstraint baseConstraint) { return string.Format( - baseConstraint is EqualConstraint ? "{0} equal to {1}" : "{0} {1}", + baseConstraint is EqualConstraint or EqualStringConstraint ? "{0} equal to {1}" : "{0} {1}", descriptionPrefix, baseConstraint.Description); } diff --git a/src/NUnitFramework/framework/Is.cs b/src/NUnitFramework/framework/Is.cs index 31360e1ea5..6d50aaaf59 100644 --- a/src/NUnitFramework/framework/Is.cs +++ b/src/NUnitFramework/framework/Is.cs @@ -158,6 +158,14 @@ public static EqualConstraint EqualTo(IEnumerable? expected) return new EqualConstraint(expected); } + /// + /// Returns a constraint that tests two strings for equality + /// + public static EqualStringConstraint EqualTo(string? expected) + { + return new EqualStringConstraint(expected); + } + #endregion #region SameAs diff --git a/src/NUnitFramework/tests/Constraints/EqualConstraintTests.cs b/src/NUnitFramework/tests/Constraints/EqualConstraintTests.cs index d278c23ba6..f79b7832bf 100644 --- a/src/NUnitFramework/tests/Constraints/EqualConstraintTests.cs +++ b/src/NUnitFramework/tests/Constraints/EqualConstraintTests.cs @@ -49,7 +49,7 @@ public void Complex_PassesEquality() [Test] public void RespectsCultureWhenCaseIgnored() { - var constraint = new EqualConstraint("r\u00E9sum\u00E9").IgnoreCase; + var constraint = new EqualStringConstraint("r\u00E9sum\u00E9").IgnoreCase; var result = constraint.ApplyTo("re\u0301sume\u0301"); @@ -59,7 +59,7 @@ public void RespectsCultureWhenCaseIgnored() [Test] public void DoesntRespectCultureWhenCasingMatters() { - var constraint = new EqualConstraint("r\u00E9sum\u00E9"); + var constraint = new EqualStringConstraint("r\u00E9sum\u00E9"); var result = constraint.ApplyTo("re\u0301sume\u0301"); @@ -69,7 +69,7 @@ public void DoesntRespectCultureWhenCasingMatters() [Test] public void IgnoreWhiteSpace() { - var constraint = new EqualConstraint("Hello World").IgnoreWhiteSpace; + var constraint = new EqualStringConstraint("Hello World").IgnoreWhiteSpace; var result = constraint.ApplyTo("Hello\tWorld"); @@ -102,7 +102,7 @@ public void ExtendedIgnoreWhiteSpaceExample() [Test] public void IgnoreWhiteSpaceFail() { - var constraint = new EqualConstraint("Hello World").IgnoreWhiteSpace; + var constraint = new EqualStringConstraint("Hello World").IgnoreWhiteSpace; var result = constraint.ApplyTo("Hello Universe"); @@ -112,7 +112,7 @@ public void IgnoreWhiteSpaceFail() [Test] public void IgnoreWhiteSpaceAndIgnoreCase() { - var constraint = new EqualConstraint("Hello World").IgnoreWhiteSpace.IgnoreCase; + var constraint = new EqualStringConstraint("Hello World").IgnoreWhiteSpace.IgnoreCase; var result = constraint.ApplyTo("hello\r\nworld\r\n"); @@ -804,7 +804,9 @@ public class ObjectEquality [Test] public void CompareObjectsWithToleranceAsserts() { - Assert.Throws(() => Assert.That("abc", new EqualConstraint("abcd").Within(1))); + // This now no longer compiles as EqualStringConstraint doesn't support Tolerance. + // Assert.Throws(() => Assert.That("abc", new EqualStringConstraint("abcd").Within(1))); + Assert.Pass("EqualStringConstraint does not support Tolerance, so this test is not applicable."); } } @@ -949,7 +951,7 @@ public void UsesProvidedLambda_IntArgs() [Test, SetCulture("en-US")] public void UsesProvidedLambda_StringArgs() { - Assert.That("hello", Is.EqualTo("HELLO").Using((x, y) => string.Compare(x, y, StringComparison.CurrentCultureIgnoreCase))); + Assert.That("hello", Is.EqualTo("HELLO").Using((x, y) => string.Compare(x, y, StringComparison.CurrentCultureIgnoreCase))); } [Test] diff --git a/src/NUnitFramework/tests/Constraints/EqualTest.cs b/src/NUnitFramework/tests/Constraints/EqualTest.cs index 88933e93ce..49bc73ec7e 100644 --- a/src/NUnitFramework/tests/Constraints/EqualTest.cs +++ b/src/NUnitFramework/tests/Constraints/EqualTest.cs @@ -15,7 +15,7 @@ public void FailedStringMatchShowsFailurePosition() CheckExceptionMessage( Assert.Throws(() => { - Assert.That("abcdgfe", new EqualConstraint("abcdefg")); + Assert.That("abcdgfe", Is.EqualTo("abcdefg")); })); } @@ -30,7 +30,7 @@ public void LongStringsAreTruncated() CheckExceptionMessage( Assert.Throws(() => { - Assert.That(actual, new EqualConstraint(expected)); + Assert.That(actual, Is.EqualTo(expected)); })); } @@ -43,7 +43,7 @@ public void LongStringsAreTruncatedAtBothEndsIfNecessary() CheckExceptionMessage( Assert.Throws(() => { - Assert.That(actual, new EqualConstraint(expected)); + Assert.That(actual, Is.EqualTo(expected)); })); } @@ -56,7 +56,7 @@ public void LongStringsAreTruncatedAtFrontEndIfNecessary() CheckExceptionMessage( Assert.Throws(() => { - Assert.That(actual, new EqualConstraint(expected)); + Assert.That(actual, Is.EqualTo(expected)); })); } diff --git a/src/NUnitFramework/tests/Constraints/NotConstraintTests.cs b/src/NUnitFramework/tests/Constraints/NotConstraintTests.cs index 7579ec3ba0..2dcd211944 100644 --- a/src/NUnitFramework/tests/Constraints/NotConstraintTests.cs +++ b/src/NUnitFramework/tests/Constraints/NotConstraintTests.cs @@ -24,7 +24,7 @@ public void SetUp() [Test] public void NotHonorsIgnoreCaseUsingConstructors() { - var ex = Assert.Throws(() => Assert.That("abc", new NotConstraint(new EqualConstraint("ABC").IgnoreCase))); + var ex = Assert.Throws(() => Assert.That("abc", new NotConstraint(new EqualStringConstraint("ABC").IgnoreCase))); Assert.That(ex?.Message, Does.Contain("ignoring case")); } diff --git a/src/NUnitFramework/tests/Constraints/ThrowsConstraintTests.cs b/src/NUnitFramework/tests/Constraints/ThrowsConstraintTests.cs index 257ed2b2fc..654ca2d96a 100644 --- a/src/NUnitFramework/tests/Constraints/ThrowsConstraintTests.cs +++ b/src/NUnitFramework/tests/Constraints/ThrowsConstraintTests.cs @@ -67,13 +67,13 @@ public class ThrowsConstraintTest_WithConstraint : ThrowsConstraintTestBase protected override Constraint TheConstraint { get; } = new ThrowsConstraint( new AndConstraint( new ExceptionTypeConstraint(typeof(ArgumentException)), - new PropertyConstraint("ParamName", new EqualConstraint("myParam")))); + new PropertyConstraint("ParamName", new EqualStringConstraint("myParam")))); [SetUp] public void SetUp() { ExpectedDescription = @" and property ParamName equal to ""myParam"""; - StringRepresentation = @" >>>"; + StringRepresentation = @" >>>"; } #pragma warning disable IDE0052 // Remove unread private members diff --git a/src/NUnitFramework/tests/Constraints/ToStringTests.cs b/src/NUnitFramework/tests/Constraints/ToStringTests.cs index ec98fd50e9..dd901bd507 100644 --- a/src/NUnitFramework/tests/Constraints/ToStringTests.cs +++ b/src/NUnitFramework/tests/Constraints/ToStringTests.cs @@ -24,7 +24,7 @@ public void CanDisplaySimpleConstraints_Resolved() Assert.That(constraint.Resolve().ToString(), Is.EqualTo("")); constraint = Has.Attribute(typeof(TestAttribute)).With.Property("Description").EqualTo("smoke"); Assert.That(constraint.Resolve().ToString(), - Is.EqualTo(">>")); + Is.EqualTo(">>")); } [Test] @@ -34,7 +34,7 @@ public void DisplayPrefixConstraints_Unresolved() Assert.That(Is.Not.All.EqualTo(5).ToString(), Is.EqualTo(">")); Assert.That(Has.Property("X").EqualTo(5).ToString(), Is.EqualTo(">")); Assert.That(Has.Attribute(typeof(TestAttribute)).With.Property("Description").EqualTo("smoke").ToString(), - Is.EqualTo(">")); + Is.EqualTo(">")); } [Test] diff --git a/src/NUnitFramework/tests/Syntax/EqualityTests.cs b/src/NUnitFramework/tests/Syntax/EqualityTests.cs index 6cd100ae8b..f7621c86c2 100644 --- a/src/NUnitFramework/tests/Syntax/EqualityTests.cs +++ b/src/NUnitFramework/tests/Syntax/EqualityTests.cs @@ -20,7 +20,7 @@ public class EqualToTest_IgnoreCase : SyntaxTest [SetUp] public void SetUp() { - ParseTree = @""; + ParseTree = @""; StaticSyntax = Is.EqualTo("X").IgnoreCase; BuilderSyntax = Builder().EqualTo("X").IgnoreCase; } diff --git a/src/NUnitFramework/tests/Syntax/ThrowsTests.cs b/src/NUnitFramework/tests/Syntax/ThrowsTests.cs index 6a1c26ab9e..dae66bbaee 100644 --- a/src/NUnitFramework/tests/Syntax/ThrowsTests.cs +++ b/src/NUnitFramework/tests/Syntax/ThrowsTests.cs @@ -23,7 +23,7 @@ public void ThrowsExceptionWithConstraint() { IResolveConstraint expr = Throws.Exception.With.Property("ParamName").EqualTo("myParam"); Assert.That( - expr.Resolve().ToString(), Is.EqualTo(@">>")); + expr.Resolve().ToString(), Is.EqualTo(@">>")); } [Test] @@ -47,7 +47,7 @@ public void ThrowsTypeOfAndConstraint() { IResolveConstraint expr = Throws.TypeOf(typeof(ArgumentException)).And.Property("ParamName").EqualTo("myParam"); Assert.That( - expr.Resolve().ToString(), Is.EqualTo(@" >>>")); + expr.Resolve().ToString(), Is.EqualTo(@" >>>")); } [Test] @@ -55,7 +55,7 @@ public void ThrowsExceptionTypeOfAndConstraint() { IResolveConstraint expr = Throws.Exception.TypeOf(typeof(ArgumentException)).And.Property("ParamName").EqualTo("myParam"); Assert.That( - expr.Resolve().ToString(), Is.EqualTo(@" >>>")); + expr.Resolve().ToString(), Is.EqualTo(@" >>>")); } [Test] @@ -63,7 +63,7 @@ public void ThrowsTypeOfWithConstraint() { IResolveConstraint expr = Throws.TypeOf(typeof(ArgumentException)).With.Property("ParamName").EqualTo("myParam"); Assert.That( - expr.Resolve().ToString(), Is.EqualTo(@" >>>")); + expr.Resolve().ToString(), Is.EqualTo(@" >>>")); } [Test] From 059a3934f623fe0ab93c6634b7046e0b52c385e6 Mon Sep 17 00:00:00 2001 From: Manfred Brands Date: Sun, 6 Oct 2024 13:27:33 +0800 Subject: [PATCH 2/5] Added an EqualTimeBaseConstraint --- .../Constraints/ConstraintBuilder.cs | 11 ++ .../Constraints/ConstraintExpression.cs | 24 ++++ .../EqualDateTimeOffsetConstraint.cs | 50 ++++++++ ...lDateTimeOffsetConstraintWithSameOffset.cs | 79 ++++++++++++ ...eBasedConstraintWithNumericTolerance{T}.cs | 89 ++++++++++++++ ...BasedConstraintWithTimeSpanTolerance{T}.cs | 86 +++++++++++++ .../EqualTimeBasedConstraint{T}.cs | 116 ++++++++++++++++++ src/NUnitFramework/framework/Is.cs | 24 ++++ .../tests/Constraints/EqualConstraintTests.cs | 8 ++ 9 files changed, 487 insertions(+) create mode 100644 src/NUnitFramework/framework/Constraints/EqualDateTimeOffsetConstraint.cs create mode 100644 src/NUnitFramework/framework/Constraints/EqualDateTimeOffsetConstraintWithSameOffset.cs create mode 100644 src/NUnitFramework/framework/Constraints/EqualTimeBasedConstraintWithNumericTolerance{T}.cs create mode 100644 src/NUnitFramework/framework/Constraints/EqualTimeBasedConstraintWithTimeSpanTolerance{T}.cs create mode 100644 src/NUnitFramework/framework/Constraints/EqualTimeBasedConstraint{T}.cs diff --git a/src/NUnitFramework/framework/Constraints/ConstraintBuilder.cs b/src/NUnitFramework/framework/Constraints/ConstraintBuilder.cs index 9b9f8cda1d..4001d8c913 100644 --- a/src/NUnitFramework/framework/Constraints/ConstraintBuilder.cs +++ b/src/NUnitFramework/framework/Constraints/ConstraintBuilder.cs @@ -175,6 +175,17 @@ public void Append(Constraint constraint) constraint.Builder = this; } + /// + /// Replaces the last pushed constraint with the specified constraint. + /// + /// The constraint to replace the lastPushed with. + public void Replace(Constraint constraint) + { + _constraints.Pop(); + _lastPushed = _ops.Top; + Append(constraint); + } + /// /// Sets the top operator right context. /// diff --git a/src/NUnitFramework/framework/Constraints/ConstraintExpression.cs b/src/NUnitFramework/framework/Constraints/ConstraintExpression.cs index d0630250b6..ff42266215 100644 --- a/src/NUnitFramework/framework/Constraints/ConstraintExpression.cs +++ b/src/NUnitFramework/framework/Constraints/ConstraintExpression.cs @@ -435,6 +435,30 @@ public EqualStringConstraint EqualTo(string? expected) return Append(new EqualStringConstraint(expected)); } + /// + /// Returns a constraint that tests two date time offset instances for equality + /// + public EqualDateTimeOffsetConstraint EqualTo(DateTimeOffset expected) + { + return Append(new EqualDateTimeOffsetConstraint(expected)); + } + + /// + /// Returns a constraint that tests two date time instances for equality + /// + public EqualTimeBaseConstraint EqualTo(DateTime expected) + { + return Append(new EqualTimeBaseConstraint(expected, x => x.Ticks)); + } + + /// + /// Returns a constraint that tests two timespan instances for equality + /// + public EqualTimeBaseConstraint EqualTo(TimeSpan expected) + { + return Append(new EqualTimeBaseConstraint(expected, x => x.Ticks)); + } + #endregion #region SameAs diff --git a/src/NUnitFramework/framework/Constraints/EqualDateTimeOffsetConstraint.cs b/src/NUnitFramework/framework/Constraints/EqualDateTimeOffsetConstraint.cs new file mode 100644 index 0000000000..3fc3800aef --- /dev/null +++ b/src/NUnitFramework/framework/Constraints/EqualDateTimeOffsetConstraint.cs @@ -0,0 +1,50 @@ +// Copyright (c) Charlie Poole, Rob Prouse and Contributors. MIT License - see LICENSE.txt + +using System; + +namespace NUnit.Framework.Constraints +{ + /// + /// EqualConstraint is able to compare an actual value with the + /// expected value provided in its constructor. Two objects are + /// considered equal if both are null, or if both have the same + /// value. NUnit has special semantics for some object types. + /// + public class EqualDateTimeOffsetConstraint : EqualTimeBaseConstraint + { + #region Constructor + + /// + /// Initializes a new instance of the class. + /// + /// The expected value. + public EqualDateTimeOffsetConstraint(DateTimeOffset expected) + : base(expected, x => x.UtcTicks) + { + } + + #endregion + + #region Constraint Modifiers + + /// + /// Flags the constraint to include + /// property in comparison of two values. + /// + /// + /// Using this modifier does not allow to use the + /// constraint modifier. + /// + public EqualDateTimeOffsetConstraintWithSameOffset WithSameOffset + { + get + { + var constraint = new EqualDateTimeOffsetConstraintWithSameOffset(Expected); + Builder?.Replace(constraint); + return constraint; + } + } + + #endregion + } +} diff --git a/src/NUnitFramework/framework/Constraints/EqualDateTimeOffsetConstraintWithSameOffset.cs b/src/NUnitFramework/framework/Constraints/EqualDateTimeOffsetConstraintWithSameOffset.cs new file mode 100644 index 0000000000..77feefb30c --- /dev/null +++ b/src/NUnitFramework/framework/Constraints/EqualDateTimeOffsetConstraintWithSameOffset.cs @@ -0,0 +1,79 @@ +// Copyright (c) Charlie Poole, Rob Prouse and Contributors. MIT License - see LICENSE.txt + +using System; +using System.Text; + +namespace NUnit.Framework.Constraints +{ + /// + /// EqualConstraint is able to compare an actual value with the + /// expected value provided in its constructor. Two objects are + /// considered equal if both are null, or if both have the same + /// value. NUnit has special semantics for some object types. + /// + public class EqualDateTimeOffsetConstraintWithSameOffset : Constraint + { + private readonly DateTimeOffset _expected; + + #region Constructor + + /// + /// Initializes a new instance of the class. + /// + /// The expected value. + public EqualDateTimeOffsetConstraintWithSameOffset(DateTimeOffset expected) + : base(expected) + { + _expected = expected; + } + + #endregion + + #region Public Methods + + /// + /// Test whether the constraint is satisfied by a given value + /// + /// The value to be tested + /// True for success, false for failure + public ConstraintResult ApplyTo(DateTimeOffset actual) + { + bool hasSucceeded = _expected.Equals(actual) && _expected.Offset == actual.Offset; + + return new ConstraintResult(this, actual, hasSucceeded); + } + + /// + /// Test whether the constraint is satisfied by a given value + /// + /// The value to be tested + /// True for success, false for failure + public override ConstraintResult ApplyTo(TActual actual) + { + if (actual is DateTimeOffset dateTimeOffset) + { + return ApplyTo(dateTimeOffset); + } + + return new ConstraintResult(this, actual, false); + } + + /// + /// The Description of what this constraint tests, for + /// use in messages and in the ConstraintResult. + /// + public override string Description + { + get + { + var sb = new StringBuilder(MsgUtils.FormatValue(_expected)); + + sb.Append(" with the same offset"); + + return sb.ToString(); + } + } + + #endregion + } +} diff --git a/src/NUnitFramework/framework/Constraints/EqualTimeBasedConstraintWithNumericTolerance{T}.cs b/src/NUnitFramework/framework/Constraints/EqualTimeBasedConstraintWithNumericTolerance{T}.cs new file mode 100644 index 0000000000..7ae866d065 --- /dev/null +++ b/src/NUnitFramework/framework/Constraints/EqualTimeBasedConstraintWithNumericTolerance{T}.cs @@ -0,0 +1,89 @@ +// Copyright (c) Charlie Poole, Rob Prouse and Contributors. MIT License - see LICENSE.txt + +using System; + +namespace NUnit.Framework.Constraints +{ + /// + /// EqualConstraint is able to compare an actual value with the + /// expected value provided in its constructor. Two objects are + /// considered equal if both are null, or if both have the same + /// value. NUnit has special semantics for some object types. + /// + public class EqualTimeBasedConstraintWithNumericTolerance + where T : notnull, IEquatable, IComparable + { + private readonly T _expected; + private readonly Func _getTicks; + private readonly double _tolerance; + + #region Constructor + + /// + /// Initializes a new instance of the class. + /// + /// The expected value. + /// Method to extract the Ticks from an instance of . + /// The tolerance to apply when qualified with a unit. + public EqualTimeBasedConstraintWithNumericTolerance(T expected, Func getTicks, double tolerance) + { + _expected = expected; + _tolerance = tolerance; + _getTicks = getTicks; + } + + #endregion + + /// + /// The ConstraintBuilder holding this constraint + /// + public ConstraintBuilder? Builder { get; set; } + + #region Constraint Modifiers + + /// + /// Causes the tolerance to be interpreted as a TimeSpan in days. + /// + /// Self + public EqualTimeBasedConstraintWithTimeSpanTolerance Days => Within(TimeSpan.FromDays(_tolerance)); + + /// + /// Causes the tolerance to be interpreted as a TimeSpan in hours. + /// + /// Self + public EqualTimeBasedConstraintWithTimeSpanTolerance Hours => Within(TimeSpan.FromHours(_tolerance)); + + /// + /// Causes the tolerance to be interpreted as a TimeSpan in minutes. + /// + /// Self + public EqualTimeBasedConstraintWithTimeSpanTolerance Minutes => Within(TimeSpan.FromMinutes(_tolerance)); + + /// + /// Causes the tolerance to be interpreted as a TimeSpan in seconds. + /// + /// Self + public EqualTimeBasedConstraintWithTimeSpanTolerance Seconds => Within(TimeSpan.FromSeconds(_tolerance)); + + /// + /// Causes the tolerance to be interpreted as a TimeSpan in milliseconds. + /// + /// Self + public EqualTimeBasedConstraintWithTimeSpanTolerance Milliseconds => Within(TimeSpan.FromMilliseconds(_tolerance)); + + /// + /// Causes the tolerance to be interpreted as a TimeSpan in clock ticks. + /// + /// Self + public EqualTimeBasedConstraintWithTimeSpanTolerance Ticks => Within(TimeSpan.FromTicks((long)_tolerance)); + + private EqualTimeBasedConstraintWithTimeSpanTolerance Within(TimeSpan amount) + { + var constraint = new EqualTimeBasedConstraintWithTimeSpanTolerance(_expected, _getTicks, amount); + Builder?.Replace(constraint); + return constraint; + } + + #endregion + } +} diff --git a/src/NUnitFramework/framework/Constraints/EqualTimeBasedConstraintWithTimeSpanTolerance{T}.cs b/src/NUnitFramework/framework/Constraints/EqualTimeBasedConstraintWithTimeSpanTolerance{T}.cs new file mode 100644 index 0000000000..f7e779706c --- /dev/null +++ b/src/NUnitFramework/framework/Constraints/EqualTimeBasedConstraintWithTimeSpanTolerance{T}.cs @@ -0,0 +1,86 @@ +// Copyright (c) Charlie Poole, Rob Prouse and Contributors. MIT License - see LICENSE.txt + +using System; +using System.Text; + +namespace NUnit.Framework.Constraints +{ + /// + /// EqualConstraint is able to compare an actual value with the + /// expected value provided in its constructor. Two objects are + /// considered equal if both are null, or if both have the same + /// value. NUnit has special semantics for some object types. + /// + public class EqualTimeBasedConstraintWithTimeSpanTolerance : Constraint + where T : notnull, IEquatable, IComparable + { + private readonly T _expected; + private readonly Func _getTicks; + private readonly TimeSpan _tolerance; + + #region Constructor + + /// + /// Initializes a new instance of the class. + /// + /// The expected value. + /// The tolerance to apply when comparing for equality. + /// Method to extract the Ticks from an instance of . + public EqualTimeBasedConstraintWithTimeSpanTolerance(T expected, Func getTicks, TimeSpan tolerance) + : base(expected) + { + _expected = expected; + _getTicks = getTicks; + _tolerance = tolerance; + } + + #endregion + + /// + /// Test whether the constraint is satisfied by a given value + /// + /// The value to be tested + /// True for success, false for failure + public ConstraintResult ApplyTo(T actual) + { + long ticksExpected = _getTicks(_expected); + long ticksActual = _getTicks(actual); + + bool hasSucceeded = Math.Abs(ticksExpected - ticksActual) <= _tolerance.Ticks; + + return new ConstraintResult(this, actual, hasSucceeded); + } + + /// + /// Test whether the constraint is satisfied by a given value + /// + /// The value to be tested + /// True for success, false for failure + public override ConstraintResult ApplyTo(TActual actual) + { + if (actual is T t) + { + return ApplyTo(t); + } + + return new ConstraintResult(this, actual, false); + } + + /// + /// The Description of what this constraint tests, for + /// use in messages and in the ConstraintResult. + /// + public override string Description + { + get + { + var sb = new StringBuilder(MsgUtils.FormatValue(_expected)); + + sb.Append(" +/- "); + sb.Append(MsgUtils.FormatValue(_tolerance)); + + return sb.ToString(); + } + } + } +} diff --git a/src/NUnitFramework/framework/Constraints/EqualTimeBasedConstraint{T}.cs b/src/NUnitFramework/framework/Constraints/EqualTimeBasedConstraint{T}.cs new file mode 100644 index 0000000000..f042f6dac2 --- /dev/null +++ b/src/NUnitFramework/framework/Constraints/EqualTimeBasedConstraint{T}.cs @@ -0,0 +1,116 @@ +// Copyright (c) Charlie Poole, Rob Prouse and Contributors. MIT License - see LICENSE.txt + +using System; + +namespace NUnit.Framework.Constraints +{ + /// + /// EqualConstraint is able to compare an actual value with the + /// expected value provided in its constructor. Two objects are + /// considered equal if both are null, or if both have the same + /// value. NUnit has special semantics for some object types. + /// + public class EqualTimeBaseConstraint : Constraint + where T : struct, IEquatable, IComparable + { + #region Static and Instance Fields + + private readonly T _expected; + private readonly Func _getTicks; + + #endregion + + #region Constructor + + /// + /// Initializes a new instance of the class. + /// + /// The expected value. + /// Method to extract the Ticks from an instance of . + public EqualTimeBaseConstraint(T expected, Func getTicks) + : base(expected) + { + _expected = expected; + _getTicks = getTicks; + } + + #endregion + + /// + /// Gets the expected value. + /// + public T Expected => _expected; + + #region Constraint Modifiers + + /// + /// Flag the constraint to use a tolerance when determining equality. + /// + /// Tolerance value to be used + /// Self. + public EqualTimeBasedConstraintWithTimeSpanTolerance Within(TimeSpan amount) + { + var constraint = new EqualTimeBasedConstraintWithTimeSpanTolerance(_expected, _getTicks, amount); + Builder?.Replace(constraint); + return constraint; + } + + /// + /// Flag the constraint to use a tolerance when determining equality. + /// + /// Tolerance value to be used + /// Self. + public EqualTimeBasedConstraintWithNumericTolerance Within(double amount) + { + return new EqualTimeBasedConstraintWithNumericTolerance(_expected, _getTicks, amount) + { + Builder = Builder, + }; + } + + #endregion + + #region Public Methods + + /// + /// Test whether the constraint is satisfied by a given value + /// + /// The value to be tested + /// True for success, false for failure + public virtual ConstraintResult ApplyTo(T actual) + { + bool hasSucceeded = _expected.Equals(actual); + + return new ConstraintResult(this, actual, hasSucceeded); + } + + /// + /// Test whether the constraint is satisfied by a given value + /// + /// The value to be tested + /// True for success, false for failure + public override ConstraintResult ApplyTo(TActual actual) + { + if (actual is T t) + { + return ApplyTo(t); + } + + return new ConstraintResult(this, actual, false); + } + + /// + /// The Description of what this constraint tests, for + /// use in messages and in the ConstraintResult. + /// + public override string Description + { + get + { + return MsgUtils.FormatValue(_expected); + } + } + + #endregion + } +} diff --git a/src/NUnitFramework/framework/Is.cs b/src/NUnitFramework/framework/Is.cs index 6d50aaaf59..0dfb6b39df 100644 --- a/src/NUnitFramework/framework/Is.cs +++ b/src/NUnitFramework/framework/Is.cs @@ -166,6 +166,30 @@ public static EqualStringConstraint EqualTo(string? expected) return new EqualStringConstraint(expected); } + /// + /// Returns a constraint that tests two date time offset instances for equality. + /// + public static EqualDateTimeOffsetConstraint EqualTo(DateTimeOffset expected) + { + return new EqualDateTimeOffsetConstraint(expected); + } + + /// + /// Returns a constraint that tests two date time instances for equality. + /// + public static EqualTimeBaseConstraint EqualTo(DateTime expected) + { + return new EqualTimeBaseConstraint(expected, x => x.Ticks); + } + + /// + /// Returns a constraint that tests two timespan instances for equality. + /// + public static EqualTimeBaseConstraint EqualTo(TimeSpan expected) + { + return new EqualTimeBaseConstraint(expected, x => x.Ticks); + } + #endregion #region SameAs diff --git a/src/NUnitFramework/tests/Constraints/EqualConstraintTests.cs b/src/NUnitFramework/tests/Constraints/EqualConstraintTests.cs index f79b7832bf..4abdc721af 100644 --- a/src/NUnitFramework/tests/Constraints/EqualConstraintTests.cs +++ b/src/NUnitFramework/tests/Constraints/EqualConstraintTests.cs @@ -384,6 +384,9 @@ public void CanMatchDatesWithinTicks() Assert.That(actual, new EqualConstraint(expected).Within(TimeSpan.TicksPerMinute * 5).Ticks); } +/* + * This no longer compiles! Preventing illegal code and runtime exceptions. + * [Test] public void ErrorIfDaysPrecedesWithin() { @@ -419,6 +422,7 @@ public void ErrorIfTicksPrecedesWithin() { Assert.Throws(() => Assert.That(DateTime.Now, Is.EqualTo(DateTime.Now).Ticks.Within(5))); } +*/ } #endregion @@ -469,6 +473,9 @@ public void NegativeEqualityTestWithTolerance(DateTimeOffset value1, DateTimeOff Assert.That(value1, Is.Not.EqualTo(value2).Within(1).Minutes); } +/* + * The XML documentation says that WithSameOffset doesn't work together with Within, but the code below would says it is. + * [Theory] public void NegativeEqualityTestWithToleranceAndWithSameOffset(DateTimeOffset value1, DateTimeOffset value2) { @@ -494,6 +501,7 @@ public void NegativeEqualityTestWithinToleranceAndWithSameOffset(DateTimeOffset Assert.That(value1, Is.Not.EqualTo(value2).Within(1).Minutes.WithSameOffset); } +*/ } public class DateTimeOffSetEquality From c8e47fc7ed2fb8d6db31751d15065d1f225120a1 Mon Sep 17 00:00:00 2001 From: Manfred Brands Date: Sun, 8 Sep 2024 02:07:36 +0800 Subject: [PATCH 3/5] Added an EqualNumericConstraint --- .../Constraints/ConstraintExpression.cs | 11 + .../Constraints/EqualConstraintResult.cs | 15 + .../Constraints/EqualNumericConstraint.cs | 305 ++++++++++++++++++ .../EqualNumericConstraintExtensions.cs | 34 ++ .../framework/Constraints/Numerics.cs | 37 +++ .../framework/Constraints/PrefixConstraint.cs | 12 +- src/NUnitFramework/framework/Is.cs | 11 + .../tests/Constraints/EqualConstraintTests.cs | 6 +- .../tests/Constraints/ToleranceTests.cs | 2 - 9 files changed, 427 insertions(+), 6 deletions(-) create mode 100644 src/NUnitFramework/framework/Constraints/EqualNumericConstraint.cs create mode 100644 src/NUnitFramework/framework/Constraints/EqualNumericConstraintExtensions.cs diff --git a/src/NUnitFramework/framework/Constraints/ConstraintExpression.cs b/src/NUnitFramework/framework/Constraints/ConstraintExpression.cs index ff42266215..4f09363560 100644 --- a/src/NUnitFramework/framework/Constraints/ConstraintExpression.cs +++ b/src/NUnitFramework/framework/Constraints/ConstraintExpression.cs @@ -459,6 +459,17 @@ public EqualTimeBaseConstraint EqualTo(TimeSpan expected) return Append(new EqualTimeBaseConstraint(expected, x => x.Ticks)); } + /// + /// Returns a constraint that tests two numbers for equality + /// +#pragma warning disable CS3024 // Constraint type is not CLS-compliant + public EqualNumericConstraint EqualTo(T expected) + where T : unmanaged, IConvertible, IEquatable + { + return Append(new EqualNumericConstraint(expected)); + } +#pragma warning restore CS3024 // Constraint type is not CLS-compliant + #endregion #region SameAs diff --git a/src/NUnitFramework/framework/Constraints/EqualConstraintResult.cs b/src/NUnitFramework/framework/Constraints/EqualConstraintResult.cs index fe7594bcd5..556b50b182 100644 --- a/src/NUnitFramework/framework/Constraints/EqualConstraintResult.cs +++ b/src/NUnitFramework/framework/Constraints/EqualConstraintResult.cs @@ -57,6 +57,21 @@ public EqualConstraintResult(EqualConstraint constraint, object? actual, bool ha _failurePoints = constraint.HasFailurePoints ? constraint.FailurePoints : Array.Empty(); } + /// + /// Construct an EqualConstraintResult + /// + public EqualConstraintResult(Constraint constraint, object? actual, Tolerance tolerance, bool hasSucceeded) + : base(constraint, actual, hasSucceeded) + { + _expectedValue = constraint.Arguments[0]; + _tolerance = tolerance; + _comparingProperties = false; + _caseInsensitive = false; + _ignoringWhiteSpace = false; + _clipStrings = false; + _failurePoints = Array.Empty(); + } + /// /// Construct an EqualConstraintResult /// diff --git a/src/NUnitFramework/framework/Constraints/EqualNumericConstraint.cs b/src/NUnitFramework/framework/Constraints/EqualNumericConstraint.cs new file mode 100644 index 0000000000..57511b306a --- /dev/null +++ b/src/NUnitFramework/framework/Constraints/EqualNumericConstraint.cs @@ -0,0 +1,305 @@ +// Copyright (c) Charlie Poole, Rob Prouse and Contributors. MIT License - see LICENSE.txt + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Text; + +namespace NUnit.Framework.Constraints +{ + /// + /// EqualNumericConstraint is able to compare an actual value with the + /// expected value provided in its constructor. Two objects are + /// considered equal if both are null, or if both have the same + /// value. NUnit has special semantics for some object types. + /// +#pragma warning disable CS3024 // Constraint type is not CLS-compliant + public class EqualNumericConstraint : Constraint + where T : unmanaged, IConvertible, IEquatable +#pragma warning restore CS3024 // Constraint type is not CLS-compliant + { + #region Static and Instance Fields + + private readonly T _expected; + + private Func _comparer; + private Func? _nonTypedComparer; + + private Tolerance _tolerance = Tolerance.Default; + + #endregion + + #region Constructor + + /// + /// Initializes a new instance of the class. + /// + /// The expected value. + public EqualNumericConstraint(T expected) + : base(expected) + { + _expected = expected; + _comparer = (x, y) => Numerics.AreEqual(x, y, ref _tolerance); + } + + #endregion + + #region Properties + + /// + public override string DisplayName => "Equal"; + + /// + /// The expected value. + /// + public T Expected => _expected; + + /// + /// Gets the tolerance for this comparison. + /// + /// + /// The tolerance. + /// + public Tolerance Tolerance => _tolerance; + + #endregion + + #region Constraint Modifiers + + /// + /// Flag the constraint to use a tolerance when determining equality. + /// + /// Tolerance value to be used + /// Self. + public EqualNumericConstraint Within(T amount) + { + if (!_tolerance.IsUnsetOrDefault) + throw new InvalidOperationException("Within modifier may appear only once in a constraint expression"); + + _tolerance = new Tolerance(amount); + return this; + } + + /// + /// Switches the .Within() modifier to interpret its tolerance as + /// a distance in representable values (see remarks). + /// + /// Self. + /// + /// Ulp stands for "unit in the last place" and describes the minimum + /// amount a given value can change. For any integers, an ulp is 1 whole + /// digit. For floating point values, the accuracy of which is better + /// for smaller numbers and worse for larger numbers, an ulp depends + /// on the size of the number. Using ulps for comparison of floating + /// point results instead of fixed tolerances is safer because it will + /// automatically compensate for the added inaccuracy of larger numbers. + /// + public EqualNumericConstraint Ulps + { + get + { + _tolerance = _tolerance.Ulps; + return this; + } + } + + /// + /// Switches the .Within() modifier to interpret its tolerance as + /// a percentage that the actual values is allowed to deviate from + /// the expected value. + /// + /// Self + public EqualNumericConstraint Percent + { + get + { + _tolerance = _tolerance.Percent; + return this; + } + } + + /// + /// Flag the constraint to use the supplied IEqualityComparer object. + /// + /// The IComparer object to use. + /// Self. + public EqualNumericConstraint Using(IEqualityComparer comparer) + { + _comparer = (x, y) => comparer.Equals(x, y); + return this; + } + + /// + /// Flag the constraint to use the supplied boolean-returning delegate. + /// + /// The boolean-returning delegate to use. + /// Self. + public EqualNumericConstraint Using(Func comparer) + { + _comparer = comparer; + return this; + } + + /// + /// Flag the constraint to use the supplied predicate function + /// + /// The comparison function to use. + /// The type of the actual value. Note for collection comparisons this is the element type. + /// The type of the expected value. Note for collection comparisons this is the element type. + /// Self. + public EqualNumericConstraint Using(Func comparer) + { + _nonTypedComparer = (x, y) => comparer.Invoke((TActual)x, (TExpected)y); + return this; + } + + /// + /// Flag the constraint to use the supplied IComparer object. + /// + /// The IComparer object to use. + /// Self. + public EqualNumericConstraint Using(IComparer comparer) + { + _comparer = (x, y) => comparer.Compare(x, y) == 0; + return this; + } + + /// + /// Flag the constraint to use the supplied Comparison object. + /// + /// The IComparer object to use. + /// Self. + public EqualNumericConstraint Using(Comparison comparer) + { + _comparer = (x, y) => comparer.Invoke(x, y) == 0; + return this; + } + + /// + /// Flag the constraint to use the supplied IEqualityComparer object. + /// + /// The IComparer object to use. + /// Self. + public EqualNumericConstraint Using(IEqualityComparer comparer) + { + _nonTypedComparer = (x, y) => comparer.Equals(x, y); + return this; + } + + /// + /// Flag the constraint to use the supplied IComparer object. + /// + /// The IComparer object to use. + /// Self. + public EqualNumericConstraint Using(IComparer comparer) + { + _nonTypedComparer = (x, y) => comparer.Compare(x, y) == 0; + return this; + } + + /// + /// Flag the constraint to use the supplied IComparer object. + /// + /// The IComparer object to use. + /// Self. + public EqualNumericConstraint Using(IComparer comparer) + { + _nonTypedComparer = (x, y) => comparer.Compare((TOther)x, (TOther)y) == 0; + return this; + } + + #endregion + + #region Public Methods + + /// + /// Test whether the constraint is satisfied by a given value + /// + /// The value to be tested + /// True for success, false for failure + public ConstraintResult ApplyTo(T actual) + { + bool hasSucceeded; + + if (_nonTypedComparer is not null) + { + hasSucceeded = _nonTypedComparer.Invoke(_expected, actual); + } + else + { + hasSucceeded = _comparer.Invoke(_expected, actual); + } + + return ConstraintResult(actual, hasSucceeded); + } + + /// + /// Test whether the constraint is satisfied by a given value + /// + /// The value to be tested + /// True for success, false for failure + public override ConstraintResult ApplyTo(TActual actual) + { + bool hasSucceeded; + + if (actual is null) + { + hasSucceeded = false; + } + else if (_nonTypedComparer is not null) + { + hasSucceeded = _nonTypedComparer.Invoke(actual, _expected); + } + else if (actual is T t) + { + hasSucceeded = _comparer.Invoke(t, _expected); + } + else if (actual is IEquatable equatable) + { + hasSucceeded = equatable.Equals(_expected); + } + else if (actual is not string and IConvertible) + { + hasSucceeded = Numerics.AreEqual(actual, _expected, ref _tolerance); + } + else + { + hasSucceeded = false; + } + + return ConstraintResult(actual, hasSucceeded); + } + + private ConstraintResult ConstraintResult(TActual actual, bool hasSucceeded) + { + return new EqualConstraintResult(this, actual, _tolerance, hasSucceeded); + } + + /// + /// The Description of what this constraint tests, for + /// use in messages and in the ConstraintResult. + /// + public override string Description + { + get + { + var sb = new StringBuilder(MsgUtils.FormatValue(_expected)); + + if (_tolerance is not null && _tolerance.HasVariance) + { + sb.Append(" +/- "); + sb.Append(MsgUtils.FormatValue(_tolerance.Amount)); + if (_tolerance.Mode != ToleranceMode.Linear) + { + sb.Append(" "); + sb.Append(_tolerance.Mode.ToString()); + } + } + + return sb.ToString(); + } + } + + #endregion + } +} diff --git a/src/NUnitFramework/framework/Constraints/EqualNumericConstraintExtensions.cs b/src/NUnitFramework/framework/Constraints/EqualNumericConstraintExtensions.cs new file mode 100644 index 0000000000..362587b41a --- /dev/null +++ b/src/NUnitFramework/framework/Constraints/EqualNumericConstraintExtensions.cs @@ -0,0 +1,34 @@ +// Copyright (c) Charlie Poole, Rob Prouse and Contributors. MIT License - see LICENSE.txt + +using NUnit.Framework.Constraints; + +namespace NUnit.Framework +{ + /// + /// Extension methods for . + /// + public static class EqualNumericConstraintExtensions + { + /// + /// Flag the constraint to use a tolerance when determining equality. + /// + /// The original constraint. + /// Tolerance value to be used + /// Original constraint promoted to . + public static EqualNumericConstraint Within(this EqualNumericConstraint constraint, double amount) + { + return new EqualNumericConstraint(constraint.Expected).Within(amount); + } + + /// + /// Flag the constraint to use a tolerance when determining equality. + /// + /// The original constraint. + /// Tolerance value to be used + /// Original constraint promoted to . + public static EqualNumericConstraint Within(this EqualNumericConstraint constraint, double amount) + { + return new EqualNumericConstraint(constraint.Expected).Within(amount); + } + } +} diff --git a/src/NUnitFramework/framework/Constraints/Numerics.cs b/src/NUnitFramework/framework/Constraints/Numerics.cs index 3982e4e2c4..cb3619a6e9 100644 --- a/src/NUnitFramework/framework/Constraints/Numerics.cs +++ b/src/NUnitFramework/framework/Constraints/Numerics.cs @@ -161,6 +161,43 @@ public static bool AreEqual(object expected, object actual, ref Tolerance tolera return AreEqual(Convert.ToInt32(expected), Convert.ToInt32(actual), tolerance); } + /// + /// Test two numeric values for equality, performing the usual numeric + /// conversions and using a provided or default tolerance. If the tolerance + /// provided is Empty, this method may set it to a default tolerance. + /// + /// The expected value + /// The actual value + /// A reference to the tolerance in effect + /// True if the values are equal + public static bool AreEqual(T1 expected, T2 actual, ref Tolerance tolerance) + where T1 : unmanaged, IConvertible + where T2 : unmanaged, IConvertible + { + if (expected is double || actual is double) + return AreEqual(expected.ToDouble(null), actual.ToDouble(null), ref tolerance); + + if (expected is float || actual is float) + return AreEqual(expected.ToSingle(null), actual.ToSingle(null), ref tolerance); + + if (tolerance.Mode == ToleranceMode.Ulps) + throw new InvalidOperationException("Ulps may only be specified for floating point arguments"); + + if (expected is decimal || actual is decimal) + return AreEqual(expected.ToDecimal(null), actual.ToDecimal(null), tolerance); + + if (expected is ulong || actual is ulong) + return AreEqual(expected.ToUInt64(null), actual.ToUInt64(null), tolerance); + + if (expected is long || actual is long) + return AreEqual(expected.ToInt64(null), actual.ToInt64(null), tolerance); + + if (expected is uint || actual is uint) + return AreEqual(expected.ToUInt32(null), actual.ToUInt32(null), tolerance); + + return AreEqual(expected.ToInt32(null), actual.ToInt32(null), tolerance); + } + private static bool AreEqual(double expected, double actual, ref Tolerance tolerance) { if (double.IsNaN(expected) && double.IsNaN(actual)) diff --git a/src/NUnitFramework/framework/Constraints/PrefixConstraint.cs b/src/NUnitFramework/framework/Constraints/PrefixConstraint.cs index ac2ad6abb8..2f785c03c0 100644 --- a/src/NUnitFramework/framework/Constraints/PrefixConstraint.cs +++ b/src/NUnitFramework/framework/Constraints/PrefixConstraint.cs @@ -1,5 +1,7 @@ // Copyright (c) Charlie Poole, Rob Prouse and Contributors. MIT License - see LICENSE.txt +using System; + namespace NUnit.Framework.Constraints { /// @@ -42,8 +44,16 @@ protected PrefixConstraint(IResolveConstraint baseConstraint, string description /// internal static string FormatDescription(string descriptionPrefix, IConstraint baseConstraint) { + bool isEqualConstraint = baseConstraint is EqualConstraint or EqualStringConstraint; + + if (!isEqualConstraint) + { + Type constraintType = baseConstraint.GetType(); + isEqualConstraint = constraintType.IsGenericType && constraintType.GetGenericTypeDefinition() == typeof(EqualNumericConstraint<>); + } + return string.Format( - baseConstraint is EqualConstraint or EqualStringConstraint ? "{0} equal to {1}" : "{0} {1}", + isEqualConstraint ? "{0} equal to {1}" : "{0} {1}", descriptionPrefix, baseConstraint.Description); } diff --git a/src/NUnitFramework/framework/Is.cs b/src/NUnitFramework/framework/Is.cs index 0dfb6b39df..cf1960fca1 100644 --- a/src/NUnitFramework/framework/Is.cs +++ b/src/NUnitFramework/framework/Is.cs @@ -190,6 +190,17 @@ public static EqualTimeBaseConstraint EqualTo(TimeSpan expected) return new EqualTimeBaseConstraint(expected, x => x.Ticks); } + /// + /// Returns a constraint that tests two numbers for equality + /// +#pragma warning disable CS3024 // Constraint type is not CLS-compliant + public static EqualNumericConstraint EqualTo(T expected) + where T : unmanaged, IConvertible, IEquatable + { + return new EqualNumericConstraint(expected); + } +#pragma warning restore CS3024 // Constraint type is not CLS-compliant + #endregion #region SameAs diff --git a/src/NUnitFramework/tests/Constraints/EqualConstraintTests.cs b/src/NUnitFramework/tests/Constraints/EqualConstraintTests.cs index 4abdc721af..f6f7f80432 100644 --- a/src/NUnitFramework/tests/Constraints/EqualConstraintTests.cs +++ b/src/NUnitFramework/tests/Constraints/EqualConstraintTests.cs @@ -939,7 +939,7 @@ public void UsesProvidedGenericEqualityComparison() var comparer = new GenericEqualityComparison(); Assert.Multiple(() => { - Assert.That(2 + 2, Is.EqualTo(4).Using(comparer.Delegate)); + Assert.That(2 + 2, Is.EqualTo(4).Using(comparer.Delegate)); Assert.That(comparer.WasCalled, "Comparer was not called"); }); } @@ -947,13 +947,13 @@ public void UsesProvidedGenericEqualityComparison() [Test] public void UsesBooleanReturningDelegate() { - Assert.That(2 + 2, Is.EqualTo(4).Using((x, y) => x.Equals(y))); + Assert.That(2 + 2, Is.EqualTo(4).Using((x, y) => x.Equals(y))); } [Test] public void UsesProvidedLambda_IntArgs() { - Assert.That(2 + 2, Is.EqualTo(4).Using((x, y) => x.CompareTo(y))); + Assert.That(2 + 2, Is.EqualTo(4).Using((x, y) => x.CompareTo(y))); } [Test, SetCulture("en-US")] diff --git a/src/NUnitFramework/tests/Constraints/ToleranceTests.cs b/src/NUnitFramework/tests/Constraints/ToleranceTests.cs index 46ff690f75..4a3595e1ab 100644 --- a/src/NUnitFramework/tests/Constraints/ToleranceTests.cs +++ b/src/NUnitFramework/tests/Constraints/ToleranceTests.cs @@ -139,8 +139,6 @@ public void ToStringTests() Assert.That(Is.EqualTo(5).Within(2).Tolerance.ToString(), Is.EqualTo("2")); Assert.That(Is.EqualTo(5).Within(2).Ulps.Tolerance.ToString(), Is.EqualTo("2 Ulps")); Assert.That(Is.EqualTo(5).Within(2).Percent.Tolerance.ToString(), Is.EqualTo("2 Percent")); - Assert.That(Is.EqualTo(5).Within(2).Seconds.Tolerance.ToString(), Is.EqualTo("00:00:02")); - Assert.That(Is.EqualTo(5).Within(2).Minutes.Tolerance.ToString(), Is.EqualTo("00:02:00")); }); } } From 73ca849ba05370f2c9e816a9b9ac4cb635903f2f Mon Sep 17 00:00:00 2001 From: Manfred Brands Date: Sun, 10 Nov 2024 08:28:22 +0800 Subject: [PATCH 4/5] Restrict Using constraint modifier Use extension methods to remove duplication. Once a user comparer is specfied other modifiers are invalid. Once constraint modifiers are specified one no longer can use a user comparer. .IgnoreCase.Using() or .Using().IgnoreCase makes no IgnoreCase as the user comparer has no access to the IgnoreCase --- .../Constraints/EqualConstraintResult.cs | 17 +- .../Constraints/EqualNumericConstraint.cs | 276 +----------------- .../EqualNumericWithoutUsingConstraint.cs | 196 +++++++++++++ ...umericWithoutUsingConstraintExtensions.cs} | 8 +- .../Constraints/EqualStringConstraint.cs | 234 +-------------- .../EqualStringWithoutUsingConstraint.cs | 165 +++++++++++ .../Constraints/EqualUsingConstraint.cs | 196 +++++++++++++ .../Constraints/IEqualWithUsingConstraint.cs | 15 + .../IEqualWithUsingConstraintExtensions.cs | 151 ++++++++++ .../tests/Constraints/EqualConstraintTests.cs | 4 +- 10 files changed, 747 insertions(+), 515 deletions(-) create mode 100644 src/NUnitFramework/framework/Constraints/EqualNumericWithoutUsingConstraint.cs rename src/NUnitFramework/framework/Constraints/{EqualNumericConstraintExtensions.cs => EqualNumericWithoutUsingConstraintExtensions.cs} (71%) create mode 100644 src/NUnitFramework/framework/Constraints/EqualStringWithoutUsingConstraint.cs create mode 100644 src/NUnitFramework/framework/Constraints/EqualUsingConstraint.cs create mode 100644 src/NUnitFramework/framework/Constraints/IEqualWithUsingConstraint.cs create mode 100644 src/NUnitFramework/framework/Constraints/IEqualWithUsingConstraintExtensions.cs diff --git a/src/NUnitFramework/framework/Constraints/EqualConstraintResult.cs b/src/NUnitFramework/framework/Constraints/EqualConstraintResult.cs index 556b50b182..c946d82028 100644 --- a/src/NUnitFramework/framework/Constraints/EqualConstraintResult.cs +++ b/src/NUnitFramework/framework/Constraints/EqualConstraintResult.cs @@ -75,7 +75,22 @@ public EqualConstraintResult(Constraint constraint, object? actual, Tolerance to /// /// Construct an EqualConstraintResult /// - public EqualConstraintResult(EqualStringConstraint constraint, object? actual, bool caseInsensitive, bool ignoringWhiteSpace, bool clipStrings, bool hasSucceeded) + public EqualConstraintResult(Constraint constraint, object? actual, bool hasSucceeded) + : base(constraint, actual, hasSucceeded) + { + _expectedValue = constraint.Arguments[0]; + _tolerance = Tolerance.Default; + _comparingProperties = false; + _caseInsensitive = false; + _ignoringWhiteSpace = false; + _clipStrings = false; + _failurePoints = Array.Empty(); + } + + /// + /// Construct an EqualConstraintResult + /// + public EqualConstraintResult(EqualStringWithoutUsingConstraint constraint, object? actual, bool caseInsensitive, bool ignoringWhiteSpace, bool clipStrings, bool hasSucceeded) : base(constraint, actual, hasSucceeded) { _expectedValue = constraint.Arguments[0]; diff --git a/src/NUnitFramework/framework/Constraints/EqualNumericConstraint.cs b/src/NUnitFramework/framework/Constraints/EqualNumericConstraint.cs index 57511b306a..c8584dff58 100644 --- a/src/NUnitFramework/framework/Constraints/EqualNumericConstraint.cs +++ b/src/NUnitFramework/framework/Constraints/EqualNumericConstraint.cs @@ -1,9 +1,6 @@ // Copyright (c) Charlie Poole, Rob Prouse and Contributors. MIT License - see LICENSE.txt using System; -using System.Collections; -using System.Collections.Generic; -using System.Text; namespace NUnit.Framework.Constraints { @@ -14,21 +11,10 @@ namespace NUnit.Framework.Constraints /// value. NUnit has special semantics for some object types. /// #pragma warning disable CS3024 // Constraint type is not CLS-compliant - public class EqualNumericConstraint : Constraint + public class EqualNumericConstraint : EqualNumericWithoutUsingConstraint, IEqualWithUsingConstraint where T : unmanaged, IConvertible, IEquatable #pragma warning restore CS3024 // Constraint type is not CLS-compliant { - #region Static and Instance Fields - - private readonly T _expected; - - private Func _comparer; - private Func? _nonTypedComparer; - - private Tolerance _tolerance = Tolerance.Default; - - #endregion - #region Constructor /// @@ -38,266 +24,6 @@ public class EqualNumericConstraint : Constraint public EqualNumericConstraint(T expected) : base(expected) { - _expected = expected; - _comparer = (x, y) => Numerics.AreEqual(x, y, ref _tolerance); - } - - #endregion - - #region Properties - - /// - public override string DisplayName => "Equal"; - - /// - /// The expected value. - /// - public T Expected => _expected; - - /// - /// Gets the tolerance for this comparison. - /// - /// - /// The tolerance. - /// - public Tolerance Tolerance => _tolerance; - - #endregion - - #region Constraint Modifiers - - /// - /// Flag the constraint to use a tolerance when determining equality. - /// - /// Tolerance value to be used - /// Self. - public EqualNumericConstraint Within(T amount) - { - if (!_tolerance.IsUnsetOrDefault) - throw new InvalidOperationException("Within modifier may appear only once in a constraint expression"); - - _tolerance = new Tolerance(amount); - return this; - } - - /// - /// Switches the .Within() modifier to interpret its tolerance as - /// a distance in representable values (see remarks). - /// - /// Self. - /// - /// Ulp stands for "unit in the last place" and describes the minimum - /// amount a given value can change. For any integers, an ulp is 1 whole - /// digit. For floating point values, the accuracy of which is better - /// for smaller numbers and worse for larger numbers, an ulp depends - /// on the size of the number. Using ulps for comparison of floating - /// point results instead of fixed tolerances is safer because it will - /// automatically compensate for the added inaccuracy of larger numbers. - /// - public EqualNumericConstraint Ulps - { - get - { - _tolerance = _tolerance.Ulps; - return this; - } - } - - /// - /// Switches the .Within() modifier to interpret its tolerance as - /// a percentage that the actual values is allowed to deviate from - /// the expected value. - /// - /// Self - public EqualNumericConstraint Percent - { - get - { - _tolerance = _tolerance.Percent; - return this; - } - } - - /// - /// Flag the constraint to use the supplied IEqualityComparer object. - /// - /// The IComparer object to use. - /// Self. - public EqualNumericConstraint Using(IEqualityComparer comparer) - { - _comparer = (x, y) => comparer.Equals(x, y); - return this; - } - - /// - /// Flag the constraint to use the supplied boolean-returning delegate. - /// - /// The boolean-returning delegate to use. - /// Self. - public EqualNumericConstraint Using(Func comparer) - { - _comparer = comparer; - return this; - } - - /// - /// Flag the constraint to use the supplied predicate function - /// - /// The comparison function to use. - /// The type of the actual value. Note for collection comparisons this is the element type. - /// The type of the expected value. Note for collection comparisons this is the element type. - /// Self. - public EqualNumericConstraint Using(Func comparer) - { - _nonTypedComparer = (x, y) => comparer.Invoke((TActual)x, (TExpected)y); - return this; - } - - /// - /// Flag the constraint to use the supplied IComparer object. - /// - /// The IComparer object to use. - /// Self. - public EqualNumericConstraint Using(IComparer comparer) - { - _comparer = (x, y) => comparer.Compare(x, y) == 0; - return this; - } - - /// - /// Flag the constraint to use the supplied Comparison object. - /// - /// The IComparer object to use. - /// Self. - public EqualNumericConstraint Using(Comparison comparer) - { - _comparer = (x, y) => comparer.Invoke(x, y) == 0; - return this; - } - - /// - /// Flag the constraint to use the supplied IEqualityComparer object. - /// - /// The IComparer object to use. - /// Self. - public EqualNumericConstraint Using(IEqualityComparer comparer) - { - _nonTypedComparer = (x, y) => comparer.Equals(x, y); - return this; - } - - /// - /// Flag the constraint to use the supplied IComparer object. - /// - /// The IComparer object to use. - /// Self. - public EqualNumericConstraint Using(IComparer comparer) - { - _nonTypedComparer = (x, y) => comparer.Compare(x, y) == 0; - return this; - } - - /// - /// Flag the constraint to use the supplied IComparer object. - /// - /// The IComparer object to use. - /// Self. - public EqualNumericConstraint Using(IComparer comparer) - { - _nonTypedComparer = (x, y) => comparer.Compare((TOther)x, (TOther)y) == 0; - return this; - } - - #endregion - - #region Public Methods - - /// - /// Test whether the constraint is satisfied by a given value - /// - /// The value to be tested - /// True for success, false for failure - public ConstraintResult ApplyTo(T actual) - { - bool hasSucceeded; - - if (_nonTypedComparer is not null) - { - hasSucceeded = _nonTypedComparer.Invoke(_expected, actual); - } - else - { - hasSucceeded = _comparer.Invoke(_expected, actual); - } - - return ConstraintResult(actual, hasSucceeded); - } - - /// - /// Test whether the constraint is satisfied by a given value - /// - /// The value to be tested - /// True for success, false for failure - public override ConstraintResult ApplyTo(TActual actual) - { - bool hasSucceeded; - - if (actual is null) - { - hasSucceeded = false; - } - else if (_nonTypedComparer is not null) - { - hasSucceeded = _nonTypedComparer.Invoke(actual, _expected); - } - else if (actual is T t) - { - hasSucceeded = _comparer.Invoke(t, _expected); - } - else if (actual is IEquatable equatable) - { - hasSucceeded = equatable.Equals(_expected); - } - else if (actual is not string and IConvertible) - { - hasSucceeded = Numerics.AreEqual(actual, _expected, ref _tolerance); - } - else - { - hasSucceeded = false; - } - - return ConstraintResult(actual, hasSucceeded); - } - - private ConstraintResult ConstraintResult(TActual actual, bool hasSucceeded) - { - return new EqualConstraintResult(this, actual, _tolerance, hasSucceeded); - } - - /// - /// The Description of what this constraint tests, for - /// use in messages and in the ConstraintResult. - /// - public override string Description - { - get - { - var sb = new StringBuilder(MsgUtils.FormatValue(_expected)); - - if (_tolerance is not null && _tolerance.HasVariance) - { - sb.Append(" +/- "); - sb.Append(MsgUtils.FormatValue(_tolerance.Amount)); - if (_tolerance.Mode != ToleranceMode.Linear) - { - sb.Append(" "); - sb.Append(_tolerance.Mode.ToString()); - } - } - - return sb.ToString(); - } } #endregion diff --git a/src/NUnitFramework/framework/Constraints/EqualNumericWithoutUsingConstraint.cs b/src/NUnitFramework/framework/Constraints/EqualNumericWithoutUsingConstraint.cs new file mode 100644 index 0000000000..6c70bd49ee --- /dev/null +++ b/src/NUnitFramework/framework/Constraints/EqualNumericWithoutUsingConstraint.cs @@ -0,0 +1,196 @@ +// Copyright (c) Charlie Poole, Rob Prouse and Contributors. MIT License - see LICENSE.txt + +using System; +using System.Text; + +namespace NUnit.Framework.Constraints +{ + /// + /// EqualNumericConstraint is able to compare an actual value with the + /// expected value provided in its constructor. Two objects are + /// considered equal if both are null, or if both have the same + /// value. NUnit has special semantics for some object types. + /// +#pragma warning disable CS3024 // Constraint type is not CLS-compliant + public class EqualNumericWithoutUsingConstraint : Constraint + where T : unmanaged, IConvertible, IEquatable +#pragma warning restore CS3024 // Constraint type is not CLS-compliant + { + #region Static and Instance Fields + + private readonly T _expected; + + private Tolerance _tolerance = Tolerance.Default; + + #endregion + + #region Constructor + + /// + /// Initializes a new instance of the class. + /// + /// The expected value. + public EqualNumericWithoutUsingConstraint(T expected) + : base(expected) + { + _expected = expected; + } + + #endregion + + #region Properties + + /// + public override string DisplayName => "Equal"; + + /// + /// The expected value. + /// + public T Expected => _expected; + + /// + /// Gets the tolerance for this comparison. + /// + /// + /// The tolerance. + /// + public Tolerance Tolerance => _tolerance; + + #endregion + + #region Constraint Modifiers + + /// + /// Flag the constraint to use a tolerance when determining equality. + /// + /// Tolerance value to be used + /// Self. + public EqualNumericWithoutUsingConstraint Within(T amount) + { + if (!_tolerance.IsUnsetOrDefault) + throw new InvalidOperationException("Within modifier may appear only once in a constraint expression"); + + _tolerance = new Tolerance(amount); + return this; + } + + /// + /// Switches the .Within() modifier to interpret its tolerance as + /// a distance in representable values (see remarks). + /// + /// Self. + /// + /// Ulp stands for "unit in the last place" and describes the minimum + /// amount a given value can change. For any integers, an ulp is 1 whole + /// digit. For floating point values, the accuracy of which is better + /// for smaller numbers and worse for larger numbers, an ulp depends + /// on the size of the number. Using ulps for comparison of floating + /// point results instead of fixed tolerances is safer because it will + /// automatically compensate for the added inaccuracy of larger numbers. + /// + public EqualNumericWithoutUsingConstraint Ulps + { + get + { + _tolerance = _tolerance.Ulps; + return this; + } + } + + /// + /// Switches the .Within() modifier to interpret its tolerance as + /// a percentage that the actual values is allowed to deviate from + /// the expected value. + /// + /// Self + public EqualNumericWithoutUsingConstraint Percent + { + get + { + _tolerance = _tolerance.Percent; + return this; + } + } + + #endregion + + #region Public Methods + + /// + /// Test whether the constraint is satisfied by a given value + /// + /// The value to be tested + /// True for success, false for failure + public ConstraintResult ApplyTo(T actual) + { + bool hasSucceeded = Numerics.AreEqual(_expected, actual, ref _tolerance); + + return ConstraintResult(actual, hasSucceeded); + } + + /// + /// Test whether the constraint is satisfied by a given value + /// + /// The value to be tested + /// True for success, false for failure + public override ConstraintResult ApplyTo(TActual actual) + { + bool hasSucceeded; + + if (actual is null) + { + hasSucceeded = false; + } + else if (actual is T t) + { + hasSucceeded = Numerics.AreEqual(_expected, t, ref _tolerance); + } + else if (actual is IEquatable equatable) + { + hasSucceeded = equatable.Equals(_expected); + } + else if (actual is not string and IConvertible) + { + hasSucceeded = Numerics.AreEqual(actual, _expected, ref _tolerance); + } + else + { + hasSucceeded = false; + } + + return ConstraintResult(actual, hasSucceeded); + } + + private ConstraintResult ConstraintResult(TActual actual, bool hasSucceeded) + { + return new EqualConstraintResult(this, actual, _tolerance, hasSucceeded); + } + + /// + /// The Description of what this constraint tests, for + /// use in messages and in the ConstraintResult. + /// + public override string Description + { + get + { + var sb = new StringBuilder(MsgUtils.FormatValue(_expected)); + + if (_tolerance is not null && _tolerance.HasVariance) + { + sb.Append(" +/- "); + sb.Append(MsgUtils.FormatValue(_tolerance.Amount)); + if (_tolerance.Mode != ToleranceMode.Linear) + { + sb.Append(" "); + sb.Append(_tolerance.Mode.ToString()); + } + } + + return sb.ToString(); + } + } + + #endregion + } +} diff --git a/src/NUnitFramework/framework/Constraints/EqualNumericConstraintExtensions.cs b/src/NUnitFramework/framework/Constraints/EqualNumericWithoutUsingConstraintExtensions.cs similarity index 71% rename from src/NUnitFramework/framework/Constraints/EqualNumericConstraintExtensions.cs rename to src/NUnitFramework/framework/Constraints/EqualNumericWithoutUsingConstraintExtensions.cs index 362587b41a..e15878d9d2 100644 --- a/src/NUnitFramework/framework/Constraints/EqualNumericConstraintExtensions.cs +++ b/src/NUnitFramework/framework/Constraints/EqualNumericWithoutUsingConstraintExtensions.cs @@ -5,9 +5,9 @@ namespace NUnit.Framework { /// - /// Extension methods for . + /// Extension methods for . /// - public static class EqualNumericConstraintExtensions + public static class EqualNumericWithoutUsingConstraintExtensions { /// /// Flag the constraint to use a tolerance when determining equality. @@ -15,7 +15,7 @@ public static class EqualNumericConstraintExtensions /// The original constraint. /// Tolerance value to be used /// Original constraint promoted to . - public static EqualNumericConstraint Within(this EqualNumericConstraint constraint, double amount) + public static EqualNumericWithoutUsingConstraint Within(this EqualNumericWithoutUsingConstraint constraint, double amount) { return new EqualNumericConstraint(constraint.Expected).Within(amount); } @@ -26,7 +26,7 @@ public static EqualNumericConstraint Within(this EqualNumericConstraint< /// The original constraint. /// Tolerance value to be used /// Original constraint promoted to . - public static EqualNumericConstraint Within(this EqualNumericConstraint constraint, double amount) + public static EqualNumericWithoutUsingConstraint Within(this EqualNumericWithoutUsingConstraint constraint, double amount) { return new EqualNumericConstraint(constraint.Expected).Within(amount); } diff --git a/src/NUnitFramework/framework/Constraints/EqualStringConstraint.cs b/src/NUnitFramework/framework/Constraints/EqualStringConstraint.cs index 3f11ab76d7..2bcbea59c6 100644 --- a/src/NUnitFramework/framework/Constraints/EqualStringConstraint.cs +++ b/src/NUnitFramework/framework/Constraints/EqualStringConstraint.cs @@ -1,11 +1,5 @@ // Copyright (c) Charlie Poole, Rob Prouse and Contributors. MIT License - see LICENSE.txt -using System; -using System.Collections; -using System.Collections.Generic; -using System.Text; -using NUnit.Framework.Constraints.Comparers; - namespace NUnit.Framework.Constraints { /// @@ -14,21 +8,8 @@ namespace NUnit.Framework.Constraints /// considered equal if both are null, or if both have the same /// value. NUnit has special semantics for some object types. /// - public class EqualStringConstraint : Constraint + public class EqualStringConstraint : EqualStringWithoutUsingConstraint, IEqualWithUsingConstraint { - #region Static and Instance Fields - - private readonly string? _expected; - - private Func _comparer; - private Func? _nonTypedComparer; - - private bool _caseInsensitive; - private bool _ignoringWhiteSpace; - private bool _clipStrings; - - #endregion - #region Constructor /// @@ -38,219 +19,6 @@ public class EqualStringConstraint : Constraint public EqualStringConstraint(string? expected) : base(expected) { - _expected = expected; - _clipStrings = true; - - _comparer = (x, y) => StringsComparer.Equals(x, y, _caseInsensitive, _ignoringWhiteSpace); - } - - #endregion - - /// - /// Gets the expected value. - /// - public string? Expected => _expected; - - #region Constraint Modifiers - - /// - /// Flag the constraint to ignore case and return self. - /// - public EqualStringConstraint IgnoreCase - { - get - { - _caseInsensitive = true; - return this; - } - } - - /// - /// Flag the constraint to ignore white space and return self. - /// - public EqualStringConstraint IgnoreWhiteSpace - { - get - { - _ignoringWhiteSpace = true; - return this; - } - } - - /// - /// Flag the constraint to suppress string clipping - /// and return self. - /// - public EqualStringConstraint NoClip - { - get - { - _clipStrings = false; - return this; - } - } - - /// - /// Flag the constraint to use the supplied IEqualityComparer object. - /// - /// The IComparer object to use. - /// Self. - public EqualStringConstraint Using(IEqualityComparer comparer) - { - _comparer = (x, y) => comparer.Equals(x, y); - return this; - } - - /// - /// Flag the constraint to use the supplied IEqualityComparer object. - /// - /// The IComparer object to use. - /// Self. - public EqualStringConstraint Using(Func comparer) - { - _comparer = comparer; - return this; - } - - /// - /// Flag the constraint to use the supplied IComparer object. - /// - /// The IComparer object to use. - /// Self. - public EqualStringConstraint Using(IComparer comparer) - { - _comparer = (x, y) => comparer.Compare(x, y) == 0; - return this; - } - - /// - /// Flag the constraint to use the supplied Comparison object. - /// - /// The IComparer object to use. - /// Self. - public EqualStringConstraint Using(Comparison comparer) - { - _comparer = (x, y) => comparer.Invoke(x, y) == 0; - return this; - } - - /// - /// Flag the constraint to use the supplied IEqualityComparer object. - /// - /// The IComparer object to use. - /// Self. - public EqualStringConstraint Using(IEqualityComparer comparer) - { - _nonTypedComparer = (x, y) => comparer.Equals(x, y); - return this; - } - - /// - /// Flag the constraint to use the supplied IEqualityComparer object. - /// - /// The IComparer object to use. - /// Self. - public EqualStringConstraint Using(IComparer comparer) - { - _nonTypedComparer = (x, y) => comparer.Compare(x, y) == 0; - return this; - } - - /// - /// Flag the constraint to use the supplied IComparer object. - /// - /// The IComparer object to use. - /// Self. - public EqualStringConstraint Using(IComparer comparer) - { - _nonTypedComparer = (x, y) => comparer.Compare((TOther)x, (TOther)y) == 0; - return this; - } - - #endregion - - #region Public Methods - - /// - /// Test whether the constraint is satisfied by a given value - /// - /// The value to be tested - /// True for success, false for failure - public ConstraintResult ApplyTo(string? actual) - { - bool hasSucceeded; - - if (actual is null) - { - hasSucceeded = _expected is null; - } - else if (_expected is null) - { - hasSucceeded = false; - } - else if (_nonTypedComparer is not null) - { - hasSucceeded = _nonTypedComparer.Invoke(_expected, actual); - } - else - { - hasSucceeded = _comparer.Invoke(_expected, actual); - } - - return ConstraintResult(actual, hasSucceeded); - } - - /// - /// - /// I wish we could hide this method, but it is public in the base class. - /// - public sealed override ConstraintResult ApplyTo(TActual actual) - { - bool hasSucceeded; - - if (actual is null) - { - hasSucceeded = _expected is null; - } - else if (_expected is null) - { - hasSucceeded = false; - } - else if (_nonTypedComparer is not null) - { - hasSucceeded = _nonTypedComparer.Invoke(_expected, actual); - } - else - { - return ApplyTo(actual as string); - } - - return ConstraintResult(actual, hasSucceeded); - } - - private ConstraintResult ConstraintResult(T actual, bool hasSucceeded) - { - return new EqualConstraintResult(this, actual, _caseInsensitive, _ignoringWhiteSpace, _clipStrings, hasSucceeded); - } - - /// - /// The Description of what this constraint tests, for - /// use in messages and in the ConstraintResult. - /// - public override string Description - { - get - { - var sb = new StringBuilder(MsgUtils.FormatValue(_expected)); - - if (_caseInsensitive) - sb.Append(", ignoring case"); - - if (_ignoringWhiteSpace) - sb.Append(", ignoring white-space"); - - return sb.ToString(); - } } #endregion diff --git a/src/NUnitFramework/framework/Constraints/EqualStringWithoutUsingConstraint.cs b/src/NUnitFramework/framework/Constraints/EqualStringWithoutUsingConstraint.cs new file mode 100644 index 0000000000..210d02add8 --- /dev/null +++ b/src/NUnitFramework/framework/Constraints/EqualStringWithoutUsingConstraint.cs @@ -0,0 +1,165 @@ +// Copyright (c) Charlie Poole, Rob Prouse and Contributors. MIT License - see LICENSE.txt + +using System.Text; +using NUnit.Framework.Constraints.Comparers; + +namespace NUnit.Framework.Constraints +{ + /// + /// EqualConstraint is able to compare an actual value with the + /// expected value provided in its constructor. Two objects are + /// considered equal if both are null, or if both have the same + /// value. NUnit has special semantics for some object types. + /// + public class EqualStringWithoutUsingConstraint : Constraint + { + #region Static and Instance Fields + + private readonly string? _expected; + + private bool _caseInsensitive; + private bool _ignoringWhiteSpace; + private bool _clipStrings; + + #endregion + + #region Constructor + + /// + /// Initializes a new instance of the class. + /// + /// The expected value. + public EqualStringWithoutUsingConstraint(string? expected) + : base(expected) + { + _expected = expected; + _clipStrings = true; + } + + #endregion + + /// + /// Gets the expected value. + /// + public string? Expected => _expected; + + #region Constraint Modifiers + + /// + /// Flag the constraint to ignore case and return self. + /// + public EqualStringWithoutUsingConstraint IgnoreCase + { + get + { + _caseInsensitive = true; + return this; + } + } + + /// + /// Flag the constraint to ignore white space and return self. + /// + public EqualStringWithoutUsingConstraint IgnoreWhiteSpace + { + get + { + _ignoringWhiteSpace = true; + return this; + } + } + + /// + /// Flag the constraint to suppress string clipping + /// and return self. + /// + public EqualStringWithoutUsingConstraint NoClip + { + get + { + _clipStrings = false; + return this; + } + } + + #endregion + + #region Public Methods + + /// + /// Test whether the constraint is satisfied by a given value + /// + /// The value to be tested + /// True for success, false for failure + public ConstraintResult ApplyTo(string? actual) + { + bool hasSucceeded; + + if (actual is null) + { + hasSucceeded = _expected is null; + } + else if (_expected is null) + { + hasSucceeded = false; + } + else + { + hasSucceeded = StringsComparer.Equals(_expected, actual, _caseInsensitive, _ignoringWhiteSpace); + } + + return ConstraintResult(actual, hasSucceeded); + } + + /// + /// + /// I wish we could hide this method, but it is public in the base class. + /// + public sealed override ConstraintResult ApplyTo(TActual actual) + { + bool hasSucceeded; + + if (actual is null) + { + hasSucceeded = _expected is null; + } + else if (_expected is null) + { + hasSucceeded = false; + } + else + { + return ApplyTo(actual as string); + } + + return ConstraintResult(actual, hasSucceeded); + } + + private ConstraintResult ConstraintResult(T actual, bool hasSucceeded) + { + return new EqualConstraintResult(this, actual, _caseInsensitive, _ignoringWhiteSpace, _clipStrings, hasSucceeded); + } + + /// + /// The Description of what this constraint tests, for + /// use in messages and in the ConstraintResult. + /// + public override string Description + { + get + { + var sb = new StringBuilder(MsgUtils.FormatValue(_expected)); + + if (_caseInsensitive) + sb.Append(", ignoring case"); + + if (_ignoringWhiteSpace) + sb.Append(", ignoring white-space"); + + return sb.ToString(); + } + } + + #endregion + } +} diff --git a/src/NUnitFramework/framework/Constraints/EqualUsingConstraint.cs b/src/NUnitFramework/framework/Constraints/EqualUsingConstraint.cs new file mode 100644 index 0000000000..dc7eac86eb --- /dev/null +++ b/src/NUnitFramework/framework/Constraints/EqualUsingConstraint.cs @@ -0,0 +1,196 @@ +// Copyright (c) Charlie Poole, Rob Prouse and Contributors. MIT License - see LICENSE.txt + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Text; + +namespace NUnit.Framework.Constraints +{ + /// + /// EqualUsingConstraint where the comparison is done by a user supplied comparer. + /// + public class EqualUsingConstraint : Constraint + { + #region Static and Instance Fields + + private readonly T? _expected; + + private readonly Func? _comparer; + private readonly Func? _nonTypedComparer; + + #endregion + + #region Constructor + + /// + /// Initializes a new instance of the class. + /// + /// The expected value. + /// The comparer to use. + public EqualUsingConstraint(T? expected, Func comparer) + : base(expected) + { + _expected = expected; + _comparer = comparer; + } + + /// + /// Initializes a new instance of the class. + /// + /// The expected value. + /// The comparer to use. + public EqualUsingConstraint(T? expected, Func comparer) + : base(expected) + { + _expected = expected; + _nonTypedComparer = comparer; + } + + /// + /// Initializes a new instance of the class. + /// + /// The expected value. + /// The comparer to use. + public EqualUsingConstraint(T? expected, IEqualityComparer comparer) + : this(expected, comparer.Equals) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The expected value. + /// The comparer to use. + public EqualUsingConstraint(T? expected, IComparer comparer) + : this(expected, (x, y) => comparer.Compare(x, y) == 0) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The expected value. + /// The comparer to use. + public EqualUsingConstraint(T? expected, Comparison comparer) + : this(expected, (x, y) => comparer.Invoke(x, y) == 0) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The expected value. + /// The comparer to use. + public EqualUsingConstraint(T? expected, IEqualityComparer comparer) + : this(expected, (object x, object y) => comparer.Equals(x, y)) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The expected value. + /// The comparer to use. + public EqualUsingConstraint(T? expected, IComparer comparer) + : this(expected, (object x, object y) => comparer.Compare(x, y) == 0) + { + } + + #endregion + + #region Public Methods + + /// + /// Test whether the constraint is satisfied by a given value + /// + /// The value to be tested + /// True for success, false for failure + public virtual ConstraintResult ApplyTo(T? actual) + { + bool hasSucceeded; + + if (actual is null) + { + hasSucceeded = _expected is null; + } + else if (_expected is null) + { + hasSucceeded = false; + } + else if (_comparer is not null) + { + hasSucceeded = _comparer.Invoke(actual, _expected); + } + else if (_nonTypedComparer is not null) + { + hasSucceeded = _nonTypedComparer.Invoke(actual, _expected); + } + else + { + hasSucceeded = false; + } + + return ConstraintResult(actual, hasSucceeded); + } + + /// + /// + /// I wish we could hide this method, but it is public in the base class. + /// + public sealed override ConstraintResult ApplyTo(TActual actual) + { + bool hasSucceeded; + + if (actual is null) + { + hasSucceeded = _expected is null; + } + else if (_expected is null) + { + hasSucceeded = false; + } + else if (_nonTypedComparer is not null) + { + hasSucceeded = _nonTypedComparer.Invoke(actual, _expected); + } + else if (_comparer is not null && actual is T t) + { + hasSucceeded = _comparer.Invoke(t, _expected); + } + else + { + hasSucceeded = false; + } + + return ConstraintResult(actual, hasSucceeded); + } + + private ConstraintResult ConstraintResult(TActual actual, bool hasSucceeded) + { + return new EqualConstraintResult(this, actual, hasSucceeded); + } + + /// + /// The Description of what this constraint tests, for + /// use in messages and in the ConstraintResult. + /// + public override string Description + { + get + { + var sb = new StringBuilder(MsgUtils.FormatValue(_expected)); + + if (_comparer is not null) + sb.Append(", using strongly typed comparer"); + + if (_nonTypedComparer is not null) + sb.Append(", using untyped comparer"); + + return sb.ToString(); + } + } + + #endregion + } +} diff --git a/src/NUnitFramework/framework/Constraints/IEqualWithUsingConstraint.cs b/src/NUnitFramework/framework/Constraints/IEqualWithUsingConstraint.cs new file mode 100644 index 0000000000..2807ce1c5d --- /dev/null +++ b/src/NUnitFramework/framework/Constraints/IEqualWithUsingConstraint.cs @@ -0,0 +1,15 @@ +// Copyright (c) Charlie Poole, Rob Prouse and Contributors. MIT License - see LICENSE.txt + +namespace NUnit.Framework +{ + /// + /// Interface for equal constraints which support user comparisons. + /// + public interface IEqualWithUsingConstraint + { + /// + /// The expected value. + /// + public T Expected { get; } + } +} diff --git a/src/NUnitFramework/framework/Constraints/IEqualWithUsingConstraintExtensions.cs b/src/NUnitFramework/framework/Constraints/IEqualWithUsingConstraintExtensions.cs new file mode 100644 index 0000000000..a7a8222cbd --- /dev/null +++ b/src/NUnitFramework/framework/Constraints/IEqualWithUsingConstraintExtensions.cs @@ -0,0 +1,151 @@ +// Copyright (c) Charlie Poole, Rob Prouse and Contributors. MIT License - see LICENSE.txt + +using System; +using System.Collections; +using System.Collections.Generic; +using NUnit.Framework.Constraints; + +namespace NUnit.Framework +{ + /// + /// Allows specifying a custom comparer for the . + /// + public static class IEqualWithUsingConstraintExtensions + { + #region TExpected Typed Comparers + + /// + /// Flag the constraint to use the supplied boolean-returning delegate. + /// + /// The constraint to add a user comparer to. + /// The boolean-returning delegate to use. + /// The type of the expected value. + /// + /// Equal constraint comparing + /// with an actual value using the user supplied comparer. + /// + public static EqualUsingConstraint Using(this IEqualWithUsingConstraint constraint, Func comparer) + { + return new EqualUsingConstraint(constraint.Expected, comparer); + } + + /// + /// Flag the constraint to use the supplied IEqualityComparer object. + /// + /// The constraint to add a user comparer to. + /// The comparer to use. + /// The type of the expected value. + /// + /// Equal constraint comparing + /// with an actual value using the user supplied comparer. + /// + public static EqualUsingConstraint Using(this IEqualWithUsingConstraint constraint, IEqualityComparer comparer) + { + return new EqualUsingConstraint(constraint.Expected, comparer); + } + + /// + /// Flag the constraint to use the supplied IComparer object. + /// + /// The constraint to add a user comparer to. + /// The comparer to use. + /// The type of the expected value. + /// + /// Equal constraint comparing + /// with an actual value using the user supplied comparer. + /// + public static EqualUsingConstraint Using(this IEqualWithUsingConstraint constraint, IComparer comparer) + { + return new EqualUsingConstraint(constraint.Expected, comparer); + } + + /// + /// Flag the constraint to use the supplied Comparison object. + /// + /// The constraint to add a user comparer to. + /// The comparer to use. + /// The type of the expected value. + /// + /// Equal constraint comparing + /// with an actual value using the user supplied comparer. + /// + public static EqualUsingConstraint Using(this IEqualWithUsingConstraint constraint, Comparison comparer) + { + return new EqualUsingConstraint(constraint.Expected, comparer); + } + + #endregion + + #region TExpected vs TActual Typed Comparers + + /// + /// Flag the constraint to use the supplied predicate function + /// + /// The constraint to add a user comparer to. + /// The comparison function to use. + /// The type of the actual value. + /// The type of the expected value. + /// + /// Equal constraint comparing + /// with an actual value using the user supplied comparer. + /// + public static EqualUsingConstraint Using(this IEqualWithUsingConstraint constraint, Func comparer) + { + return new EqualUsingConstraint(constraint.Expected, + (object x, object y) => comparer.Invoke((TActual)x, (TExpected)y)); + } + + /// + /// Flag the constraint to use the supplied IComparer object. + /// + /// The constraint to add a user comparer to. + /// The comparer to use. + /// The type of the expected value. + /// The type of the actual value. + /// + /// Equal constraint comparing + /// with an actual value using the user supplied comparer. + /// + public static EqualUsingConstraint Using(this IEqualWithUsingConstraint constraint, IComparer comparer) + { + return new EqualUsingConstraint(constraint.Expected, + (object x, object y) => comparer.Compare((TActual)x, (TActual)y) == 0); + } + + #endregion + + #region Non-Generic Comparers + + /// + /// Flag the constraint to use the supplied IEqualityComparer object. + /// + /// The constraint to add a user comparer to. + /// The comparer object to use. + /// The type of the expected value. + /// + /// Equal constraint comparing + /// with an actual value using the user supplied comparer. + /// + public static EqualUsingConstraint Using(this IEqualWithUsingConstraint constraint, IEqualityComparer comparer) + { + return new EqualUsingConstraint(constraint.Expected, comparer); + } + + /// + /// Flag the constraint to use the supplied IComparer object. + /// + /// The constraint to add a user comparer to. + /// The comparer to use. + /// The type of the expected value. + /// + /// Equal constraint comparing + /// with an actual value using the user supplied comparer. + /// + public static EqualUsingConstraint Using(this IEqualWithUsingConstraint constraint, IComparer comparer) + { + return new EqualUsingConstraint(constraint.Expected, comparer); + } + + #endregion + } +} diff --git a/src/NUnitFramework/tests/Constraints/EqualConstraintTests.cs b/src/NUnitFramework/tests/Constraints/EqualConstraintTests.cs index f6f7f80432..07f46e7395 100644 --- a/src/NUnitFramework/tests/Constraints/EqualConstraintTests.cs +++ b/src/NUnitFramework/tests/Constraints/EqualConstraintTests.cs @@ -917,7 +917,7 @@ public void UsesProvidedGenericComparer() var comparer = new GenericComparer(); Assert.Multiple(() => { - Assert.That(2 + 2, Is.EqualTo(4).Using(comparer)); + Assert.That(2 + 2, Is.EqualTo(4).Using(comparer)); Assert.That(comparer.WasCalled, "Comparer was not called"); }); } @@ -939,7 +939,7 @@ public void UsesProvidedGenericEqualityComparison() var comparer = new GenericEqualityComparison(); Assert.Multiple(() => { - Assert.That(2 + 2, Is.EqualTo(4).Using(comparer.Delegate)); + Assert.That(2 + 2, Is.EqualTo(4).Using(comparer.Delegate)); Assert.That(comparer.WasCalled, "Comparer was not called"); }); } From 879c48994c8e6bfe53bd1bf50139c47bd630fa80 Mon Sep 17 00:00:00 2001 From: Manfred Brands Date: Sun, 10 Nov 2024 17:07:16 +0800 Subject: [PATCH 5/5] Added StringComparer overload to EqualStringConstraint. This to prevent the ambiguity that the Comparer matches 4 different overloads. --- .../Constraints/EqualStringConstraint.cs | 20 +++++++++++++++++++ .../tests/Constraints/EqualConstraintTests.cs | 6 ++++++ 2 files changed, 26 insertions(+) diff --git a/src/NUnitFramework/framework/Constraints/EqualStringConstraint.cs b/src/NUnitFramework/framework/Constraints/EqualStringConstraint.cs index 2bcbea59c6..9cb2b33542 100644 --- a/src/NUnitFramework/framework/Constraints/EqualStringConstraint.cs +++ b/src/NUnitFramework/framework/Constraints/EqualStringConstraint.cs @@ -1,5 +1,8 @@ // Copyright (c) Charlie Poole, Rob Prouse and Contributors. MIT License - see LICENSE.txt +using System; +using System.Collections.Generic; + namespace NUnit.Framework.Constraints { /// @@ -22,5 +25,22 @@ public EqualStringConstraint(string? expected) } #endregion + + #region Public Methods + + /// + /// Sets the to be used in the comparison. + /// + /// comparer to use for comparing strings. + /// + /// Equal constraint comparing + /// with an actual value using the user supplied comparer. + /// + public EqualUsingConstraint Using(StringComparer comparer) + { + return new EqualUsingConstraint(Expected, (IEqualityComparer)comparer); + } + + #endregion } } diff --git a/src/NUnitFramework/tests/Constraints/EqualConstraintTests.cs b/src/NUnitFramework/tests/Constraints/EqualConstraintTests.cs index 07f46e7395..5e1e951304 100644 --- a/src/NUnitFramework/tests/Constraints/EqualConstraintTests.cs +++ b/src/NUnitFramework/tests/Constraints/EqualConstraintTests.cs @@ -962,6 +962,12 @@ public void UsesProvidedLambda_StringArgs() Assert.That("hello", Is.EqualTo("HELLO").Using((x, y) => string.Compare(x, y, StringComparison.CurrentCultureIgnoreCase))); } + [Test, SetCulture("en-US")] + public void UsesStringComparer() + { + Assert.That("hello", Is.EqualTo("HELLO").Using(StringComparer.OrdinalIgnoreCase)); + } + [Test] public void UsesProvidedListComparer() {