utils/Testing/ClassComparer.cs 4.0 KB (4136b); Thu, 17 Feb 2011 23:49
´╗┐using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace Cselian.Utilities.Testing
{
	public class ClassComparer<TLeft, TRight>
	{
		private readonly List<string> leftOnly = new List<string>();
		private readonly List<string> rightOnly = new List<string>();
		private readonly Dictionary<string, Type> leftTypes = new Dictionary<string, Type>();
		private readonly Dictionary<string, Type> rightTypes = new Dictionary<string, Type>();

		public ClassComparer<TLeft, TRight> OnlyLeft(Expression<Func<TLeft, object>> property)
		{
			leftOnly.Add(GetName(property));
			return this;
		}

		public ClassComparer<TLeft, TRight> OnlyRight(Expression<Func<TRight, object>> property)
		{
			rightOnly.Add(GetName(property));
			return this;
		}

		public ClassComparer<TLeft, TRight> TypeChange<TLeftProp, TRightProp>(
			Expression<Func<TLeft, object>> leftProp,
			Expression<Func<TRight, object>> rightProp)
		{
			var leftName = GetName(leftProp);
			var rightName = GetName(rightProp);
			if (leftName != rightName) throw new Exception("Properties must have same name");
			leftTypes.Add(leftName, typeof(TLeftProp));
			rightTypes.Add(rightName, typeof(TRightProp));
			return this;
		}

		StringBuilder errors = new StringBuilder();

		public void Verify()
		{
			var lefts = typeof(TLeft).GetProperties().ToList();
			var rights = typeof(TRight).GetProperties().ToList();

			ANotInB(leftOnly, rights, lefts, "Left");
			ANotInB(rightOnly, lefts, rights, "Right");
			RemoveCommon(lefts, rights);
			Remaining(lefts, "Left");
			Remaining(rights, "Right");

			if (errors.Length > 0)
			{
				errors.Insert(0, string.Format("Comparing {0} and {1}",
					typeof(TLeft).FullName, typeof(TRight).FullName)
					+ Environment.NewLine);
				Assert.Fail(errors.ToString());
			}
		}

		private void Remaining(List<PropertyInfo> list, string side)
		{
			foreach (var item in list)
			{
				errors.AppendLine(string.Format("{0} of type '{1}' found only in {2}", item.Name, item.PropertyType.FullName, side));
			}
		}

		private void RemoveCommon(List<PropertyInfo> lefts, List<PropertyInfo> rights)
		{
			var allLefts = lefts.ToArray();
			foreach (var left in allLefts)
			{
				var right = rights.FirstOrDefault(x => x.Name == left.Name);
				if (right == null) continue;
				if (leftTypes.ContainsKey(left.Name))
				{
					var leftType = leftTypes[left.Name];
					if (left.PropertyType != leftType)
						Compare(left.Name, leftType, left.PropertyType, "Left", " (as configured in Test)");

					var rightType = rightTypes[left.Name];
					if (right.PropertyType != rightType)
						Compare(left.Name, rightType, right.PropertyType, "Right", " (as configured in Test)");
				}
				else if (right.PropertyType != left.PropertyType)
				{
					Compare(left.Name, left.PropertyType, right.PropertyType, "Right", string.Empty);
				}
				lefts.Remove(left);
				rights.Remove(right);
			}
		}

		private void Compare(string name, Type expected, Type actual, string side, string why)
		{
			errors.AppendLine(string.Format(
								  "Property {0} on {3} expected to be {1}. Actual {2}{4}",
								  name,
								  expected.FullName,
								  actual.FullName,
								  side, why
							  ));
		}

		private void ANotInB(List<string> aNames, List<PropertyInfo> bList, List<PropertyInfo> aList, string side)
		{
			aList.RemoveAll(x => aNames.Contains(x.Name));
			var shouldntBe = bList.Where(x => aNames.Contains(x.Name)).ToArray();
			foreach (var item in shouldntBe)
			{
				bList.Remove(item);
				errors.AppendLine(item.Name + " should Exist only in LEFT");
			}
		}

		protected static string GetName<T>(Expression<Func<T, object>> property)
		{
			if (property.Body is UnaryExpression)
				return ((property.Body as UnaryExpression).Operand as MemberExpression).Member.Name;
			else
				return (property.Body as MemberExpression).Member.Name;
		}
	}
}