File: StringAsserts.cs
Web Access
Project: src\src\Microsoft.DotNet.XUnitAssert\src\Microsoft.DotNet.XUnitAssert.csproj (xunit.assert)
#if XUNIT_NULLABLE
#nullable enable
#endif

using System;
using System.Text.RegularExpressions;
using Xunit.Sdk;

namespace Xunit
{
#if XUNIT_VISIBILITY_INTERNAL
	internal
#else
	public
#endif
	partial class Assert
	{
		/// <summary>
		/// Verifies that a string contains a given sub-string, using the current culture.
		/// </summary>
		/// <param name="expectedSubstring">The sub-string expected to be in the string</param>
		/// <param name="actualString">The string to be inspected</param>
		/// <exception cref="ContainsException">Thrown when the sub-string is not present inside the string</exception>
		public static void Contains(
			string expectedSubstring,
#if XUNIT_NULLABLE
			string? actualString) =>
#else
			string actualString) =>
#endif
				Contains(expectedSubstring, actualString, StringComparison.CurrentCulture);

		/// <summary>
		/// Verifies that a string contains a given sub-string, using the given comparison type.
		/// </summary>
		/// <param name="expectedSubstring">The sub-string expected to be in the string</param>
		/// <param name="actualString">The string to be inspected</param>
		/// <param name="comparisonType">The type of string comparison to perform</param>
		/// <exception cref="ContainsException">Thrown when the sub-string is not present inside the string</exception>
		public static void Contains(
			string expectedSubstring,
#if XUNIT_NULLABLE
			string? actualString,
#else
			string actualString,
#endif
			StringComparison comparisonType)
		{
			GuardArgumentNotNull(nameof(expectedSubstring), expectedSubstring);

			if (actualString == null || actualString.IndexOf(expectedSubstring, comparisonType) < 0)
				throw new ContainsException(expectedSubstring, actualString);
		}

		/// <summary>
		/// Verifies that a string does not contain a given sub-string, using the current culture.
		/// </summary>
		/// <param name="expectedSubstring">The sub-string which is expected not to be in the string</param>
		/// <param name="actualString">The string to be inspected</param>
		/// <exception cref="DoesNotContainException">Thrown when the sub-string is present inside the string</exception>
		public static void DoesNotContain(
			string expectedSubstring,
#if XUNIT_NULLABLE
			string? actualString) =>
#else
			string actualString) =>
#endif
				DoesNotContain(expectedSubstring, actualString, StringComparison.CurrentCulture);

		/// <summary>
		/// Verifies that a string does not contain a given sub-string, using the current culture.
		/// </summary>
		/// <param name="expectedSubstring">The sub-string which is expected not to be in the string</param>
		/// <param name="actualString">The string to be inspected</param>
		/// <param name="comparisonType">The type of string comparison to perform</param>
		/// <exception cref="DoesNotContainException">Thrown when the sub-string is present inside the given string</exception>
		public static void DoesNotContain(
			string expectedSubstring,
#if XUNIT_NULLABLE
			string? actualString,
#else
			string actualString,
#endif
			StringComparison comparisonType)
		{
			GuardArgumentNotNull(nameof(expectedSubstring), expectedSubstring);

			if (actualString != null && actualString.IndexOf(expectedSubstring, comparisonType) >= 0)
				throw new DoesNotContainException(expectedSubstring, actualString);
		}

		/// <summary>
		/// Verifies that a string starts with a given string, using the current culture.
		/// </summary>
		/// <param name="expectedStartString">The string expected to be at the start of the string</param>
		/// <param name="actualString">The string to be inspected</param>
		/// <exception cref="ContainsException">Thrown when the string does not start with the expected string</exception>
		public static void StartsWith(
#if XUNIT_NULLABLE
			string? expectedStartString,
			string? actualString) =>
#else
			string expectedStartString,
			string actualString) =>
#endif
				StartsWith(expectedStartString, actualString, StringComparison.CurrentCulture);

		/// <summary>
		/// Verifies that a string starts with a given string, using the given comparison type.
		/// </summary>
		/// <param name="expectedStartString">The string expected to be at the start of the string</param>
		/// <param name="actualString">The string to be inspected</param>
		/// <param name="comparisonType">The type of string comparison to perform</param>
		/// <exception cref="ContainsException">Thrown when the string does not start with the expected string</exception>
		public static void StartsWith(
#if XUNIT_NULLABLE
			string? expectedStartString,
			string? actualString,
#else
			string expectedStartString,
			string actualString,
#endif
			StringComparison comparisonType)
		{
			if (expectedStartString == null || actualString == null || !actualString.StartsWith(expectedStartString, comparisonType))
				throw new StartsWithException(expectedStartString, actualString);
		}

		/// <summary>
		/// Verifies that a string ends with a given string, using the current culture.
		/// </summary>
		/// <param name="expectedEndString">The string expected to be at the end of the string</param>
		/// <param name="actualString">The string to be inspected</param>
		/// <exception cref="ContainsException">Thrown when the string does not end with the expected string</exception>
		public static void EndsWith(
#if XUNIT_NULLABLE
			string? expectedEndString,
			string? actualString) =>
#else
			string expectedEndString,
			string actualString) =>
#endif
				EndsWith(expectedEndString, actualString, StringComparison.CurrentCulture);

		/// <summary>
		/// Verifies that a string ends with a given string, using the given comparison type.
		/// </summary>
		/// <param name="expectedEndString">The string expected to be at the end of the string</param>
		/// <param name="actualString">The string to be inspected</param>
		/// <param name="comparisonType">The type of string comparison to perform</param>
		/// <exception cref="ContainsException">Thrown when the string does not end with the expected string</exception>
		public static void EndsWith(
#if XUNIT_NULLABLE
			string? expectedEndString,
			string? actualString,
#else
			string expectedEndString,
			string actualString,
#endif
			StringComparison comparisonType)
		{
			if (expectedEndString == null || actualString == null || !actualString.EndsWith(expectedEndString, comparisonType))
				throw new EndsWithException(expectedEndString, actualString);
		}

		/// <summary>
		/// Verifies that a string matches a regular expression.
		/// </summary>
		/// <param name="expectedRegexPattern">The regex pattern expected to match</param>
		/// <param name="actualString">The string to be inspected</param>
		/// <exception cref="MatchesException">Thrown when the string does not match the regex pattern</exception>
		public static void Matches(
			string expectedRegexPattern,
#if XUNIT_NULLABLE
			string? actualString)
#else
			string actualString)
#endif
		{
			GuardArgumentNotNull(nameof(expectedRegexPattern), expectedRegexPattern);

			if (actualString == null || !Regex.IsMatch(actualString, expectedRegexPattern))
				throw new MatchesException(expectedRegexPattern, actualString);
		}

		/// <summary>
		/// Verifies that a string matches a regular expression.
		/// </summary>
		/// <param name="expectedRegex">The regex expected to match</param>
		/// <param name="actualString">The string to be inspected</param>
		/// <exception cref="MatchesException">Thrown when the string does not match the regex</exception>
		public static void Matches(
			Regex expectedRegex,
#if XUNIT_NULLABLE
			string? actualString)
#else
			string actualString)
#endif
		{
			GuardArgumentNotNull(nameof(expectedRegex), expectedRegex);

			if (actualString == null || !expectedRegex.IsMatch(actualString))
				throw new MatchesException(expectedRegex.ToString(), actualString);
		}

		/// <summary>
		/// Verifies that a string does not match a regular expression.
		/// </summary>
		/// <param name="expectedRegexPattern">The regex pattern expected not to match</param>
		/// <param name="actualString">The string to be inspected</param>
		/// <exception cref="DoesNotMatchException">Thrown when the string matches the regex pattern</exception>
		public static void DoesNotMatch(
			string expectedRegexPattern,
#if XUNIT_NULLABLE
			string? actualString)
#else
			string actualString)
#endif
		{
			GuardArgumentNotNull(nameof(expectedRegexPattern), expectedRegexPattern);

			if (actualString != null && Regex.IsMatch(actualString, expectedRegexPattern))
				throw new DoesNotMatchException(expectedRegexPattern, actualString);
		}

		/// <summary>
		/// Verifies that a string does not match a regular expression.
		/// </summary>
		/// <param name="expectedRegex">The regex expected not to match</param>
		/// <param name="actualString">The string to be inspected</param>
		/// <exception cref="DoesNotMatchException">Thrown when the string matches the regex</exception>
		public static void DoesNotMatch(
			Regex expectedRegex,
#if XUNIT_NULLABLE
			string? actualString)
#else
			string actualString)
#endif
		{
			GuardArgumentNotNull(nameof(expectedRegex), expectedRegex);

			if (actualString != null && expectedRegex.IsMatch(actualString))
				throw new DoesNotMatchException(expectedRegex.ToString(), actualString);
		}

		/// <summary>
		/// Verifies that two strings are equivalent.
		/// </summary>
		/// <param name="expected">The expected string value.</param>
		/// <param name="actual">The actual string value.</param>
		/// <exception cref="EqualException">Thrown when the strings are not equivalent.</exception>
		public static void Equal(
#if XUNIT_NULLABLE
			string? expected,
			string? actual) =>
#else
			string expected,
			string actual) =>
#endif
				Equal(expected, actual, false, false, false);

		/// <summary>
		/// Verifies that two strings are equivalent.
		/// </summary>
		/// <param name="expected">The expected string value.</param>
		/// <param name="actual">The actual string value.</param>
		/// <param name="ignoreCase">If set to <c>true</c>, ignores cases differences. The invariant culture is used.</param>
		/// <param name="ignoreLineEndingDifferences">If set to <c>true</c>, treats \r\n, \r, and \n as equivalent.</param>
		/// <param name="ignoreWhiteSpaceDifferences">If set to <c>true</c>, treats spaces and tabs (in any non-zero quantity) as equivalent.</param>
		/// <param name="ignoreAllWhiteSpace">If set to <c>true</c>, ignores all white space differences during comparison.</param>
		/// <exception cref="EqualException">Thrown when the strings are not equivalent.</exception>
		public static void Equal(
#if XUNIT_NULLABLE
			string? expected,
			string? actual,
#else
			string expected,
			string actual,
#endif
			bool ignoreCase = false,
			bool ignoreLineEndingDifferences = false,
			bool ignoreWhiteSpaceDifferences = false,
			bool ignoreAllWhiteSpace = false)
		{
#if XUNIT_SPAN
			if (expected == null && actual == null)
				return;
			if (expected == null || actual == null)
				throw new EqualException(expected, actual, -1, -1);

			Equal(expected.AsSpan(), actual.AsSpan(), ignoreCase, ignoreLineEndingDifferences, ignoreWhiteSpaceDifferences, ignoreAllWhiteSpace);
#else
			// Start out assuming the one of the values is null
			int expectedIndex = -1;
			int actualIndex = -1;
			int expectedLength = 0;
			int actualLength = 0;

			if (expected == null)
			{
				if (actual == null)
					return;
			}
			else if (actual != null)
			{
				// Walk the string, keeping separate indices since we can skip variable amounts of
				// data based on ignoreLineEndingDifferences and ignoreWhiteSpaceDifferences.
				expectedIndex = 0;
				actualIndex = 0;
				expectedLength = expected.Length;
				actualLength = actual.Length;

				// Block used to fix edge case of Equal("", " ") when ignoreAllWhiteSpace enabled.
				if (ignoreAllWhiteSpace)
				{
					if (expectedLength == 0 && SkipWhitespace(actual, 0) == actualLength)
						return;
					if (actualLength == 0 && SkipWhitespace(expected, 0) == expectedLength)
						return;
				}

				while (expectedIndex < expectedLength && actualIndex < actualLength)
				{
					char expectedChar = expected[expectedIndex];
					char actualChar = actual[actualIndex];

					if (ignoreLineEndingDifferences && IsLineEnding(expectedChar) && IsLineEnding(actualChar))
					{
						expectedIndex = SkipLineEnding(expected, expectedIndex);
						actualIndex = SkipLineEnding(actual, actualIndex);
					}
					else if (ignoreAllWhiteSpace && (IsWhiteSpace(expectedChar) || IsWhiteSpace(actualChar)))
					{
						expectedIndex = SkipWhitespace(expected, expectedIndex);
						actualIndex = SkipWhitespace(actual, actualIndex);
					}
					else if (ignoreWhiteSpaceDifferences && IsWhiteSpace(expectedChar) && IsWhiteSpace(actualChar))
					{
						expectedIndex = SkipWhitespace(expected, expectedIndex);
						actualIndex = SkipWhitespace(actual, actualIndex);
					}
					else
					{
						if (ignoreCase)
						{
							expectedChar = Char.ToUpperInvariant(expectedChar);
							actualChar = Char.ToUpperInvariant(actualChar);
						}

						if (expectedChar != actualChar)
							break;

						expectedIndex++;
						actualIndex++;
					}
				}
			}

			if (expectedIndex < expectedLength || actualIndex < actualLength)
				throw new EqualException(expected, actual, expectedIndex, actualIndex);
#endif
		}

#if !XUNIT_SPAN
		static bool IsLineEnding(char c) =>
			c == '\r' || c == '\n';

		static bool IsWhiteSpace(char c) =>
			c == ' ' || c == '\t';

		static int SkipLineEnding(
			string value,
			int index)
		{
			if (value[index] == '\r')
				++index;
			if (index < value.Length && value[index] == '\n')
				++index;

			return index;
		}

		static int SkipWhitespace(
			string value,
			int index)
		{
			while (index < value.Length)
			{
				switch (value[index])
				{
					case ' ':
					case '\t':
						index++;
						break;

					default:
						return index;
				}
			}

			return index;
		}
#endif
	}
}