diff --git a/Source/Extensions.cs b/Source/Extensions.cs index 5921ddb9b..c43c94368 100644 --- a/Source/Extensions.cs +++ b/Source/Extensions.cs @@ -40,6 +40,7 @@ using System; using System.Collections; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; @@ -323,5 +324,55 @@ private static MethodInfo GetInvokeMethodFromUntypedDelegateCallback(Delegate ca return null; } } + + /// + /// Gets all properties of the specified type in depth-first order. + /// That is, properties of the furthest ancestors are returned first, + /// and the type's own properties are returned last. + /// + /// The type whose properties are to be returned. + internal static List GetAllPropertiesInDepthFirstOrder(this Type type) + { + var properties = new List(); + var none = new HashSet(); + + type.AddPropertiesInDepthFirstOrderTo(properties, typesAlreadyVisited: none); + + return properties; + } + + /// + /// This is a helper method supporting + /// and is not supposed to be called directly. + /// + private static void AddPropertiesInDepthFirstOrderTo(this Type type, List properties, HashSet typesAlreadyVisited) + { + if (!typesAlreadyVisited.Contains(type)) + { + // make sure we do not process properties of the current type twice: + typesAlreadyVisited.Add(type); + + //// follow down axis 1: add properties of base class. note that this is currently + //// disabled, since it wasn't done previously and this can only result in changed + //// behavior. + //if (type.GetTypeInfo().BaseType != null) + //{ + // type.GetTypeInfo().BaseType.AddPropertiesInDepthFirstOrderTo(properties, typesAlreadyVisited); + //} + + // follow down axis 2: add properties of inherited / implemented interfaces: + var superInterfaceTypes = type.GetInterfaces(); + foreach (var superInterfaceType in superInterfaceTypes) + { + superInterfaceType.AddPropertiesInDepthFirstOrderTo(properties, typesAlreadyVisited); + } + + // add own properties: + foreach (var property in type.GetProperties()) + { + properties.Add(property); + } + } + } } } diff --git a/Source/Mock.cs b/Source/Mock.cs index f05a9ea61..e915ef557 100644 --- a/Source/Mock.cs +++ b/Source/Mock.cs @@ -691,12 +691,21 @@ private static void SetupAllProperties(Mock mock, Stack mockedTypesStack) { var mockType = mock.MockedType; mockedTypesStack.Push(mockType); - var properties = mockType.GetProperties() - .Concat(mockType.GetInterfaces().SelectMany(i => i.GetProperties())) + + var properties = + mockType + .GetAllPropertiesInDepthFirstOrder() + // ^ Depth-first traversal is important because properties in derived interfaces + // that shadow properties in base interfaces should be set up last. This + // enables the use case where a getter-only property is redeclared in a derived + // interface as a getter-and-setter property. .Where(p => p.CanRead && p.CanOverrideGet() && p.GetIndexParameters().Length == 0 && - !(p.CanWrite ^ (p.CanWrite & p.CanOverrideSet()))) + p.CanWrite == p.CanOverrideSet()) + // ^ The last condition will be true for two kinds of properties: + // (a) those that are read-only; and + // (b) those that are writable and whose setter can be overridden. .Distinct(); var setupPropertyMethod = mock.GetType().GetMethods() diff --git a/UnitTests/Regressions/IssueReportsFixture.cs b/UnitTests/Regressions/IssueReportsFixture.cs index 7a1e40661..deb83b7a6 100644 --- a/UnitTests/Regressions/IssueReportsFixture.cs +++ b/UnitTests/Regressions/IssueReportsFixture.cs @@ -299,6 +299,31 @@ public virtual void Close() #endregion + #region 162 + + public class Issue162 + { + [Fact] + public void GetSetPropertyThatOverridesGetPropertyRetainsValueSetUpWithMockOf() + { + const decimal expectedValue = .14M; + var i = Mock.Of(b => b.Value == expectedValue); + Assert.Equal(expectedValue, actual: i.Value); + } + + public interface A + { + decimal? Value { get; } + } + + public interface B : A + { + new decimal? Value { get; set; } + } + } + + #endregion + #region 163 #if FEATURE_SERIALIZATION @@ -696,6 +721,50 @@ public void strict_mock_accepts_null_as_nullable_guid_value() #endregion // #184 + #region 239 + + public class Issue239 + { + [Fact] + public void PropertyInBaseInterfaceRetainsValueSetUpWitMockOf() + { + var i1 = Mock.Of(i => i.ABoolean == true); + Assert.True(i1.ABoolean); + } + [Fact] + public void RedeclaredPropertyInDerivedInterfaceRetainsValueSetUpWithNewMockAndSetupReturns() + { + var i2 = new Mock(); + i2.Setup(i => i.ABoolean).Returns(true); + Assert.True(i2.Object.ABoolean); + } + [Fact] + public void RedeclaredPropertyInDerivedInterfaceRetainsValueSetUpWithMockOf() + { + var i2 = Mock.Of(i => i.ABoolean == true); + Assert.True(i2.ABoolean); + } + [Fact] + public void RedeclaredPropertyInDerivedInterfaceRetainsValueSetUpWitSetupAllPropertiesAndSetter() + { + var i2 = new Mock(); + i2.SetupAllProperties(); + i2.Object.ABoolean = true; + Assert.True(i2.Object.ABoolean); + } + + public interface Interface1 + { + bool ABoolean { get; } + } + public interface Interface2 : Interface1 + { + new bool ABoolean { get; set; } + } + } + + #endregion + #region #252 public class Issue252 @@ -750,6 +819,87 @@ public interface IMyInterface #endregion // #252 + #region 275 + + public class Issue275 + { + private const int EXPECTED = int.MaxValue; + + [Fact] + public void Root1Test() + { + var mock = Mock.Of(c => c.Value == EXPECTED); + Assert.Equal(EXPECTED, mock.Value); + } + + [Fact] + public void Derived1Test() + { + var mock = Mock.Of(c => c.Value == EXPECTED); + Assert.Equal(EXPECTED, mock.Value); + } + + [Fact] + public void Implementation1Test() + { + var mock = Mock.Of(c => c.Value == EXPECTED); + Assert.Equal(EXPECTED, mock.Value); + } + + [Fact] + public void Root2Test() + { + var mock = Mock.Of(c => c.Value == EXPECTED); + Assert.Equal(EXPECTED, mock.Value); + } + + [Fact] + public void Derived2Test() + { + var mock = Mock.Of(c => c.Value == EXPECTED); + Assert.Equal(EXPECTED, mock.Value); + } + + [Fact] + public void Implementation2Test() + { + var mock = Mock.Of(c => c.Value == EXPECTED); + Assert.Equal(EXPECTED, mock.Value); + } + + public interface IRoot1 + { + int Value { get; } + } + + public interface IRoot2 + { + int Value { get; set; } + } + + public interface IDerived1 : IRoot1 + { + new int Value { get; set; } + } + + public interface IDerived2 : IRoot2 + { + new int Value { get; set; } + } + + public class Implementation1 : IDerived1 + { + public int Value { get; set; } + } + + public class Implementation2 : IDerived1 + { + public int Value { get; set; } + } + } + + #endregion + #region 311 public sealed class Issue311