PageRenderTime 52ms CodeModel.GetById 23ms RepoModel.GetById 0ms app.codeStats 0ms

/Releases/1.7.0/FluentAssertions.Net35/Assertions/PropertyEqualityValidator.cs

#
C# | 271 lines | 217 code | 40 blank | 14 comment | 29 complexity | e11d1583cffceef5ca01265b379af0c0 MD5 | raw file
  1. using System;
  2. using System.Collections;
  3. using System.Collections.Generic;
  4. using System.Globalization;
  5. using System.Linq;
  6. using System.Reflection;
  7. using FluentAssertions.Common;
  8. namespace FluentAssertions.Assertions
  9. {
  10. /// <summary>
  11. /// Is responsible for validating the equality of one or more properties of a subject with another object.
  12. /// </summary>
  13. internal class PropertyEqualityValidator
  14. {
  15. #region Private Definitions
  16. private const BindingFlags InstancePropertiesFlag =
  17. BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.FlattenHierarchy;
  18. private const int RootLevel = 0;
  19. private UniqueObjectTracker uniqueObjectTracker;
  20. private string parentPropertyName = "";
  21. private readonly object subject;
  22. #endregion
  23. public PropertyEqualityValidator(object subject)
  24. {
  25. this.subject = subject;
  26. Properties = new List<PropertyInfo>();
  27. }
  28. public object OtherObject { get; set; }
  29. /// <summary>
  30. /// Contains the properties that should be included when comparing two objects.
  31. /// </summary>
  32. public IList<PropertyInfo> Properties { get; private set; }
  33. /// <summary>
  34. /// Gets or sets a value indicating whether the validator will ignore properties from the <see cref="Properties"/>
  35. /// collection that the <see cref="Other"/> object doesn't have.
  36. /// </summary>
  37. public bool OnlySharedProperties { get; set; }
  38. /// <summary>
  39. /// Gets or sets a value indicating whether it should continue comparing (collections of objects) that
  40. /// the <see cref="OtherObject"/> refers to.
  41. /// </summary>
  42. public bool RecurseOnNestedObjects { get; set; }
  43. public string Reason { get; set; }
  44. public object[] ReasonArgs { get; set; }
  45. public void Validate()
  46. {
  47. Validate(new UniqueObjectTracker(), "");
  48. }
  49. private void Validate(UniqueObjectTracker tracker, string parentPropertyName)
  50. {
  51. this.parentPropertyName = parentPropertyName;
  52. uniqueObjectTracker = tracker;
  53. uniqueObjectTracker.Track(subject);
  54. if (ReferenceEquals(OtherObject, null))
  55. {
  56. throw new NullReferenceException("Cannot compare subject's properties with a <null> object.");
  57. }
  58. if (Properties.Count == 0)
  59. {
  60. throw new InvalidOperationException("Please specify some properties to include in the comparison.");
  61. }
  62. AssertSelectedPropertiesAreEqual(subject, OtherObject);
  63. }
  64. private void AssertSelectedPropertiesAreEqual(object subject, object expected)
  65. {
  66. foreach (var propertyInfo in Properties)
  67. {
  68. object actualValue = propertyInfo.GetValue(subject, null);
  69. PropertyInfo compareeProperty = FindPropertyFrom(expected, propertyInfo.Name);
  70. if (compareeProperty != null)
  71. {
  72. object expectedValue = compareeProperty.GetValue(OtherObject, null);
  73. AssertPropertyEqualityUsingVerificationContext(expectedValue, actualValue, propertyInfo);
  74. }
  75. }
  76. }
  77. private PropertyInfo FindPropertyFrom(object obj, string propertyName)
  78. {
  79. PropertyInfo compareeProperty =
  80. obj.GetType().GetProperties(InstancePropertiesFlag).SingleOrDefault(pi => pi.Name == propertyName);
  81. if (!OnlySharedProperties && (compareeProperty == null))
  82. {
  83. Execute.Verification.FailWith(
  84. "Subject has property " + GetPropertyPath(propertyName) + " that the other object does not have.");
  85. }
  86. return compareeProperty;
  87. }
  88. private void AssertPropertyEqualityUsingVerificationContext(object expectedValue, object actualValue, PropertyInfo propertyInfo)
  89. {
  90. try
  91. {
  92. Verification.SubjectName = "property " + GetPropertyPath(propertyInfo.Name);
  93. AssertSinglePropertyEquality(propertyInfo.Name, actualValue, expectedValue);
  94. }
  95. finally
  96. {
  97. Verification.SubjectName = null;
  98. }
  99. }
  100. private void AssertSinglePropertyEquality(string propertyName, object actualValue, object expectedValue)
  101. {
  102. actualValue = TryConvertTo(expectedValue, actualValue);
  103. if (!actualValue.IsSameOrEqualTo(expectedValue))
  104. {
  105. if (expectedValue is string)
  106. {
  107. ((string)actualValue).Should().Be(expectedValue.ToString(), Reason, ReasonArgs);
  108. }
  109. else if (expectedValue is DateTime)
  110. {
  111. ((DateTime)actualValue).Should().Be((DateTime)expectedValue, Reason, ReasonArgs);
  112. }
  113. else if (IsCollection(expectedValue))
  114. {
  115. if (RecurseOnNestedObjects)
  116. {
  117. AssertNestedCollectionEquality(actualValue, (IEnumerable)expectedValue, GetPropertyPath(propertyName));
  118. }
  119. else
  120. {
  121. ((IEnumerable)actualValue).Should().Equal(((IEnumerable)expectedValue), Reason, ReasonArgs);
  122. }
  123. }
  124. else if (IsComplexType(expectedValue) & RecurseOnNestedObjects)
  125. {
  126. AssertNestedEquality(actualValue, expectedValue, GetPropertyPath(propertyName));
  127. }
  128. else
  129. {
  130. actualValue.Should().Be(expectedValue, Reason, ReasonArgs);
  131. }
  132. }
  133. }
  134. private void AssertNestedCollectionEquality(object actualValue, IEnumerable expectedValue, string propertyPath)
  135. {
  136. if (!IsCollection(actualValue))
  137. {
  138. Execute.Verification
  139. .BecauseOf(Reason, ReasonArgs)
  140. .FailWith("Expected {0} property to be a collection{reason}, but {1} is a {2}.",
  141. propertyPath, actualValue, actualValue.GetType().FullName);
  142. }
  143. var actualItems = ((IEnumerable)actualValue).Cast<object>().ToArray();
  144. var expectedItems = expectedValue.Cast<object>().ToArray();
  145. if (actualItems.Length != expectedItems.Length)
  146. {
  147. Execute.Verification
  148. .BecauseOf(Reason, ReasonArgs)
  149. .FailWith("Expected {0} property to be a collection with {1} item(s){reason}, but found {2}.",
  150. propertyPath, expectedItems.Length, actualItems.Length);
  151. }
  152. for (int index = 0; index < actualItems.Length; index++)
  153. {
  154. try
  155. {
  156. var validator = CreateNestedValidatorFor(actualItems[index], expectedItems[index]);
  157. validator.Validate(uniqueObjectTracker, propertyPath + "[index " + index + "]");
  158. }
  159. catch (ObjectAlreadyTrackedException)
  160. {
  161. Execute.Verification
  162. .BecauseOf(Reason, ReasonArgs)
  163. .FailWith("Expected property " + propertyPath + " to be {0}{reason}, but it contains a cyclic reference.",
  164. expectedValue);
  165. }
  166. }
  167. }
  168. private static bool IsCollection(object value)
  169. {
  170. return (!(value is string) && (value is IEnumerable));
  171. }
  172. private static bool IsComplexType(object expectedValue)
  173. {
  174. return (expectedValue != null) && expectedValue.GetType().GetProperties(InstancePropertiesFlag).Any();
  175. }
  176. private object TryConvertTo(object expectedValue, object subjectValue)
  177. {
  178. if (!ReferenceEquals(expectedValue, null) && !ReferenceEquals(subjectValue, null)
  179. && !subjectValue.GetType().IsSameOrInherits(expectedValue.GetType()))
  180. {
  181. try
  182. {
  183. subjectValue = Convert.ChangeType(subjectValue, expectedValue.GetType(), CultureInfo.CurrentCulture);
  184. }
  185. catch (FormatException)
  186. {
  187. }
  188. catch (InvalidCastException)
  189. {
  190. }
  191. }
  192. return subjectValue;
  193. }
  194. private void AssertNestedEquality(object actualValue, object expectedValue, string propertyName)
  195. {
  196. try
  197. {
  198. var validator = CreateNestedValidatorFor(actualValue, expectedValue);
  199. validator.Validate(uniqueObjectTracker, propertyName);
  200. }
  201. catch (ObjectAlreadyTrackedException)
  202. {
  203. Execute.Verification
  204. .BecauseOf(Reason, ReasonArgs)
  205. .FailWith("Expected property " + propertyName + " to be {0}{reason}, but it contains a cyclic reference.",
  206. expectedValue);
  207. }
  208. }
  209. private PropertyEqualityValidator CreateNestedValidatorFor(object actualValue, object expectedValue)
  210. {
  211. var validator = new PropertyEqualityValidator(actualValue)
  212. {
  213. RecurseOnNestedObjects = true,
  214. OtherObject = expectedValue,
  215. OnlySharedProperties = OnlySharedProperties
  216. };
  217. foreach (var propertyInfo in actualValue.GetType().GetProperties(InstancePropertiesFlag))
  218. {
  219. if (!propertyInfo.GetGetMethod(true).IsPrivate)
  220. {
  221. validator.Properties.Add(propertyInfo);
  222. }
  223. }
  224. return validator;
  225. }
  226. private string GetPropertyPath(string propertyName)
  227. {
  228. return (parentPropertyName.Length > 0) ? parentPropertyName + "." + propertyName : propertyName;
  229. }
  230. }
  231. }