diff --git a/nuget/framework/nunit.nuspec b/nuget/framework/nunit.nuspec index ef8aae14bf..eabb7639a3 100644 --- a/nuget/framework/nunit.nuspec +++ b/nuget/framework/nunit.nuspec @@ -33,6 +33,7 @@ + diff --git a/src/NUnitFramework/framework/Constraints/AnyOfConstraint.cs b/src/NUnitFramework/framework/Constraints/AnyOfConstraint.cs index bed68c18fb..a870da6466 100644 --- a/src/NUnitFramework/framework/Constraints/AnyOfConstraint.cs +++ b/src/NUnitFramework/framework/Constraints/AnyOfConstraint.cs @@ -70,6 +70,18 @@ public AnyOfConstraint IgnoreCase } } + /// + /// Flag the constraint to ignore white space and return self. + /// + public AnyOfConstraint IgnoreWhiteSpace + { + get + { + _comparer.IgnoreWhiteSpace = true; + return this; + } + } + /// /// Flag the constraint to use the supplied IComparer object. /// diff --git a/src/NUnitFramework/framework/Constraints/CollectionItemsEqualConstraint.cs b/src/NUnitFramework/framework/Constraints/CollectionItemsEqualConstraint.cs index e28afb29be..2c5987289a 100644 --- a/src/NUnitFramework/framework/Constraints/CollectionItemsEqualConstraint.cs +++ b/src/NUnitFramework/framework/Constraints/CollectionItemsEqualConstraint.cs @@ -40,6 +40,11 @@ protected CollectionItemsEqualConstraint(object? arg) : base(arg) /// protected bool IgnoringCase => _comparer.IgnoreCase; + /// + /// Get a flag indicating whether the user requested us to ignore white space. + /// + protected bool IgnoringWhiteSpace => _comparer.IgnoreWhiteSpace; + /// /// Get a flag indicating whether any external comparers are in use. /// @@ -61,6 +66,18 @@ public CollectionItemsEqualConstraint IgnoreCase } } + /// + /// Flag the constraint to ignore white space and return self. + /// + public CollectionItemsEqualConstraint IgnoreWhiteSpace + { + get + { + _comparer.IgnoreWhiteSpace = true; + return this; + } + } + /// /// Flag the constraint to use the supplied IComparer object. /// diff --git a/src/NUnitFramework/framework/Constraints/Comparers/StringsComparer.cs b/src/NUnitFramework/framework/Constraints/Comparers/StringsComparer.cs index cdd517b298..482db57e27 100644 --- a/src/NUnitFramework/framework/Constraints/Comparers/StringsComparer.cs +++ b/src/NUnitFramework/framework/Constraints/Comparers/StringsComparer.cs @@ -17,9 +17,22 @@ public static EqualMethodResult Equal(object x, object y, ref Tolerance toleranc if (tolerance.HasVariance) return EqualMethodResult.ToleranceNotSupported; - var stringComparison = equalityComparer.IgnoreCase ? StringComparison.CurrentCultureIgnoreCase : StringComparison.Ordinal; - return xString.Equals(yString, stringComparison) ? - EqualMethodResult.ComparedEqual : EqualMethodResult.ComparedNotEqual; + return Equals(xString, yString, equalityComparer.IgnoreCase, equalityComparer.IgnoreWhiteSpace) ? + EqualMethodResult.ComparedEqual : + EqualMethodResult.ComparedNotEqual; + } + + public static bool Equals(string x, string y, bool ignoreCase, bool ignoreWhiteSpace) + { + if (ignoreWhiteSpace) + { + (int mismatchExpected, int mismatchActual) = MsgUtils.FindMismatchPosition(x, y, ignoreCase, true); + return mismatchExpected == -1 && mismatchActual == -1; + } + else + { + return x.Equals(y, ignoreCase ? StringComparison.CurrentCultureIgnoreCase : StringComparison.Ordinal); + } } } } diff --git a/src/NUnitFramework/framework/Constraints/ConstraintExpression.cs b/src/NUnitFramework/framework/Constraints/ConstraintExpression.cs index 0608027142..634693e944 100644 --- a/src/NUnitFramework/framework/Constraints/ConstraintExpression.cs +++ b/src/NUnitFramework/framework/Constraints/ConstraintExpression.cs @@ -104,6 +104,23 @@ public Constraint Append(Constraint constraint) return constraint; } + /// + /// Appends a constraint to the expression and returns that + /// constraint, which is associated with the current state + /// of the expression being built. Note that the constraint + /// is not reduced at this time. For example, if there + /// is a NotOperator on the stack we don't reduce and + /// return a NotConstraint. The original constraint must + /// be returned because it may support modifiers that + /// are yet to be applied. + /// + public T Append(T constraint) + where T : Constraint + { + builder.Append(constraint); + return constraint; + } + #endregion #region Not @@ -300,7 +317,7 @@ public Constraint Matches(Predicate predicate) /// /// Returns a constraint that tests for null /// - public NullConstraint Null => (NullConstraint)Append(new NullConstraint()); + public NullConstraint Null => Append(new NullConstraint()); #endregion @@ -309,7 +326,7 @@ public Constraint Matches(Predicate predicate) /// /// Returns a constraint that tests for default value /// - public DefaultConstraint Default => (DefaultConstraint)Append(new DefaultConstraint()); + public DefaultConstraint Default => Append(new DefaultConstraint()); #endregion @@ -318,7 +335,7 @@ public Constraint Matches(Predicate predicate) /// /// Returns a constraint that tests for True /// - public TrueConstraint True => (TrueConstraint)Append(new TrueConstraint()); + public TrueConstraint True => Append(new TrueConstraint()); #endregion @@ -327,7 +344,7 @@ public Constraint Matches(Predicate predicate) /// /// Returns a constraint that tests for False /// - public FalseConstraint False => (FalseConstraint)Append(new FalseConstraint()); + public FalseConstraint False => Append(new FalseConstraint()); #endregion @@ -336,7 +353,7 @@ public Constraint Matches(Predicate predicate) /// /// Returns a constraint that tests for a positive value /// - public GreaterThanConstraint Positive => (GreaterThanConstraint)Append(new GreaterThanConstraint(0)); + public GreaterThanConstraint Positive => Append(new GreaterThanConstraint(0)); #endregion @@ -345,7 +362,7 @@ public Constraint Matches(Predicate predicate) /// /// Returns a constraint that tests for a negative value /// - public LessThanConstraint Negative => (LessThanConstraint)Append(new LessThanConstraint(0)); + public LessThanConstraint Negative => Append(new LessThanConstraint(0)); #endregion @@ -354,7 +371,7 @@ public Constraint Matches(Predicate predicate) /// /// Returns a constraint that tests if item is equal to zero /// - public EqualConstraint Zero => (EqualConstraint)Append(new EqualConstraint(0)); + public EqualConstraint Zero => Append(new EqualConstraint(0)); #endregion @@ -363,7 +380,7 @@ public Constraint Matches(Predicate predicate) /// /// Returns a constraint that tests for NaN /// - public NaNConstraint NaN => (NaNConstraint)Append(new NaNConstraint()); + public NaNConstraint NaN => Append(new NaNConstraint()); #endregion @@ -372,7 +389,16 @@ public Constraint Matches(Predicate predicate) /// /// Returns a constraint that tests for empty /// - public EmptyConstraint Empty => (EmptyConstraint)Append(new EmptyConstraint()); + public EmptyConstraint Empty => Append(new EmptyConstraint()); + + #endregion + + #region WhiteSpace + + /// + /// Returns a constraint that tests for white-space + /// + public WhiteSpaceConstraint WhiteSpace => Append(new WhiteSpaceConstraint()); #endregion @@ -382,14 +408,14 @@ public Constraint Matches(Predicate predicate) /// Returns a constraint that tests whether a collection /// contains all unique items. /// - public UniqueItemsConstraint Unique => (UniqueItemsConstraint)Append(new UniqueItemsConstraint()); + public UniqueItemsConstraint Unique => Append(new UniqueItemsConstraint()); #endregion /// /// Returns a constraint that tests whether an object graph is serializable in XML format. /// - public XmlSerializableConstraint XmlSerializable => (XmlSerializableConstraint)Append(new XmlSerializableConstraint()); + public XmlSerializableConstraint XmlSerializable => Append(new XmlSerializableConstraint()); #region EqualTo @@ -398,7 +424,7 @@ public Constraint Matches(Predicate predicate) /// public EqualConstraint EqualTo(object? expected) { - return (EqualConstraint)Append(new EqualConstraint(expected)); + return Append(new EqualConstraint(expected)); } #endregion @@ -410,7 +436,7 @@ public EqualConstraint EqualTo(object? expected) /// public SameAsConstraint SameAs(object? expected) { - return (SameAsConstraint)Append(new SameAsConstraint(expected)); + return Append(new SameAsConstraint(expected)); } #endregion @@ -423,7 +449,7 @@ public SameAsConstraint SameAs(object? expected) /// public GreaterThanConstraint GreaterThan(object expected) { - return (GreaterThanConstraint)Append(new GreaterThanConstraint(expected)); + return Append(new GreaterThanConstraint(expected)); } #endregion @@ -436,7 +462,7 @@ public GreaterThanConstraint GreaterThan(object expected) /// public GreaterThanOrEqualConstraint GreaterThanOrEqualTo(object expected) { - return (GreaterThanOrEqualConstraint)Append(new GreaterThanOrEqualConstraint(expected)); + return Append(new GreaterThanOrEqualConstraint(expected)); } /// @@ -445,7 +471,7 @@ public GreaterThanOrEqualConstraint GreaterThanOrEqualTo(object expected) /// public GreaterThanOrEqualConstraint AtLeast(object expected) { - return (GreaterThanOrEqualConstraint)Append(new GreaterThanOrEqualConstraint(expected)); + return Append(new GreaterThanOrEqualConstraint(expected)); } #endregion @@ -458,7 +484,7 @@ public GreaterThanOrEqualConstraint AtLeast(object expected) /// public LessThanConstraint LessThan(object expected) { - return (LessThanConstraint)Append(new LessThanConstraint(expected)); + return Append(new LessThanConstraint(expected)); } #endregion @@ -471,7 +497,7 @@ public LessThanConstraint LessThan(object expected) /// public LessThanOrEqualConstraint LessThanOrEqualTo(object expected) { - return (LessThanOrEqualConstraint)Append(new LessThanOrEqualConstraint(expected)); + return Append(new LessThanOrEqualConstraint(expected)); } /// @@ -480,7 +506,7 @@ public LessThanOrEqualConstraint LessThanOrEqualTo(object expected) /// public LessThanOrEqualConstraint AtMost(object expected) { - return (LessThanOrEqualConstraint)Append(new LessThanOrEqualConstraint(expected)); + return Append(new LessThanOrEqualConstraint(expected)); } #endregion @@ -493,7 +519,7 @@ public LessThanOrEqualConstraint AtMost(object expected) /// public ExactTypeConstraint TypeOf(Type expectedType) { - return (ExactTypeConstraint)Append(new ExactTypeConstraint(expectedType)); + return Append(new ExactTypeConstraint(expectedType)); } /// @@ -502,7 +528,7 @@ public ExactTypeConstraint TypeOf(Type expectedType) /// public ExactTypeConstraint TypeOf() { - return (ExactTypeConstraint)Append(new ExactTypeConstraint(typeof(TExpected))); + return Append(new ExactTypeConstraint(typeof(TExpected))); } #endregion @@ -515,7 +541,7 @@ public ExactTypeConstraint TypeOf() /// public InstanceOfTypeConstraint InstanceOf(Type expectedType) { - return (InstanceOfTypeConstraint)Append(new InstanceOfTypeConstraint(expectedType)); + return Append(new InstanceOfTypeConstraint(expectedType)); } /// @@ -524,7 +550,7 @@ public InstanceOfTypeConstraint InstanceOf(Type expectedType) /// public InstanceOfTypeConstraint InstanceOf() { - return (InstanceOfTypeConstraint)Append(new InstanceOfTypeConstraint(typeof(TExpected))); + return Append(new InstanceOfTypeConstraint(typeof(TExpected))); } #endregion @@ -537,7 +563,7 @@ public InstanceOfTypeConstraint InstanceOf() /// public AssignableFromConstraint AssignableFrom(Type expectedType) { - return (AssignableFromConstraint)Append(new AssignableFromConstraint(expectedType)); + return Append(new AssignableFromConstraint(expectedType)); } /// @@ -546,7 +572,7 @@ public AssignableFromConstraint AssignableFrom(Type expectedType) /// public AssignableFromConstraint AssignableFrom() { - return (AssignableFromConstraint)Append(new AssignableFromConstraint(typeof(TExpected))); + return Append(new AssignableFromConstraint(typeof(TExpected))); } #endregion @@ -559,7 +585,7 @@ public AssignableFromConstraint AssignableFrom() /// public AssignableToConstraint AssignableTo(Type expectedType) { - return (AssignableToConstraint)Append(new AssignableToConstraint(expectedType)); + return Append(new AssignableToConstraint(expectedType)); } /// @@ -568,7 +594,7 @@ public AssignableToConstraint AssignableTo(Type expectedType) /// public AssignableToConstraint AssignableTo() { - return (AssignableToConstraint)Append(new AssignableToConstraint(typeof(TExpected))); + return Append(new AssignableToConstraint(typeof(TExpected))); } #endregion @@ -582,7 +608,7 @@ public AssignableToConstraint AssignableTo() /// public CollectionEquivalentConstraint EquivalentTo(IEnumerable expected) { - return (CollectionEquivalentConstraint)Append(new CollectionEquivalentConstraint(expected)); + return Append(new CollectionEquivalentConstraint(expected)); } #endregion @@ -595,7 +621,7 @@ public CollectionEquivalentConstraint EquivalentTo(IEnumerable expected) /// public CollectionSubsetConstraint SubsetOf(IEnumerable expected) { - return (CollectionSubsetConstraint)Append(new CollectionSubsetConstraint(expected)); + return Append(new CollectionSubsetConstraint(expected)); } #endregion @@ -608,7 +634,7 @@ public CollectionSubsetConstraint SubsetOf(IEnumerable expected) /// public CollectionSupersetConstraint SupersetOf(IEnumerable expected) { - return (CollectionSupersetConstraint)Append(new CollectionSupersetConstraint(expected)); + return Append(new CollectionSupersetConstraint(expected)); } #endregion @@ -618,7 +644,7 @@ public CollectionSupersetConstraint SupersetOf(IEnumerable expected) /// /// Returns a constraint that tests whether a collection is ordered /// - public CollectionOrderedConstraint Ordered => (CollectionOrderedConstraint)Append(new CollectionOrderedConstraint()); + public CollectionOrderedConstraint Ordered => Append(new CollectionOrderedConstraint()); #endregion @@ -630,7 +656,7 @@ public CollectionSupersetConstraint SupersetOf(IEnumerable expected) /// public SomeItemsConstraint Member(object? expected) { - return (SomeItemsConstraint)Append(new SomeItemsConstraint(new EqualConstraint(expected))); + return Append(new SomeItemsConstraint(new EqualConstraint(expected))); } #endregion @@ -649,7 +675,7 @@ public SomeItemsConstraint Member(object? expected) /// public SomeItemsConstraint Contains(object? expected) { - return (SomeItemsConstraint)Append(new SomeItemsConstraint(new EqualConstraint(expected))); + return Append(new SomeItemsConstraint(new EqualConstraint(expected))); } /// @@ -665,7 +691,7 @@ public SomeItemsConstraint Contains(object? expected) /// public ContainsConstraint Contains(string? expected) { - return (ContainsConstraint)Append(new ContainsConstraint(expected)); + return Append(new ContainsConstraint(expected)); } /// @@ -700,7 +726,7 @@ public ContainsConstraint Contain(string? expected) /// The key to be matched in the Dictionary key collection public DictionaryContainsKeyConstraint ContainKey(object expected) { - return (DictionaryContainsKeyConstraint)Append(new DictionaryContainsKeyConstraint(expected)); + return Append(new DictionaryContainsKeyConstraint(expected)); } /// @@ -710,7 +736,7 @@ public DictionaryContainsKeyConstraint ContainKey(object expected) /// The value to be matched in the Dictionary value collection public DictionaryContainsValueConstraint ContainValue(object expected) { - return (DictionaryContainsValueConstraint)Append(new DictionaryContainsValueConstraint(expected)); + return Append(new DictionaryContainsValueConstraint(expected)); } #endregion @@ -722,7 +748,7 @@ public DictionaryContainsValueConstraint ContainValue(object expected) /// public StartsWithConstraint StartWith(string expected) { - return (StartsWithConstraint)Append(new StartsWithConstraint(expected)); + return Append(new StartsWithConstraint(expected)); } /// @@ -731,7 +757,7 @@ public StartsWithConstraint StartWith(string expected) /// public StartsWithConstraint StartsWith(string expected) { - return (StartsWithConstraint)Append(new StartsWithConstraint(expected)); + return Append(new StartsWithConstraint(expected)); } #endregion @@ -744,7 +770,7 @@ public StartsWithConstraint StartsWith(string expected) /// public EndsWithConstraint EndWith(string expected) { - return (EndsWithConstraint)Append(new EndsWithConstraint(expected)); + return Append(new EndsWithConstraint(expected)); } /// @@ -753,7 +779,7 @@ public EndsWithConstraint EndWith(string expected) /// public EndsWithConstraint EndsWith(string expected) { - return (EndsWithConstraint)Append(new EndsWithConstraint(expected)); + return Append(new EndsWithConstraint(expected)); } #endregion @@ -766,7 +792,7 @@ public EndsWithConstraint EndsWith(string expected) /// public RegexConstraint Match([StringSyntax(StringSyntaxAttribute.Regex)] string pattern) { - return (RegexConstraint)Append(new RegexConstraint(pattern)); + return Append(new RegexConstraint(pattern)); } /// @@ -775,7 +801,7 @@ public RegexConstraint Match([StringSyntax(StringSyntaxAttribute.Regex)] string /// public RegexConstraint Match(Regex regex) { - return (RegexConstraint)Append(new RegexConstraint(regex)); + return Append(new RegexConstraint(regex)); } /// @@ -784,7 +810,7 @@ public RegexConstraint Match(Regex regex) /// public RegexConstraint Matches([StringSyntax(StringSyntaxAttribute.Regex)] string pattern) { - return (RegexConstraint)Append(new RegexConstraint(pattern)); + return Append(new RegexConstraint(pattern)); } /// @@ -793,7 +819,7 @@ public RegexConstraint Matches([StringSyntax(StringSyntaxAttribute.Regex)] strin /// public RegexConstraint Matches(Regex regex) { - return (RegexConstraint)Append(new RegexConstraint(regex)); + return Append(new RegexConstraint(regex)); } #endregion @@ -806,7 +832,7 @@ public RegexConstraint Matches(Regex regex) /// public SamePathConstraint SamePath(string expected) { - return (SamePathConstraint)Append(new SamePathConstraint(expected)); + return Append(new SamePathConstraint(expected)); } #endregion @@ -819,7 +845,7 @@ public SamePathConstraint SamePath(string expected) /// public SubPathConstraint SubPathOf(string expected) { - return (SubPathConstraint)Append(new SubPathConstraint(expected)); + return Append(new SubPathConstraint(expected)); } #endregion @@ -832,7 +858,7 @@ public SubPathConstraint SubPathOf(string expected) /// public SamePathOrUnderConstraint SamePathOrUnder(string expected) { - return (SamePathOrUnderConstraint)Append(new SamePathOrUnderConstraint(expected)); + return Append(new SamePathOrUnderConstraint(expected)); } #endregion @@ -846,7 +872,7 @@ public SamePathOrUnderConstraint SamePathOrUnder(string expected) /// Inclusive end of the range. public RangeConstraint InRange(object from, object to) { - return (RangeConstraint)Append(new RangeConstraint(from, to)); + return Append(new RangeConstraint(from, to)); } #endregion @@ -874,7 +900,7 @@ public AnyOfConstraint AnyOf(params object?[]? expected) expected = new object?[] { null }; } - return (AnyOfConstraint)Append(new AnyOfConstraint(expected)); + return Append(new AnyOfConstraint(expected)); } /// @@ -883,7 +909,7 @@ public AnyOfConstraint AnyOf(params object?[]? expected) /// Expected values public AnyOfConstraint AnyOf(ICollection expected) { - return (AnyOfConstraint)Append(new AnyOfConstraint(expected)); + return Append(new AnyOfConstraint(expected)); } #endregion diff --git a/src/NUnitFramework/framework/Constraints/ContainsConstraint.cs b/src/NUnitFramework/framework/Constraints/ContainsConstraint.cs index 592b546dbe..9e3f714fba 100644 --- a/src/NUnitFramework/framework/Constraints/ContainsConstraint.cs +++ b/src/NUnitFramework/framework/Constraints/ContainsConstraint.cs @@ -17,6 +17,7 @@ public class ContainsConstraint : Constraint private readonly object? _expected; private Constraint? _realConstraint; private bool _ignoreCase; + private bool _ignoreWhiteSpace; /// /// Initializes a new instance of the class. @@ -61,6 +62,18 @@ public ContainsConstraint IgnoreCase } } + /// + /// Flag the constraint to ignore white-space and return self. + /// + public ContainsConstraint IgnoreWhiteSpace + { + get + { + _ignoreWhiteSpace = true; + return this; + } + } + /// /// Test whether the constraint is satisfied by a given value /// @@ -78,6 +91,8 @@ public override ConstraintResult ApplyTo(TActual actual) StringConstraint constraint = new SubstringConstraint(substring); if (_ignoreCase) constraint = constraint.IgnoreCase; + if (_ignoreWhiteSpace) + throw new InvalidOperationException("IgnoreWhiteSpace not supported on SubStringConstraint"); _realConstraint = constraint; } else @@ -85,6 +100,8 @@ public override ConstraintResult ApplyTo(TActual actual) var itemConstraint = new EqualConstraint(_expected); if (_ignoreCase) itemConstraint = itemConstraint.IgnoreCase; + if (_ignoreWhiteSpace) + itemConstraint = itemConstraint.IgnoreWhiteSpace; _realConstraint = new SomeItemsConstraint(itemConstraint); } diff --git a/src/NUnitFramework/framework/Constraints/EmptyStringConstraint.cs b/src/NUnitFramework/framework/Constraints/EmptyStringConstraint.cs index fddadd6cad..656f876eba 100644 --- a/src/NUnitFramework/framework/Constraints/EmptyStringConstraint.cs +++ b/src/NUnitFramework/framework/Constraints/EmptyStringConstraint.cs @@ -18,7 +18,7 @@ public class EmptyStringConstraint : StringConstraint /// /// The value to be tested /// True for success, false for failure - protected override bool Matches(string actual) + protected override bool Matches(string? actual) { return actual == string.Empty; } diff --git a/src/NUnitFramework/framework/Constraints/EndsWithConstraint.cs b/src/NUnitFramework/framework/Constraints/EndsWithConstraint.cs index c3e3c216e2..e99e826319 100644 --- a/src/NUnitFramework/framework/Constraints/EndsWithConstraint.cs +++ b/src/NUnitFramework/framework/Constraints/EndsWithConstraint.cs @@ -26,7 +26,7 @@ public EndsWithConstraint(string expected) : base(expected) /// /// /// - protected override bool Matches(string actual) + protected override bool Matches(string? actual) { var stringComparison = caseInsensitive ? StringComparison.CurrentCultureIgnoreCase : StringComparison.CurrentCulture; return actual is not null && actual.EndsWith(expected, stringComparison); diff --git a/src/NUnitFramework/framework/Constraints/EqualConstraint.cs b/src/NUnitFramework/framework/Constraints/EqualConstraint.cs index 31eee03212..7bc9a6a07d 100644 --- a/src/NUnitFramework/framework/Constraints/EqualConstraint.cs +++ b/src/NUnitFramework/framework/Constraints/EqualConstraint.cs @@ -65,6 +65,14 @@ public EqualConstraint(object? expected) /// public bool CaseInsensitive => _comparer.IgnoreCase; + /// + /// Gets a value indicating whether to compare ignoring white space. + /// + /// + /// if comparing ignoreing white space; otherwise, . + /// + public bool IgnoringWhiteSpace => _comparer.IgnoreWhiteSpace; + /// /// Gets a value indicating whether or not to clip strings. /// @@ -96,6 +104,18 @@ public EqualConstraint IgnoreCase } } + /// + /// Flag the constraint to ignore white space and return self. + /// + public EqualConstraint IgnoreWhiteSpace + { + get + { + _comparer.IgnoreWhiteSpace = true; + return this; + } + } + /// /// Flag the constraint to suppress string clipping /// and return self. @@ -397,6 +417,9 @@ public override string Description if (_comparer.IgnoreCase) sb.Append(", ignoring case"); + if (_comparer.IgnoreWhiteSpace) + sb.Append(", ignoring white-space"); + return sb.ToString(); } } diff --git a/src/NUnitFramework/framework/Constraints/EqualConstraintResult.cs b/src/NUnitFramework/framework/Constraints/EqualConstraintResult.cs index ecdce827e7..4b45e9fa41 100644 --- a/src/NUnitFramework/framework/Constraints/EqualConstraintResult.cs +++ b/src/NUnitFramework/framework/Constraints/EqualConstraintResult.cs @@ -16,6 +16,7 @@ public class EqualConstraintResult : ConstraintResult private readonly object? _expectedValue; private readonly Tolerance _tolerance; private readonly bool _caseInsensitive; + private readonly bool _ignoringWhiteSpace; private readonly bool _clipStrings; private readonly IList _failurePoints; @@ -49,6 +50,7 @@ public EqualConstraintResult(EqualConstraint constraint, object? actual, bool ha _expectedValue = constraint.Arguments[0]; _tolerance = constraint.Tolerance; _caseInsensitive = constraint.CaseInsensitive; + _ignoringWhiteSpace = constraint.IgnoringWhiteSpace; _clipStrings = constraint.ClipStrings; _failurePoints = constraint.FailurePoints; } @@ -82,14 +84,14 @@ private void DisplayDifferences(MessageWriter writer, object? expected, object? #region DisplayStringDifferences private void DisplayStringDifferences(MessageWriter writer, string expected, string actual) { - int mismatch = MsgUtils.FindMismatchPosition(expected, actual, 0, _caseInsensitive); + (int mismatchExpected, int mismatchActual) = MsgUtils.FindMismatchPosition(expected, actual, _caseInsensitive, _ignoringWhiteSpace); if (expected.Length == actual.Length) - writer.WriteMessageLine(StringsDiffer_1, expected.Length, mismatch); + writer.WriteMessageLine(StringsDiffer_1, expected.Length, mismatchExpected); else - writer.WriteMessageLine(StringsDiffer_2, expected.Length, actual.Length, mismatch); + writer.WriteMessageLine(StringsDiffer_2, expected.Length, actual.Length, mismatchExpected); - writer.DisplayStringDifferences(expected, actual, mismatch, _caseInsensitive, _clipStrings); + writer.DisplayStringDifferences(expected, actual, mismatchExpected, mismatchActual, _caseInsensitive, _ignoringWhiteSpace, _clipStrings); } #endregion @@ -162,12 +164,12 @@ private void DisplayCollectionDifferenceWithFailurePoint(MessageWriter writer, I { if (failurePoint.ExpectedValue is string expectedString && failurePoint.ActualValue is string actualString) { - int mismatch = MsgUtils.FindMismatchPosition(expectedString, actualString, 0, _caseInsensitive); + (int mismatchExpected, int _) = MsgUtils.FindMismatchPosition(expectedString, actualString, _caseInsensitive, _ignoringWhiteSpace); if (expectedString.Length == actualString.Length) - writer.WriteMessageLine(StringsDiffer_1, expectedString.Length, mismatch); + writer.WriteMessageLine(StringsDiffer_1, expectedString.Length, mismatchExpected); else - writer.WriteMessageLine(StringsDiffer_2, expectedString.Length, actualString.Length, mismatch); + writer.WriteMessageLine(StringsDiffer_2, expectedString.Length, actualString.Length, mismatchExpected); writer.WriteLine($" Expected: {MsgUtils.FormatCollection(expected)}"); writer.WriteLine($" But was: {MsgUtils.FormatCollection(actual)}"); writer.WriteLine($" First non-matching item at index [{failurePoint.Position}]: \"{failurePoint.ExpectedValue}\""); diff --git a/src/NUnitFramework/framework/Constraints/MessageWriter.cs b/src/NUnitFramework/framework/Constraints/MessageWriter.cs index 07d60a8c25..c040c8c00d 100644 --- a/src/NUnitFramework/framework/Constraints/MessageWriter.cs +++ b/src/NUnitFramework/framework/Constraints/MessageWriter.cs @@ -2,6 +2,7 @@ using System.IO; using System.Collections; +using System; namespace NUnit.Framework.Constraints { @@ -81,11 +82,33 @@ public void WriteMessageLine(string message, params object?[]? args) /// /// The expected string value /// The actual string value - /// The point at which the strings don't match or -1 + /// The point in at which the strings don't match or -1 /// If true, case is ignored in locating the point where the strings differ /// If true, the strings should be clipped to fit the line public abstract void DisplayStringDifferences(string expected, string actual, int mismatch, bool ignoreCase, bool clipping); + /// + /// Display the expected and actual string values on separate lines. + /// If the mismatch parameter is >=0, an additional line is displayed + /// line containing a caret that points to the mismatch point. + /// + /// The expected string value + /// The actual string value + /// The point in at which the strings don't match or -1 + /// The point in at which the strings don't match or -1 + /// If true, case is ignored in locating the point where the strings differ + /// If true, white space is ignored in locating the point where the strings differ + /// If true, the strings should be clipped to fit the line + public virtual void DisplayStringDifferences(string expected, string actual, int mismatchExpected, int mismatchActual, bool ignoreCase, bool ignoreWhiteSpace, bool clipping) + { + if (ignoreWhiteSpace && mismatchExpected != mismatchActual) + { + throw new NotImplementedException("Please override to show difference with 'ignoreWhiteSpace'"); + } + + DisplayStringDifferences(expected, actual, mismatchExpected, ignoreCase, clipping); + } + /// /// Writes the text for an actual value. /// diff --git a/src/NUnitFramework/framework/Constraints/MsgUtils.cs b/src/NUnitFramework/framework/Constraints/MsgUtils.cs index ba72fc3930..5d1a98e4f0 100644 --- a/src/NUnitFramework/framework/Constraints/MsgUtils.cs +++ b/src/NUnitFramework/framework/Constraints/MsgUtils.cs @@ -395,64 +395,84 @@ public static string GetTypeRepresentation(object obj) [return: NotNullIfNotNull("s")] public static string? EscapeControlChars(string? s) { - if (s is not null) - { - StringBuilder sb = new StringBuilder(); + int index = 0; + return EscapeControlChars(s, ref index); + } - foreach (char c in s) - { - switch (c) - { - //case '\'': - // sb.Append("\\\'"); - // break; - //case '\"': - // sb.Append("\\\""); - // break; - case '\\': - sb.Append("\\\\"); - break; - case '\0': - sb.Append("\\0"); - break; - case '\a': - sb.Append("\\a"); - break; - case '\b': - sb.Append("\\b"); - break; - case '\f': - sb.Append("\\f"); - break; - case '\n': - sb.Append("\\n"); - break; - case '\r': - sb.Append("\\r"); - break; - case '\t': - sb.Append("\\t"); - break; - case '\v': - sb.Append("\\v"); - break; + /// + /// Converts any control characters in a string + /// to their escaped representation. + /// + /// The string to be converted + /// The index in the array of a specific spot, which needs to be updated when expanding. + /// The converted string + [return: NotNullIfNotNull("s")] + public static string? EscapeControlChars(string? s, ref int index) + { + if (s is null) + return null; - case '\x0085': - case '\x2028': - case '\x2029': - sb.Append($"\\x{(int)c:X4}"); - break; + int originalIndex = index; + const int headRoom = 42; + StringBuilder sb = new(s.Length + headRoom); - default: - sb.Append(c); - break; + for (int i = 0; i < s.Length; i++) + { + char c = s[i]; + string? escaped = EscapeControlChars(c); + if (escaped is null) + { + sb.Append(c); + } + else + { + sb.Append(escaped); + if (originalIndex > i) + { + index += escaped.Length - 1; } } - - s = sb.ToString(); } - return s; + return sb.ToString(); + } + + private static string? EscapeControlChars(char c) + { + switch (c) + { + //case '\'': + // return "\\\'"; + //case '\"': + // return ("\\\""); + // break; + case '\\': + return "\\\\"; + case '\0': + return "\\0"; + case '\a': + return "\\a"; + case '\b': + return "\\b"; + case '\f': + return "\\f"; + case '\n': + return "\\n"; + case '\r': + return "\\r"; + case '\t': + return "\\t"; + case '\v': + return "\\v"; + + case '\x0085': + case '\x2028': + case '\x2029': + return $"\\x{(int)c:X4}"; + + default: + return null; + } } /// @@ -466,7 +486,8 @@ public static string GetTypeRepresentation(object obj) { if (s is not null) { - StringBuilder sb = new StringBuilder(); + const int headRoom = 42; + StringBuilder sb = new(s.Length + headRoom); foreach (char c in s) { @@ -536,85 +557,147 @@ public static int[] GetArrayIndicesFromCollectionIndex(IEnumerable collection, l /// string with ellipses representing the removed parts /// /// The string to be clipped - /// The maximum permitted length of the result string + /// The length of the clipped string /// The point at which to start clipping /// The clipped string - public static string ClipString(string s, int maxStringLength, int clipStart) + public static string ClipString(string s, int clipLength, int clipStart) { - int clipLength = maxStringLength; - StringBuilder sb = new StringBuilder(); + StringBuilder sb = new StringBuilder(s.Length + 2 * ELLIPSIS.Length); if (clipStart > 0) - { - clipLength -= ELLIPSIS.Length; sb.Append(ELLIPSIS); + + int remainingLength = s.Length - clipStart; + int count = Math.Min(remainingLength, clipLength); + sb.Append(s, clipStart, count); + + if (remainingLength > clipLength) + sb.Append(ELLIPSIS); + + return sb.ToString(); + } + + /// + /// Clips the string if it exceeds . + /// + /// + /// The string ensures that the content around stays visible + /// by either clipping from the front or the back or both. The clipped part is replaced with "...". + /// + /// The string to clip + /// The assumed length of the string (needed if called for a pair) + /// The maximum length of the display message. + /// The location in that needs to stay visible. + /// Clip string with a maximum length of . + public static string ClipWhenNeeded(string s, int length, int maxDisplayLength, ref int mismatchLocation) + { + if (length <= maxDisplayLength) + { + // No need to clip + return s; } - if (s.Length - clipStart > clipLength) + // We need to clip at least one side. + maxDisplayLength -= ELLIPSIS.Length; + + const int minimumJoiningMatchingCharacters = 5; + + int clipStart; + + if (mismatchLocation + minimumJoiningMatchingCharacters < maxDisplayLength) { - clipLength -= ELLIPSIS.Length; - sb.Append(s.Substring(clipStart, clipLength)); - sb.Append(ELLIPSIS); + // Clip the tail + clipStart = 0; } - else if (clipStart > 0) + else if (length - mismatchLocation + minimumJoiningMatchingCharacters < maxDisplayLength) { - sb.Append(s.Substring(clipStart)); + // Show the tail + clipStart = length - maxDisplayLength; } else { - sb.Append(s); + // We need to clip both sides. + maxDisplayLength -= ELLIPSIS.Length; + + // Centre the clip around the mismatchLocation + clipStart = mismatchLocation - maxDisplayLength / 2; } - return sb.ToString(); + if (clipStart > 0) + { + // If clipping off the front, adjust the location + // and correct for the ... added to the front. + mismatchLocation -= clipStart - ELLIPSIS.Length; + } + + return ClipString(s, maxDisplayLength, clipStart); } /// /// Clip the expected and actual strings in a coordinated fashion, /// so that they may be displayed together. /// - /// - /// - /// - /// - public static void ClipExpectedAndActual(ref string expected, ref string actual, int maxDisplayLength, int mismatch) + /// + /// The values of and + /// are assumed to be the same. If and + /// are not linked, then call individually. + /// + /// The expected string to clip + /// The actual string to clip + /// The maximum length of the display message. + /// The location in that needs to stay visible. + /// The location in that needs to stay visible. + public static void ClipExpectedAndActual(ref string expected, ref string actual, int maxDisplayLength, ref int mismatchExpected, ref int mismatchActual) { - // Case 1: Both strings fit on line - int maxStringLength = Math.Max(expected.Length, actual.Length); - if (maxStringLength <= maxDisplayLength) - return; - - // Case 2: Assume that the tail of each string fits on line - int clipLength = maxDisplayLength - ELLIPSIS.Length; - int clipStart = maxStringLength - clipLength; + if (mismatchExpected != mismatchActual) + { + throw new ArgumentException($"The values for {nameof(mismatchExpected)} and {nameof(mismatchActual)} should be the same."); + } - // Case 3: If it doesn't, center the mismatch position - if (clipStart > mismatch) - clipStart = Math.Max(0, mismatch - clipLength / 2); + // Clip based upon longest length + int longestLength = Math.Max(expected.Length, actual.Length); + if (longestLength <= maxDisplayLength) + return; - expected = ClipString(expected, maxDisplayLength, clipStart); - actual = ClipString(actual, maxDisplayLength, clipStart); + expected = ClipWhenNeeded(expected, longestLength, maxDisplayLength, ref mismatchExpected); + actual = ClipWhenNeeded(actual, longestLength, maxDisplayLength, ref mismatchActual); } /// - /// Shows the position two strings start to differ. Comparison - /// starts at the start index. + /// Finds the position two strings start to differ. /// /// The expected string /// The actual string - /// The index in the strings at which comparison should start /// Boolean indicating whether case should be ignored - /// -1 if no mismatch found, or the index where mismatch found - public static int FindMismatchPosition(string expected, string actual, int istart, bool ignoreCase) + /// Boolean indicating whether white space should be ignored + /// (-1,-1) if no mismatch found, or the indices (expected, actual) where mismatches found. + public static (int, int) FindMismatchPosition(string expected, string actual, bool ignoreCase, bool ignoreWhiteSpace) { - int length = Math.Min(expected.Length, actual.Length); - string s1 = ignoreCase ? expected.ToLower() : expected; string s2 = ignoreCase ? actual.ToLower() : actual; + int i1 = 0; + int i2 = 0; - for (int i = istart; i < length; i++) + while (true) { - if (s1[i] != s2[i]) - return i; + if (ignoreWhiteSpace) + { + // Find next non-white space character in both s1 and s2. + i1 = FindNonWhiteSpace(s1, i1); + i2 = FindNonWhiteSpace(s2, i2); + } + + if (i1 < s1.Length && i2 < s2.Length) + { + if (s1[i1] != s2[i2]) + return (i1, i2); + i1++; + i2++; + } + else + { + break; + } } // @@ -622,13 +705,21 @@ public static int FindMismatchPosition(string expected, string actual, int istar // Mismatch occurs because string lengths are different, so show // that they start differing where the shortest string ends // - if (expected.Length != actual.Length) - return length; + if (i1 < s1.Length || i2 < s2.Length) + return (i1, i2); // // Same strings : We shouldn't get here // - return -1; + return (-1, -1); + } + + private static int FindNonWhiteSpace(string s, int i) + { + while (i < s.Length && char.IsWhiteSpace(s[i])) + i++; + + return i; } } } diff --git a/src/NUnitFramework/framework/Constraints/NUnitEqualityComparer.cs b/src/NUnitFramework/framework/Constraints/NUnitEqualityComparer.cs index 4d05f2262a..4777868a0a 100644 --- a/src/NUnitFramework/framework/Constraints/NUnitEqualityComparer.cs +++ b/src/NUnitFramework/framework/Constraints/NUnitEqualityComparer.cs @@ -58,6 +58,11 @@ public sealed class NUnitEqualityComparer /// private bool _caseInsensitive; + /// + /// If true, all string comparisons will ignore white space differences + /// + private bool _ignoreWhiteSpace; + /// /// If true, arrays will be treated as collections, allowing /// those of different dimensions to be compared @@ -101,6 +106,16 @@ public bool IgnoreCase set => _caseInsensitive = value; } + /// + /// Gets and sets a flag indicating whether white space should + /// be ignored in determining equality. + /// + public bool IgnoreWhiteSpace + { + get => _ignoreWhiteSpace; + set => _ignoreWhiteSpace = value; + } + /// /// Gets and sets a flag indicating whether an instance properties /// should be compared when determining equality. diff --git a/src/NUnitFramework/framework/Constraints/SamePathConstraint.cs b/src/NUnitFramework/framework/Constraints/SamePathConstraint.cs index 6f09d49a88..9bac6757cb 100644 --- a/src/NUnitFramework/framework/Constraints/SamePathConstraint.cs +++ b/src/NUnitFramework/framework/Constraints/SamePathConstraint.cs @@ -28,7 +28,7 @@ public SamePathConstraint(string expected) : base(expected) /// /// The value to be tested /// True for success, false for failure - protected override bool Matches(string actual) + protected override bool Matches(string? actual) { return actual is not null && StringUtil.StringsEqual(Canonicalize(expected), Canonicalize(actual), caseInsensitive); } diff --git a/src/NUnitFramework/framework/Constraints/SamePathOrUnderConstraint.cs b/src/NUnitFramework/framework/Constraints/SamePathOrUnderConstraint.cs index 98d9bc9c1b..c23c981c88 100644 --- a/src/NUnitFramework/framework/Constraints/SamePathOrUnderConstraint.cs +++ b/src/NUnitFramework/framework/Constraints/SamePathOrUnderConstraint.cs @@ -28,7 +28,7 @@ public SamePathOrUnderConstraint(string expected) : base(expected) /// /// The value to be tested /// True for success, false for failure - protected override bool Matches(string actual) + protected override bool Matches(string? actual) { if (actual is null) return false; diff --git a/src/NUnitFramework/framework/Constraints/StartsWithConstraint.cs b/src/NUnitFramework/framework/Constraints/StartsWithConstraint.cs index 3397186d3a..c40eb78a61 100644 --- a/src/NUnitFramework/framework/Constraints/StartsWithConstraint.cs +++ b/src/NUnitFramework/framework/Constraints/StartsWithConstraint.cs @@ -26,7 +26,7 @@ public StartsWithConstraint(string expected) : base(expected) /// /// /// - protected override bool Matches(string actual) + protected override bool Matches(string? actual) { var stringComparison = caseInsensitive ? StringComparison.CurrentCultureIgnoreCase : StringComparison.CurrentCulture; return actual is not null && actual.StartsWith(expected, stringComparison); diff --git a/src/NUnitFramework/framework/Constraints/StringConstraint.cs b/src/NUnitFramework/framework/Constraints/StringConstraint.cs index b4ce5a2841..b2787711b1 100644 --- a/src/NUnitFramework/framework/Constraints/StringConstraint.cs +++ b/src/NUnitFramework/framework/Constraints/StringConstraint.cs @@ -100,6 +100,6 @@ public override ConstraintResult ApplyTo(TActual actual) /// /// The string to be tested /// True for success, false for failure - protected abstract bool Matches(string actual); + protected abstract bool Matches(string? actual); } } diff --git a/src/NUnitFramework/framework/Constraints/SubPathConstraint.cs b/src/NUnitFramework/framework/Constraints/SubPathConstraint.cs index b72b63bd35..6f4d7a8b93 100644 --- a/src/NUnitFramework/framework/Constraints/SubPathConstraint.cs +++ b/src/NUnitFramework/framework/Constraints/SubPathConstraint.cs @@ -26,7 +26,7 @@ public SubPathConstraint(string expected) : base(expected) /// /// The value to be tested /// True for success, false for failure - protected override bool Matches(string actual) + protected override bool Matches(string? actual) { return actual is not null && IsSubPath(Canonicalize(expected), Canonicalize(actual)); } diff --git a/src/NUnitFramework/framework/Constraints/SubstringConstraint.cs b/src/NUnitFramework/framework/Constraints/SubstringConstraint.cs index f4e46aa503..71e45f7125 100644 --- a/src/NUnitFramework/framework/Constraints/SubstringConstraint.cs +++ b/src/NUnitFramework/framework/Constraints/SubstringConstraint.cs @@ -41,7 +41,7 @@ public override StringConstraint IgnoreCase /// /// The value to be tested /// True for success, false for failure - protected override bool Matches(string actual) + protected override bool Matches(string? actual) { if (actual is null) return false; diff --git a/src/NUnitFramework/framework/Constraints/UniqueItemsConstraint.cs b/src/NUnitFramework/framework/Constraints/UniqueItemsConstraint.cs index dd7ba6f96e..a6e32080d8 100644 --- a/src/NUnitFramework/framework/Constraints/UniqueItemsConstraint.cs +++ b/src/NUnitFramework/framework/Constraints/UniqueItemsConstraint.cs @@ -5,6 +5,8 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; +using System.Text.RegularExpressions; +using NUnit.Framework.Constraints.Comparers; using NUnit.Framework.Internal; namespace NUnit.Framework.Constraints @@ -100,10 +102,10 @@ private ICollection OriginalAlgorithm(IEnumerable actual) { var itemsOfT = ItemsCastMethod.MakeGenericMethod(itemsType).Invoke(null, new[] { actual })!; - if (IgnoringCase) + if (IgnoringCase || IgnoringWhiteSpace) { if (itemsType == typeof(string)) - return (ICollection)StringsUniqueIgnoringCase((IEnumerable)itemsOfT); + return (ICollection)StringsUniqueIgnoringCaseOrWhiteSpace((IEnumerable)itemsOfT); else if (itemsType == typeof(char)) return (ICollection)CharsUniqueIgnoringCase((IEnumerable)itemsOfT); } @@ -156,11 +158,11 @@ private ICollection GetNonUniqueItems(IEnumerable actual) else if (!IsTypeSafeForFastPath(memberType)) return OriginalAlgorithm(actual); - // Special handling for ignore case with strings and chars - if (IgnoringCase) + // Special handling for ignore case/white-space with strings and chars + if (IgnoringCase || IgnoringWhiteSpace) { if (memberType == typeof(string)) - return (ICollection)StringsUniqueIgnoringCase((IEnumerable)actual); + return (ICollection)StringsUniqueIgnoringCaseOrWhiteSpace((IEnumerable)actual); else if (memberType == typeof(char)) return (ICollection)CharsUniqueIgnoringCase((IEnumerable)actual); } @@ -182,14 +184,14 @@ private static bool IsTypeSafeForFastPath(Type? type) private static ICollection ItemsUnique(IEnumerable actual) => NonUniqueItemsInternal(actual, EqualityComparer.Default); - private ICollection StringsUniqueIgnoringCase(IEnumerable actual) - => NonUniqueItemsInternal(actual, new NUnitStringEqualityComparer(IgnoringCase)); + private ICollection StringsUniqueIgnoringCaseOrWhiteSpace(IEnumerable actual) + => NonUniqueItemsInternal(actual, new NUnitStringEqualityComparer(IgnoringCase, IgnoringWhiteSpace)); private ICollection CharsUniqueIgnoringCase(IEnumerable actual) { var result = NonUniqueItemsInternal( actual.Select(x => x.ToString()), - new NUnitStringEqualityComparer(IgnoringCase)); + new NUnitStringEqualityComparer(IgnoringCase, false)); return result.Select(x => x[0]).ToList(); } @@ -247,27 +249,40 @@ private static bool IsHandledSpeciallyByNUnit(Type type) private sealed class NUnitStringEqualityComparer : IEqualityComparer { + private static readonly Regex WhiteSpace = new(@"\s+", RegexOptions.Compiled); + private readonly bool _ignoreCase; + private readonly bool _ignoreWhiteSpace; - public NUnitStringEqualityComparer(bool ignoreCase) + public NUnitStringEqualityComparer(bool ignoreCase, bool ignoreWhiteSpace) { _ignoreCase = ignoreCase; + _ignoreWhiteSpace = ignoreWhiteSpace; } public bool Equals(string? x, string? y) { - var stringComparison = _ignoreCase ? StringComparison.CurrentCultureIgnoreCase : StringComparison.Ordinal; - return string.Equals(x, y, stringComparison); + return x is not null && y is not null ? + StringsComparer.Equals(x, y, _ignoreCase, _ignoreWhiteSpace) : + ReferenceEquals(x, y); } public int GetHashCode(string obj) { - if (obj is null) + if (obj is not string s) + { return 0; - else if (_ignoreCase) - return StringComparer.CurrentCultureIgnoreCase.GetHashCode(obj); + } + + if (_ignoreWhiteSpace) + { + s = WhiteSpace.Replace(s, string.Empty); + } + + if (_ignoreCase) + return StringComparer.CurrentCultureIgnoreCase.GetHashCode(s); else - return obj.GetHashCode(); + return s.GetHashCode(); } } diff --git a/src/NUnitFramework/framework/Constraints/WhiteSpaceConstraint.cs b/src/NUnitFramework/framework/Constraints/WhiteSpaceConstraint.cs new file mode 100644 index 0000000000..dc9d80b69b --- /dev/null +++ b/src/NUnitFramework/framework/Constraints/WhiteSpaceConstraint.cs @@ -0,0 +1,28 @@ +// Copyright (c) Charlie Poole, Rob Prouse and Contributors. MIT License - see LICENSE.txt + +namespace NUnit.Framework.Constraints +{ + /// + /// WhiteSpaceConstraint tests whether a string contains white space. + /// + public class WhiteSpaceConstraint : StringConstraint + { + private const string WhiteSpace = "white-space"; + + /// + public override string Description => WhiteSpace; + + /// + public override string DisplayName => WhiteSpace; + + /// + /// Test whether the constraint is satisfied by a given value + /// + /// The value to be tested + /// True for success, false for failure + protected override bool Matches(string? actual) + { + return string.IsNullOrWhiteSpace(actual); + } + } +} diff --git a/src/NUnitFramework/framework/Internal/Execution/TextMessageWriter.cs b/src/NUnitFramework/framework/Internal/Execution/TextMessageWriter.cs index f7547e5e7a..972cb4bb4e 100644 --- a/src/NUnitFramework/framework/Internal/Execution/TextMessageWriter.cs +++ b/src/NUnitFramework/framework/Internal/Execution/TextMessageWriter.cs @@ -164,17 +164,14 @@ public override void DisplayDifferences(object? expected, object? actual, Tolera } } - /// - /// Display the expected and actual string values on separate lines. - /// If the mismatch parameter is >=0, an additional line is displayed - /// line containing a caret that points to the mismatch point. - /// - /// The expected string value - /// The actual string value - /// The point at which the strings don't match or -1 - /// If true, case is ignored in string comparisons - /// If true, clip the strings to fit the max line length + /// public override void DisplayStringDifferences(string expected, string actual, int mismatch, bool ignoreCase, bool clipping) + { + DisplayStringDifferences(expected, actual, mismatch, mismatch, ignoreCase, false, clipping); + } + + /// + public override void DisplayStringDifferences(string expected, string actual, int mismatchExpected, int mismatchActual, bool ignoreCase, bool ignoreWhiteSpace, bool clipping) { // Maximum string we can display without truncating int maxDisplayLength = MaxLineLength @@ -182,23 +179,33 @@ public override void DisplayStringDifferences(string expected, string actual, in - 2; // 2 quotation marks if (clipping) - MsgUtils.ClipExpectedAndActual(ref expected, ref actual, maxDisplayLength, mismatch); - - expected = MsgUtils.EscapeControlChars(expected); - actual = MsgUtils.EscapeControlChars(actual); + { + if (ignoreWhiteSpace) + { + expected = MsgUtils.ClipWhenNeeded(expected, expected.Length, maxDisplayLength, ref mismatchExpected); + actual = MsgUtils.ClipWhenNeeded(actual, actual.Length, maxDisplayLength, ref mismatchActual); + } + else + { + MsgUtils.ClipExpectedAndActual(ref expected, ref actual, maxDisplayLength, ref mismatchExpected, ref mismatchActual); + } + } - // The mismatch position may have changed due to clipping or white space conversion - mismatch = MsgUtils.FindMismatchPosition(expected, actual, 0, ignoreCase); + expected = MsgUtils.EscapeControlChars(expected, ref mismatchExpected); + actual = MsgUtils.EscapeControlChars(actual, ref mismatchActual); Write(Pfx_Expected); Write(MsgUtils.FormatValue(expected)); if (ignoreCase) Write(", ignoring case"); + if (ignoreWhiteSpace) + Write(", ignoring white-space"); WriteLine(); + if (mismatchExpected >= 0 && mismatchExpected != mismatchActual) + WriteCaretLine(mismatchExpected); WriteActualLine(actual); - //DisplayDifferences(expected, actual); - if (mismatch >= 0) - WriteCaretLine(mismatch); + if (mismatchActual >= 0) + WriteCaretLine(mismatchActual); } #endregion diff --git a/src/NUnitFramework/framework/Is.cs b/src/NUnitFramework/framework/Is.cs index aa0c42a6cb..8dc06cbea5 100644 --- a/src/NUnitFramework/framework/Is.cs +++ b/src/NUnitFramework/framework/Is.cs @@ -115,6 +115,15 @@ public abstract class Is #endregion + #region WhiteSpace + + /// + /// Returns a constraint that tests for white-space + /// + public static WhiteSpaceConstraint WhiteSpace => new(); + + #endregion + #region Unique /// diff --git a/src/NUnitFramework/framework/nunit.framework.csproj b/src/NUnitFramework/framework/nunit.framework.csproj index 32f1a3581b..36818728cf 100644 --- a/src/NUnitFramework/framework/nunit.framework.csproj +++ b/src/NUnitFramework/framework/nunit.framework.csproj @@ -9,6 +9,7 @@ + diff --git a/src/NUnitFramework/tests/Constraints/AnyOfConstraintTests.cs b/src/NUnitFramework/tests/Constraints/AnyOfConstraintTests.cs index 7fed988e6a..d31cabb9d4 100644 --- a/src/NUnitFramework/tests/Constraints/AnyOfConstraintTests.cs +++ b/src/NUnitFramework/tests/Constraints/AnyOfConstraintTests.cs @@ -30,6 +30,20 @@ public void ItemIsPresent_IgnoreCase() Assert.That(anyOf.ApplyTo("AB").Status, Is.EqualTo(ConstraintStatus.Success)); } + [Test] + public void ItemIsPresent_IgnoreWhiteSpace() + { + var anyOf = new AnyOfConstraint(new[] { "a", "B", "a b" }).IgnoreWhiteSpace; + Assert.That(anyOf.ApplyTo("ab").Status, Is.EqualTo(ConstraintStatus.Success)); + } + + [Test] + public void ItemIsPresent_IgnoreCaseWhiteSpace() + { + var anyOf = new AnyOfConstraint(new[] { "a", "B", "ab" }).IgnoreCase.IgnoreWhiteSpace; + Assert.That(anyOf.ApplyTo("A B").Status, Is.EqualTo(ConstraintStatus.Success)); + } + [Test] public void ItemIsPresent_WithEqualityComparer() { diff --git a/src/NUnitFramework/tests/Constraints/CollectionEqualsTests.cs b/src/NUnitFramework/tests/Constraints/CollectionEqualsTests.cs index 6147d04019..2d015d7c8d 100644 --- a/src/NUnitFramework/tests/Constraints/CollectionEqualsTests.cs +++ b/src/NUnitFramework/tests/Constraints/CollectionEqualsTests.cs @@ -95,6 +95,17 @@ public void HonorsIgnoreCase(IEnumerable expected, IEnumerable actual) new object[] { new List { "a", "b", "c" }, new List { "A", "B", "C" } }, }; + [TestCaseSource(nameof(IgnoreWhiteSpaceData))] + public void HonorsIgnoreWhiteSpace(IEnumerable expected, IEnumerable actual) + { + Assert.That(expected, Is.EqualTo(actual).IgnoreWhiteSpace); + } + + private static readonly object[] IgnoreWhiteSpaceData = + { + new object[] { new SimpleObjectCollection(" x", "y ", " z "), new SimpleObjectCollection("x ", " y", "z") }, + }; + [Test] [DefaultFloatingPointTolerance(0.5)] public void StructuralComparerOnSameCollection_RespectsAndSetsToleranceByRef() diff --git a/src/NUnitFramework/tests/Constraints/CollectionEquivalentConstraintTests.cs b/src/NUnitFramework/tests/Constraints/CollectionEquivalentConstraintTests.cs index 3d2e4b0e6c..2bc87f10fd 100644 --- a/src/NUnitFramework/tests/Constraints/CollectionEquivalentConstraintTests.cs +++ b/src/NUnitFramework/tests/Constraints/CollectionEquivalentConstraintTests.cs @@ -101,6 +101,15 @@ public void EquivalentHonorsIgnoreCase() Assert.That(new CollectionEquivalentConstraint(set1).IgnoreCase.ApplyTo(set2).IsSuccess); } + [Test] + public void EquivalentHonorsIgnoreWhiteSpace() + { + ICollection set1 = new SimpleObjectCollection("abc", "def", "ghi"); + ICollection set2 = new SimpleObjectCollection("g h i", "d e f", "a b c"); + + Assert.That(new CollectionEquivalentConstraint(set1).IgnoreWhiteSpace.ApplyTo(set2).IsSuccess); + } + [Test] [TestCaseSource(typeof(IgnoreCaseDataProvider), nameof(IgnoreCaseDataProvider.TestCases))] public void HonorsIgnoreCase(IEnumerable expected, IEnumerable actual) diff --git a/src/NUnitFramework/tests/Constraints/ConstraintExpressionTests.cs b/src/NUnitFramework/tests/Constraints/ConstraintExpressionTests.cs index 0af9b1d135..b81c42c78c 100644 --- a/src/NUnitFramework/tests/Constraints/ConstraintExpressionTests.cs +++ b/src/NUnitFramework/tests/Constraints/ConstraintExpressionTests.cs @@ -88,6 +88,14 @@ public void ConstraintExpressionAnyOfType() Assert.That("red", constraint); } + [Test] + public void ConstraintExpressionAnyOfTypeIgnoreWhiteSpace() + { + var constraintExpression = new ConstraintExpression(); + var constraint = constraintExpression.AnyOf(new string[] { "RED", "GREEN" }).IgnoreWhiteSpace; + Assert.That(" R E D ", constraint); + } + [Test] public void ConstraintExpressionAnyOfList() { diff --git a/src/NUnitFramework/tests/Constraints/ContainsConstraintTests.cs b/src/NUnitFramework/tests/Constraints/ContainsConstraintTests.cs index f7328ed344..0a6fd2b7cd 100644 --- a/src/NUnitFramework/tests/Constraints/ContainsConstraintTests.cs +++ b/src/NUnitFramework/tests/Constraints/ContainsConstraintTests.cs @@ -21,6 +21,26 @@ public void HonorsIgnoreCaseForStringCollection() Assert.That(result.IsSuccess); } + [Test] + public void HonorsIgnoreWhiteSpaceForStringCollection() + { + var actualItems = new[] { "ABC", "d e f" }; + var constraint = new ContainsConstraint("def").IgnoreWhiteSpace; + + var result = constraint.ApplyTo(actualItems); + Assert.That(result.IsSuccess); + } + + [Test] + public void HonorsIgnoreWhiteSpaceForStringCollectionSearchItem() + { + var actualItems = new[] { "ABC", "d e f" }; + var constraint = new ContainsConstraint("A B C").IgnoreWhiteSpace; + + var result = constraint.ApplyTo(actualItems); + Assert.That(result.IsSuccess); + } + [Test, SetCulture("en-US")] public void HonorsIgnoreCaseForString() { diff --git a/src/NUnitFramework/tests/Constraints/DictionaryContainsKeyValueConstraintTests.cs b/src/NUnitFramework/tests/Constraints/DictionaryContainsKeyValueConstraintTests.cs index 03a30be62d..30426387b6 100644 --- a/src/NUnitFramework/tests/Constraints/DictionaryContainsKeyValueConstraintTests.cs +++ b/src/NUnitFramework/tests/Constraints/DictionaryContainsKeyValueConstraintTests.cs @@ -181,6 +181,14 @@ public void IgnoreCaseIsHonored() Assert.That(dictionary, new DictionaryContainsKeyValuePairConstraint("HI", "UNIVERSE").IgnoreCase); } + [Test] + public void IgnoreWhiteSpaceIsHonored() + { + var dictionary = new Dictionary { { "Hello", "World" }, { "Hi ", "Universe" }, { "Hola", "Mundo" } }; + + Assert.That(dictionary, new DictionaryContainsKeyValuePairConstraint("Hi", " U n i v e r s e").IgnoreWhiteSpace); + } + [Test, SetCulture("en-US")] public void UsingIsHonored() { diff --git a/src/NUnitFramework/tests/Constraints/DictionaryContainsValueConstraintTests.cs b/src/NUnitFramework/tests/Constraints/DictionaryContainsValueConstraintTests.cs index 78c66f2777..8a7d0ddd52 100644 --- a/src/NUnitFramework/tests/Constraints/DictionaryContainsValueConstraintTests.cs +++ b/src/NUnitFramework/tests/Constraints/DictionaryContainsValueConstraintTests.cs @@ -69,6 +69,14 @@ public void IgnoreCaseIsHonored() Assert.That(dictionary, new DictionaryContainsValueConstraint("UNIVERSE").IgnoreCase); } + [Test] + public void IgnoreWhiteSpaceIsHonored() + { + var dictionary = new Dictionary { { "Hello", "World" }, { "Hi", "Universe" }, { "Hola", "Mundo" } }; + + Assert.That(dictionary, new DictionaryContainsValueConstraint("U n i v e r s e").IgnoreWhiteSpace); + } + [Test, SetCulture("en-US")] public void UsingIsHonored() { diff --git a/src/NUnitFramework/tests/Constraints/EqualConstraintTests.cs b/src/NUnitFramework/tests/Constraints/EqualConstraintTests.cs index 38d161cb30..371457cb80 100644 --- a/src/NUnitFramework/tests/Constraints/EqualConstraintTests.cs +++ b/src/NUnitFramework/tests/Constraints/EqualConstraintTests.cs @@ -66,6 +66,59 @@ public void DoesntRespectCultureWhenCasingMatters() Assert.That(result.IsSuccess, Is.False); } + [Test] + public void IgnoreWhiteSpace() + { + var constraint = new EqualConstraint("Hello World").IgnoreWhiteSpace; + + var result = constraint.ApplyTo("Hello\tWorld"); + + Assert.That(result.IsSuccess, Is.True); + } + + [Test] + public void ExtendedIgnoreWhiteSpaceExample() + { + const string prettyJson = """ + "persons":[ + { + "name": "John", + "surname": "Smith" + }, + { + "name": "Jane", + "surname": "Doe" + } + ] + """; + const string condensedJson = """ + "persons":[{"name":"John","surname":"Smith"},{"name": "Jane","surname": "Doe"}] + """; + + Assert.That(condensedJson, Is.Not.EqualTo(prettyJson)); + Assert.That(condensedJson, Is.EqualTo(prettyJson).IgnoreWhiteSpace); + } + + [Test] + public void IgnoreWhiteSpaceFail() + { + var constraint = new EqualConstraint("Hello World").IgnoreWhiteSpace; + + var result = constraint.ApplyTo("Hello Universe"); + + Assert.That(result.IsSuccess, Is.False); + } + + [Test] + public void IgnoreWhiteSpaceAndIgnoreCase() + { + var constraint = new EqualConstraint("Hello World").IgnoreWhiteSpace.IgnoreCase; + + var result = constraint.ApplyTo("hello\r\nworld\r\n"); + + Assert.That(result.IsSuccess, Is.True); + } + [Test] public void Bug524CharIntWithoutOverload() { diff --git a/src/NUnitFramework/tests/Constraints/MsgUtilTests.cs b/src/NUnitFramework/tests/Constraints/MsgUtilTests.cs index 52c123c324..305265c777 100644 --- a/src/NUnitFramework/tests/Constraints/MsgUtilTests.cs +++ b/src/NUnitFramework/tests/Constraints/MsgUtilTests.cs @@ -277,6 +277,14 @@ public static void EscapeControlCharsTest(string? input, string? expected) Assert.That(MsgUtils.EscapeControlChars(input), Is.EqualTo(expected)); } + [TestCase("Hello\r\nWorld", 4, "Hello\\r\\nWorld", 4)] + [TestCase("Hello\r\nWorld", 7, "Hello\\r\\nWorld", 9)] + public static void EscapeControlCharsWithIndexTest(string? input, int index, string? expected, int expectedIndex) + { + Assert.That(MsgUtils.EscapeControlChars(input, ref index), Is.EqualTo(expected)); + Assert.That(index, Is.EqualTo(expectedIndex)); + } + [Test] public static void EscapeNullCharInString() { @@ -307,9 +315,9 @@ public static void EscapesNullControlChars() private const string S52 = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; [TestCase(S52, 52, 0, S52, TestName = "NoClippingNeeded")] - [TestCase(S52, 29, 0, "abcdefghijklmnopqrstuvwxyz...", TestName = "ClipAtEnd")] - [TestCase(S52, 29, 26, "...ABCDEFGHIJKLMNOPQRSTUVWXYZ", TestName = "ClipAtStart")] - [TestCase(S52, 28, 26, "...ABCDEFGHIJKLMNOPQRSTUV...", TestName = "ClipAtStartAndEnd")] + [TestCase(S52, 26, 0, "abcdefghijklmnopqrstuvwxyz...", TestName = "ClipAtEnd")] + [TestCase(S52, 26, 26, "...ABCDEFGHIJKLMNOPQRSTUVWXYZ", TestName = "ClipAtStart")] + [TestCase(S52, 22, 26, "...ABCDEFGHIJKLMNOPQRSTUV...", TestName = "ClipAtStartAndEnd")] public static void TestClipString(string input, int max, int start, string result) { System.Console.WriteLine("input= \"{0}\"", input); @@ -318,7 +326,45 @@ public static void TestClipString(string input, int max, int start, string resul } #endregion + #region ClipWhenNeeded + + [Test] + public static void ClipWhenNeeded_StringFitsInLine() + { + int mismatchedLocation = 5; + string clipped = MsgUtils.ClipWhenNeeded(S52, S52.Length, 52, ref mismatchedLocation); + Assert.That(clipped, Is.EqualTo(S52)); + Assert.That(mismatchedLocation, Is.EqualTo(5)); + } + + [Test] + public static void ClipWhenNeeded_StringDoesNotFitInLineMismatchLocationEarly() + { + int mismatchedLocation = 10; + string clipped = MsgUtils.ClipWhenNeeded(S52, S52.Length, 29, ref mismatchedLocation); + Assert.That(clipped, Is.EqualTo("abcdefghijklmnopqrstuvwxyz...")); + Assert.That(mismatchedLocation, Is.EqualTo(10)); + } + + [Test] + public static void ClipWhenNeeded_StringDoesNotFitInLineMismatchLocationInTheMiddle() + { + int mismatchedLocation = 26; + string clipped = MsgUtils.ClipWhenNeeded(S52, S52.Length, 29, ref mismatchedLocation); + Assert.That(clipped, Is.EqualTo("...pqrstuvwxyzABCDEFGHIJKL...")); + Assert.That(mismatchedLocation, Is.EqualTo(26 - (26 - 23 / 2) + 3)); + } + [Test] + public static void ClipWhenNeeded_StringDoesNotFitInLineMismatchLocationAlmostAtEnd() + { + int mismatchedLocation = 50; + string clipped = MsgUtils.ClipWhenNeeded(S52, S52.Length, 29, ref mismatchedLocation); + Assert.That(clipped, Is.EqualTo("...ABCDEFGHIJKLMNOPQRSTUVWXYZ")); + Assert.That(mismatchedLocation, Is.EqualTo(50 - (29 - 3) + 3)); + } + + #endregion #region ClipExpectedAndActual [Test] @@ -326,15 +372,22 @@ public static void ClipExpectedAndActual_StringsFitInLine() { string eClip = S52; string aClip = "abcde"; - MsgUtils.ClipExpectedAndActual(ref eClip, ref aClip, 52, 5); + int mismatchExpected = 5; + int mismatchActual = 5; + MsgUtils.ClipExpectedAndActual(ref eClip, ref aClip, 52, ref mismatchExpected, ref mismatchActual); Assert.That(eClip, Is.EqualTo(S52)); Assert.That(aClip, Is.EqualTo("abcde")); + Assert.That(mismatchExpected, Is.EqualTo(5)); + Assert.That(mismatchActual, Is.EqualTo(5)); eClip = S52; aClip = "abcdefghijklmno?qrstuvwxyz"; - MsgUtils.ClipExpectedAndActual(ref eClip, ref aClip, 52, 15); + mismatchExpected = mismatchActual = 15; + MsgUtils.ClipExpectedAndActual(ref eClip, ref aClip, 52, ref mismatchExpected, ref mismatchActual); Assert.That(eClip, Is.EqualTo(S52)); Assert.That(aClip, Is.EqualTo("abcdefghijklmno?qrstuvwxyz")); + Assert.That(mismatchExpected, Is.EqualTo(15)); + Assert.That(mismatchActual, Is.EqualTo(15)); } [Test] @@ -342,24 +395,40 @@ public static void ClipExpectedAndActual_StringTailsFitInLine() { string s1 = S52; string s2 = S52.Replace('Z', '?'); - MsgUtils.ClipExpectedAndActual(ref s1, ref s2, 29, 51); + int mismatchExpected = 51; + int mismatchActual = 51; + MsgUtils.ClipExpectedAndActual(ref s1, ref s2, 29, ref mismatchExpected, ref mismatchActual); Assert.That(s1, Is.EqualTo("...ABCDEFGHIJKLMNOPQRSTUVWXYZ")); + Assert.That(mismatchExpected, Is.EqualTo(51 - 26 + 3)); + Assert.That(mismatchActual, Is.EqualTo(51 - 26 + 3)); } [Test] - public static void ClipExpectedAndActual_StringsDoNotFitInLine() + public static void ClipExpectedAndActual_StringsHeadFitsInLine() { string s1 = S52; string s2 = "abcdefghij"; - MsgUtils.ClipExpectedAndActual(ref s1, ref s2, 29, 10); + int mismatchExpected = 10; + int mismatchActual = 10; + MsgUtils.ClipExpectedAndActual(ref s1, ref s2, 29, ref mismatchExpected, ref mismatchActual); Assert.That(s1, Is.EqualTo("abcdefghijklmnopqrstuvwxyz...")); Assert.That(s2, Is.EqualTo("abcdefghij")); + Assert.That(mismatchExpected, Is.EqualTo(10)); + Assert.That(mismatchActual, Is.EqualTo(10)); + } - s1 = S52; - s2 = "abcdefghijklmno?qrstuvwxyz"; - MsgUtils.ClipExpectedAndActual(ref s1, ref s2, 25, 15); - Assert.That(s1, Is.EqualTo("...efghijklmnopqrstuvw...")); - Assert.That(s2, Is.EqualTo("...efghijklmno?qrstuvwxyz")); + [Test] + public static void ClipExpectedAndActual_StringsDoNotFitInLine() + { + string s1 = S52; + string s2 = "abcdefghijklmno?qrstuvwxyz"; + int mismatchExpected = 15; + int mismatchActual = 15; + MsgUtils.ClipExpectedAndActual(ref s1, ref s2, 17, ref mismatchExpected, ref mismatchActual); + Assert.That(s1, Is.EqualTo("...klmnopqrstu...")); + Assert.That(s2, Is.EqualTo("...klmno?qrstu...")); + Assert.That(mismatchExpected, Is.EqualTo(8)); + Assert.That(mismatchActual, Is.EqualTo(8)); } #endregion diff --git a/src/NUnitFramework/tests/Constraints/UniqueItemsConstraintTests.cs b/src/NUnitFramework/tests/Constraints/UniqueItemsConstraintTests.cs index b8d9540e11..0dfd50fcbb 100644 --- a/src/NUnitFramework/tests/Constraints/UniqueItemsConstraintTests.cs +++ b/src/NUnitFramework/tests/Constraints/UniqueItemsConstraintTests.cs @@ -45,6 +45,15 @@ public void HonorsIgnoreCase(IEnumerable actual) Assert.That(result.IsSuccess, Is.False, $"{actual} should not be unique ignoring case"); } + [TestCaseSource(nameof(IgnoreWhiteSpaceData))] + public void HonorsIgnoreWhiteSpace(IEnumerable actual) + { + var constraint = new UniqueItemsConstraint().IgnoreWhiteSpace; + var result = constraint.ApplyTo(actual); + + Assert.That(result.IsSuccess, Is.False, $"{actual} should not be unique ignoring white-space"); + } + private static readonly object[] IgnoreCaseData = { new object[] { new SimpleObjectCollection("x", "y", "z", "Z") }, @@ -52,6 +61,12 @@ public void HonorsIgnoreCase(IEnumerable actual) new object[] { new[] { "a", "b", "c", "C" } } }; + private static readonly object[] IgnoreWhiteSpaceData = + { + new object[] { new SimpleObjectCollection("x", "y", "z", " z ") }, + new object[] { new[] { "a", "b", "c", " c " } } + }; + private static readonly object[] DuplicateItemsData = { new object[] { new[] { 1, 2, 3, 2 }, new[] { 2 } }, diff --git a/src/NUnitFramework/tests/Constraints/WhiteSpaceContraintTests.cs b/src/NUnitFramework/tests/Constraints/WhiteSpaceContraintTests.cs new file mode 100644 index 0000000000..ec5765651b --- /dev/null +++ b/src/NUnitFramework/tests/Constraints/WhiteSpaceContraintTests.cs @@ -0,0 +1,47 @@ +// Copyright (c) Charlie Poole, Rob Prouse and Contributors. MIT License - see LICENSE.txt + +using NUnit.Framework.Constraints; + +namespace NUnit.Framework.Tests.Constraints +{ + [TestFixture] + public class WhiteSpaceContraintTests : StringConstraintTests + { + protected override Constraint TheConstraint { get; } = new WhiteSpaceConstraint(); + + [SetUp] + public void SetUp() + { + ExpectedDescription = "white-space"; + StringRepresentation = ""; + } + + private static readonly object[] SuccessData = new object[] + { + string.Empty, + " ", + "\f", + "\n", + "\r", + "\t", + "\v", + }; + private static readonly object[] FailureData = new object[] + { + new TestCaseData("Hello", "\"Hello\""), + new TestCaseData("Hello World", "\"Hello World\""), + }; + + [TestCaseSource(nameof(SuccessData))] + public void TestIsWhiteSpace(string text) + { + Assert.That(text, Is.WhiteSpace); + } + + [TestCaseSource(nameof(FailureData))] + public void TestIsNotWhiteSpace(string text, string message) + { + Assert.That(text, Is.Not.WhiteSpace, message); + } + } +} diff --git a/src/NUnitFramework/tests/Internal/TextMessageWriterTests.cs b/src/NUnitFramework/tests/Internal/TextMessageWriterTests.cs index 0f65d39c29..97cb3c64c9 100644 --- a/src/NUnitFramework/tests/Internal/TextMessageWriterTests.cs +++ b/src/NUnitFramework/tests/Internal/TextMessageWriterTests.cs @@ -31,7 +31,7 @@ public void DisplayStringDifferences() string s72 = "abcdefghijklmnopqrstuvwxyz1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"; string exp = "abcdefghijklmnopqrstuvwxyz1234567890ABCDEFGHIJKLMNOPQRSTUVWXY..."; - _writer.DisplayStringDifferences(s72, "abcde", 5, false, true); + _writer.DisplayStringDifferences(s72, "abcde", 5, 5, false, false, true); string message = _writer.ToString(); Assert.That(message, Is.EqualTo( TextMessageWriter.Pfx_Expected + Q(exp) + NL + @@ -44,7 +44,7 @@ public void DisplayStringDifferences_NoClipping() { string s72 = "abcdefghijklmnopqrstuvwxyz1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"; - _writer.DisplayStringDifferences(s72, "abcde", 5, false, false); + _writer.DisplayStringDifferences(s72, "abcde", 5, 5, false, false, false); string message = _writer.ToString(); Assert.That(message, Is.EqualTo( TextMessageWriter.Pfx_Expected + Q(s72) + NL + @@ -52,6 +52,22 @@ public void DisplayStringDifferences_NoClipping() " ----------------^" + NL)); } + [Test] + public void DisplayStringDifferences_IgnoreWhiteSpace() + { + string expected = "abc def"; + string actual = "a b c d e g"; + + _writer.DisplayStringDifferences(expected, actual, 6, 10, false, true, false); + string message = _writer.ToString(); + string expectedMessage = + TextMessageWriter.Pfx_Expected + Q(expected) + ", ignoring white-space" + NL + + " -----------------^" + NL + + TextMessageWriter.Pfx_Actual + Q(actual) + NL + + " ---------------------^" + NL; + Assert.That(message, Is.EqualTo(expectedMessage)); + } + [Test] public void WriteMessageLine_EmbeddedZeroes() {