// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT License. // See the LICENSE file in the project root for more information. using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Reactive; using System.Reactive.Linq; using System.Text; using Xunit; namespace Microsoft.Reactive.Testing { /// /// Helper class to write asserts in unit tests for applications and libraries built using Reactive Extensions. /// public static class ReactiveAssert { private static string Message(IEnumerable actual, IEnumerable expected) { var sb = new StringBuilder(); sb.AppendLine(); sb.Append("Expected: ["); sb.Append(string.Join(", ", expected.Select(x => x.ToString()).ToArray())); sb.Append(']'); sb.AppendLine(); sb.Append("Actual..: ["); sb.Append(string.Join(", ", actual.Select(x => x.ToString()).ToArray())); sb.Append(']'); sb.AppendLine(); return sb.ToString(); } /// /// Asserts that both enumerable sequences have equal length and equal elements. /// /// The type of the elements in the sequence. /// Expected sequence. /// Actual sequence to compare against the expected one. /// or is null. public static void AreElementsEqual(IEnumerable expected, IEnumerable actual) => AreElementsEqual(expected, actual, EqualityComparer.Default); /// /// Asserts that both enumerable sequences have equal length and equal elements. /// /// The type of the elements in the sequence. /// Expected sequence. /// Actual sequence to compare against the expected one. /// Comparer used to check for equality. /// or is null. public static void AreElementsEqual(IEnumerable expected, IEnumerable actual, IEqualityComparer comparer) { if (expected == null) { throw new ArgumentNullException(nameof(expected)); } if (actual == null) { throw new ArgumentNullException(nameof(actual)); } if (!expected.SequenceEqual(actual, comparer ?? EqualityComparer.Default)) { Assert.True(false, Message(actual, expected)); } } /// /// Asserts that both enumerable sequences have equal length and equal elements. /// /// The type of the elements in the sequence. /// Expected sequence. /// Actual sequence to compare against the expected one. /// Error message for assert failure. /// or is null. public static void AreElementsEqual(IEnumerable expected, IEnumerable actual, string message) => AreElementsEqual(expected, actual, EqualityComparer.Default, message); /// /// Asserts that both enumerable sequences have equal length and equal elements. /// /// The type of the elements in the sequence. /// Expected sequence. /// Actual sequence to compare against the expected one. /// Comparer used to check for equality. /// Error message for assert failure. /// or is null. public static void AreElementsEqual(IEnumerable expected, IEnumerable actual, IEqualityComparer comparer, string message) { if (expected == null) { throw new ArgumentNullException(nameof(expected)); } if (actual == null) { throw new ArgumentNullException(nameof(actual)); } if (!expected.SequenceEqual(actual, comparer ?? EqualityComparer.Default)) { Assert.True(false, message); } } /// /// Asserts that both observable sequences have equal length and equal notifications. /// /// The type of the elements in the sequence. /// Expected sequence. /// Actual sequence to compare against the expected one. /// or is null. public static void AreElementsEqual(IObservable expected, IObservable actual) { if (expected == null) { throw new ArgumentNullException(nameof(expected)); } if (actual == null) { throw new ArgumentNullException(nameof(actual)); } AreElementsEqual(expected.Materialize().ToEnumerable(), actual.Materialize().ToEnumerable()); } /// /// Asserts that both observable sequences have equal length and equal notifications. /// /// The type of the elements in the sequence. /// Expected sequence. /// Actual sequence to compare against the expected one. /// Comparer used to check for equality. /// or is null. public static void AreElementsEqual(IObservable expected, IObservable actual, IEqualityComparer comparer) { if (expected == null) { throw new ArgumentNullException(nameof(expected)); } if (actual == null) { throw new ArgumentNullException(nameof(actual)); } AreElementsEqual(expected.Materialize().ToEnumerable(), actual.Materialize().ToEnumerable(), new NotificationComparer(comparer)); } /// /// Asserts that both observable sequences have equal length and equal elements. /// /// The type of the elements in the sequence. /// Expected sequence. /// Actual sequence to compare against the expected one. /// Error message for assert failure. /// or is null. public static void AreElementsEqual(IObservable expected, IObservable actual, string message) { if (expected == null) { throw new ArgumentNullException(nameof(expected)); } if (actual == null) { throw new ArgumentNullException(nameof(actual)); } AreElementsEqual(expected.Materialize().ToEnumerable(), actual.Materialize().ToEnumerable(), message); } /// /// Asserts that both observable sequences have equal length and equal elements. /// /// The type of the elements in the sequence. /// Expected sequence. /// Actual sequence to compare against the expected one. /// Comparer used to check for equality. /// Error message for assert failure. /// or is null. public static void AreElementsEqual(IObservable expected, IObservable actual, IEqualityComparer comparer, string message) { if (expected == null) { throw new ArgumentNullException(nameof(expected)); } if (actual == null) { throw new ArgumentNullException(nameof(actual)); } AreElementsEqual(expected.Materialize().ToEnumerable(), actual.Materialize().ToEnumerable(), new NotificationComparer(comparer), message); } /// /// Asserts that the given action throws an exception of the type specified in the generic parameter, or a subtype thereof. /// /// Type of the exception to check for. /// Action to run. /// is null. public static void Throws(Action action) where TException : Exception { if (action == null) { throw new ArgumentNullException(nameof(action)); } var failed = false; try { action(); failed = true; } catch (TException) { } catch (Exception ex) { Assert.True(false, string.Format(CultureInfo.CurrentCulture, "Expected {0} threw {1}.\r\n\r\nStack trace:\r\n{2}", typeof(TException).Name, ex.GetType().Name, ex.StackTrace)); } if (failed) { Assert.True(false, string.Format(CultureInfo.CurrentCulture, "Expected {0}.", typeof(TException).Name)); } } /// /// Asserts that the given action throws an exception of the type specified in the generic parameter, or a subtype thereof. /// /// Type of the exception to check for. /// Action to run. /// Error message for assert failure. /// is null. public static void Throws(Action action, string message) where TException : Exception { if (action == null) { throw new ArgumentNullException(nameof(action)); } var failed = false; try { action(); failed = true; } catch (TException) { } catch { Assert.True(false, message); } if (failed) { Assert.True(false, message); } } /// /// Asserts that the given action throws the specified exception. /// /// Type of the exception to check for. /// Exception to assert being thrown. /// Action to run. /// is null. public static void Throws(TException exception, Action action) where TException : Exception { if (action == null) { throw new ArgumentNullException(nameof(action)); } var failed = false; try { action(); failed = true; } catch (TException ex) { Assert.Same(exception, ex); } catch (Exception ex) { Assert.True(false, string.Format(CultureInfo.CurrentCulture, "Expected {0} threw {1}.\r\n\r\nStack trace:\r\n{2}", typeof(TException).Name, ex.GetType().Name, ex.StackTrace)); } if (failed) { Assert.True(false, string.Format(CultureInfo.CurrentCulture, "Expected {0}.", typeof(TException).Name)); } } /// /// Asserts that the given action throws the specified exception. /// /// Type of the exception to check for. /// Exception to assert being thrown. /// Action to run. /// Error message for assert failure. /// is null. public static void Throws(TException exception, Action action, string message) where TException : Exception { if (action == null) { throw new ArgumentNullException(nameof(action)); } var failed = false; try { action(); failed = true; } catch (TException ex) { Assert.Same(exception, ex); } catch { Assert.True(false, message); } if (failed) { Assert.True(false, message); } } /// /// Asserts that both enumerable sequences have equal length and equal elements. /// /// The type of the elements in the sequence. /// Actual sequence to compare against the expected one. /// Expected sequence. /// or is null. public static void AssertEqual(this IEnumerable actual, IEnumerable expected) { if (actual == null) { throw new ArgumentNullException(nameof(actual)); } if (expected == null) { throw new ArgumentNullException(nameof(expected)); } AreElementsEqual(expected, actual); } /// /// Asserts that both enumerable sequences have equal length and equal elements. /// /// The type of the elements in the sequence. /// Actual sequence to compare against the expected one. /// Expected sequence. /// Comparer used to check for equality. /// or is null. public static void AssertEqual(this IEnumerable actual, IEnumerable expected, IEqualityComparer comparer) { if (actual == null) { throw new ArgumentNullException(nameof(actual)); } if (expected == null) { throw new ArgumentNullException(nameof(expected)); } AreElementsEqual(expected, actual, comparer); } /// /// Asserts the enumerable sequence has the expected elements. /// /// The type of the elements in the sequence. /// Actual sequence to compare against the expected elements. /// Expected elements. /// or is null. public static void AssertEqual(this IEnumerable actual, params T[] expected) { if (actual == null) { throw new ArgumentNullException(nameof(actual)); } if (expected == null) { throw new ArgumentNullException(nameof(expected)); } AreElementsEqual(expected, actual); } /// /// Asserts that both observable sequences have equal length and equal notifications. /// /// The type of the elements in the sequence. /// Actual sequence to compare against the expected one. /// Expected sequence. /// or is null. public static void AssertEqual(this IObservable actual, IObservable expected) { if (actual == null) { throw new ArgumentNullException(nameof(actual)); } if (expected == null) { throw new ArgumentNullException(nameof(expected)); } AreElementsEqual(expected, actual); } /// /// Asserts that both observable sequences have equal length and equal notifications. /// /// The type of the elements in the sequence. /// Actual sequence to compare against the expected one. /// Expected sequence. /// Comparer used to check for equality. /// or is null. public static void AssertEqual(this IObservable actual, IObservable expected, IEqualityComparer comparer) { if (actual == null) { throw new ArgumentNullException(nameof(actual)); } if (expected == null) { throw new ArgumentNullException(nameof(expected)); } AreElementsEqual(expected, actual, comparer); } private sealed class NotificationComparer : IEqualityComparer> { private readonly IEqualityComparer _comparer; public NotificationComparer(IEqualityComparer comparer) { _comparer = comparer; } public bool Equals(Notification x, Notification y) { if (x != null && y != null) { if (x.HasValue && y.HasValue) { return _comparer.Equals(x.Value, y.Value); } } return EqualityComparer>.Default.Equals(x, y); } public int GetHashCode(Notification obj) { if (obj != null && obj.HasValue) { return _comparer.GetHashCode(obj.Value); } return EqualityComparer>.Default.GetHashCode(obj); } } } }