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 536d89fe5a..4f09363560 100644
--- a/src/NUnitFramework/framework/Constraints/ConstraintExpression.cs
+++ b/src/NUnitFramework/framework/Constraints/ConstraintExpression.cs
@@ -427,6 +427,49 @@ 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));
+ }
+
+ ///
+ /// 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));
+ }
+
+ ///
+ /// 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 e6d6dd41d8..c946d82028 100644
--- a/src/NUnitFramework/framework/Constraints/EqualConstraintResult.cs
+++ b/src/NUnitFramework/framework/Constraints/EqualConstraintResult.cs
@@ -57,6 +57,51 @@ 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
+ ///
+ 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];
+ _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/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/EqualNumericConstraint.cs b/src/NUnitFramework/framework/Constraints/EqualNumericConstraint.cs
new file mode 100644
index 0000000000..c8584dff58
--- /dev/null
+++ b/src/NUnitFramework/framework/Constraints/EqualNumericConstraint.cs
@@ -0,0 +1,31 @@
+// Copyright (c) Charlie Poole, Rob Prouse and Contributors. MIT License - see LICENSE.txt
+
+using System;
+
+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 : EqualNumericWithoutUsingConstraint, IEqualWithUsingConstraint
+ where T : unmanaged, IConvertible, IEquatable
+#pragma warning restore CS3024 // Constraint type is not CLS-compliant
+ {
+ #region Constructor
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The expected value.
+ public EqualNumericConstraint(T expected)
+ : base(expected)
+ {
+ }
+
+ #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/EqualNumericWithoutUsingConstraintExtensions.cs b/src/NUnitFramework/framework/Constraints/EqualNumericWithoutUsingConstraintExtensions.cs
new file mode 100644
index 0000000000..e15878d9d2
--- /dev/null
+++ b/src/NUnitFramework/framework/Constraints/EqualNumericWithoutUsingConstraintExtensions.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 EqualNumericWithoutUsingConstraintExtensions
+ {
+ ///
+ /// Flag the constraint to use a tolerance when determining equality.
+ ///
+ /// The original constraint.
+ /// Tolerance value to be used
+ /// Original constraint promoted to .
+ public static EqualNumericWithoutUsingConstraint Within(this EqualNumericWithoutUsingConstraint 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 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
new file mode 100644
index 0000000000..9cb2b33542
--- /dev/null
+++ b/src/NUnitFramework/framework/Constraints/EqualStringConstraint.cs
@@ -0,0 +1,46 @@
+// Copyright (c) Charlie Poole, Rob Prouse and Contributors. MIT License - see LICENSE.txt
+
+using System;
+using System.Collections.Generic;
+
+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 : EqualStringWithoutUsingConstraint, IEqualWithUsingConstraint
+ {
+ #region Constructor
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The expected value.
+ public EqualStringConstraint(string? expected)
+ : base(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/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/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/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