Skip to content

Commit

Permalink
Consider types assignable to open generics (#954) (#955)
Browse files Browse the repository at this point in the history
* Consider types assignable to open generics (#954)

* code clean up

* also consider open generics as base types

* code clean up

* make BeOfType NotBeOfType consistent

* make BeAssignableTo same for objects

* add more tests
  • Loading branch information
mdonoughe authored and dennisdoomen committed Nov 8, 2018
1 parent a144189 commit a9abfa8
Show file tree
Hide file tree
Showing 6 changed files with 507 additions and 11 deletions.
55 changes: 55 additions & 0 deletions Src/FluentAssertions/Common/TypeExtensions.cs
Expand Up @@ -455,5 +455,60 @@ private static bool IsTuple(this Type type)
|| openType == typeof(ValueTuple<,,,,,,>)
|| (openType == typeof(ValueTuple<,,,,,,,>) && IsTuple(type.GetGenericArguments()[7]));
}

internal static bool IsAssignableToOpenGeneric(this Type type, Type definition)
{
// The CLR type system does not consider anything to be assignable to an open generic type.
// For the purposes of test assertions, the user probably means that the subject type is
// assignable to any generic type based on the given generic type definition.

if (definition.GetTypeInfo().IsInterface)
{
return type.IsImplementationOfOpenGeneric(definition);
}
else
{
return type.IsSameOrEqualTo(definition) || type.IsDerivedFromOpenGeneric(definition);
}
}

internal static bool IsImplementationOfOpenGeneric(this Type type, Type definition)
{
// check subject against definition
TypeInfo subjectInfo = type.GetTypeInfo();
if (subjectInfo.IsInterface && subjectInfo.IsGenericType &&
subjectInfo.GetGenericTypeDefinition().IsSameOrEqualTo(definition))
{
return true;
}

// check subject's interfaces against definition
return subjectInfo.ImplementedInterfaces
.Select(i => i.GetTypeInfo())
.Where(i => i.IsGenericType)
.Select(i => i.GetGenericTypeDefinition())
.Any(d => d.IsSameOrEqualTo(definition));
}

internal static bool IsDerivedFromOpenGeneric(this Type type, Type definition)
{
if (type.IsSameOrEqualTo(definition))
{
// do not consider a type to be derived from itself
return false;
}

// check subject and its base types against definition
for (TypeInfo baseType = type.GetTypeInfo(); baseType != null;
baseType = baseType.BaseType?.GetTypeInfo())
{
if (baseType.IsGenericType && baseType.GetGenericTypeDefinition().IsSameOrEqualTo(definition))
{
return true;
}
}

return false;
}
}
}
35 changes: 32 additions & 3 deletions Src/FluentAssertions/Primitives/ReferenceTypeAssertions.cs
Expand Up @@ -3,6 +3,7 @@
using System.Linq.Expressions;
using System.Reflection;

using FluentAssertions.Common;
using FluentAssertions.Execution;

namespace FluentAssertions.Primitives
Expand Down Expand Up @@ -198,7 +199,15 @@ public AndConstraint<TAssertions> NotBeOfType(Type expectedType, string because
.WithDefaultIdentifier("type")
.FailWith("Expected {context} not to be {0}{reason}, but found <null>.", expectedType);

Subject.GetType().Should().NotBe(expectedType, because, becauseArgs);
Type subjectType = Subject.GetType();
if (expectedType.GetTypeInfo().IsGenericTypeDefinition && subjectType.GetTypeInfo().IsGenericType)
{
subjectType.GetGenericTypeDefinition().Should().NotBe(expectedType, because, becauseArgs);
}
else
{
subjectType.Should().NotBe(expectedType, because, becauseArgs);
}

return new AndConstraint<TAssertions>((TAssertions)this);
}
Expand Down Expand Up @@ -238,8 +247,18 @@ public AndConstraint<TAssertions> BeAssignableTo(Type type, string because = "",
.WithDefaultIdentifier("type")
.FailWith("Expected {context} not to be {0}{reason}, but found <null>.", type);

bool isAssignable;
if (type.GetTypeInfo().IsGenericTypeDefinition)
{
isAssignable = Subject.GetType().IsAssignableToOpenGeneric(type);
}
else
{
isAssignable = type.IsAssignableFrom(Subject.GetType());
}

Execute.Assertion
.ForCondition(type.IsAssignableFrom(Subject.GetType()))
.ForCondition(isAssignable)
.BecauseOf(because, becauseArgs)
.WithDefaultIdentifier(Identifier)
.FailWith("Expected {context} to be assignable to {0}{reason}, but {1} is not.",
Expand Down Expand Up @@ -276,8 +295,18 @@ public AndConstraint<TAssertions> NotBeAssignableTo(Type type, string because =
.WithDefaultIdentifier("type")
.FailWith("Expected {context} not to be {0}{reason}, but found <null>.", type);

bool isAssignable;
if (type.GetTypeInfo().IsGenericTypeDefinition)
{
isAssignable = Subject.GetType().IsAssignableToOpenGeneric(type);
}
else
{
isAssignable = type.IsAssignableFrom(Subject.GetType());
}

Execute.Assertion
.ForCondition(!type.IsAssignableFrom(Subject.GetType()))
.ForCondition(!isAssignable)
.BecauseOf(because, becauseArgs)
.WithDefaultIdentifier(Identifier)
.FailWith("Expected {context} to not be assignable to {0}{reason}, but {1} is.",
Expand Down
48 changes: 44 additions & 4 deletions Src/FluentAssertions/Types/TypeAssertions.cs
Expand Up @@ -81,8 +81,18 @@ public new AndConstraint<TypeAssertions> BeAssignableTo<T>(string because = "",
/// <returns>An <see cref="AndConstraint{T}"/> which can be used to chain assertions.</returns>
public new AndConstraint<TypeAssertions> BeAssignableTo(Type type, string because = "", params object[] becauseArgs)
{
bool isAssignable;
if (type.GetTypeInfo().IsGenericTypeDefinition)
{
isAssignable = Subject.IsAssignableToOpenGeneric(type);
}
else
{
isAssignable = type.IsAssignableFrom(Subject);
}

Execute.Assertion
.ForCondition(type.IsAssignableFrom(Subject))
.ForCondition(isAssignable)
.BecauseOf(because, becauseArgs)
.FailWith(
"Expected {context:" + Identifier + "} {0} to be assignable to {1}{reason}, but it is not.",
Expand Down Expand Up @@ -113,8 +123,18 @@ public new AndConstraint<TypeAssertions> NotBeAssignableTo<T>(string because = "
/// <returns>An <see cref="AndConstraint{T}"/> which can be used to chain assertions.</returns>
public new AndConstraint<TypeAssertions> NotBeAssignableTo(Type type, string because = "", params object[] becauseArgs)
{
bool isAssignable;
if (type.GetTypeInfo().IsGenericTypeDefinition)
{
isAssignable = Subject.IsAssignableToOpenGeneric(type);
}
else
{
isAssignable = type.IsAssignableFrom(Subject);
}

Execute.Assertion
.ForCondition(!type.IsAssignableFrom(Subject))
.ForCondition(!isAssignable)
.BecauseOf(because, becauseArgs)
.FailWith(
"Expected {context:" + Identifier + "} {0} to not be assignable to {1}{reason}, but it is.",
Expand Down Expand Up @@ -471,7 +491,17 @@ public AndConstraint<TypeAssertions> BeDerivedFrom(Type baseType, string because
throw new ArgumentException("Must not be an interface Type.", nameof(baseType));
}

Execute.Assertion.ForCondition(Subject.GetTypeInfo().IsSubclassOf(baseType))
bool isDerivedFrom;
if (baseType.GetTypeInfo().IsGenericTypeDefinition)
{
isDerivedFrom = Subject.IsDerivedFromOpenGeneric(baseType);
}
else
{
isDerivedFrom = Subject.GetTypeInfo().IsSubclassOf(baseType);
}

Execute.Assertion.ForCondition(isDerivedFrom)
.BecauseOf(because, becauseArgs)
.FailWith("Expected type {0} to be derived from {1}{reason}, but it is not.", Subject, baseType);

Expand Down Expand Up @@ -505,8 +535,18 @@ public AndConstraint<TypeAssertions> NotBeDerivedFrom(Type baseType, string beca
throw new ArgumentException("Must not be an interface Type.", nameof(baseType));
}

bool isDerivedFrom;
if (baseType.GetTypeInfo().IsGenericTypeDefinition)
{
isDerivedFrom = Subject.IsDerivedFromOpenGeneric(baseType);
}
else
{
isDerivedFrom = Subject.GetTypeInfo().IsSubclassOf(baseType);
}

Execute.Assertion
.ForCondition(!Subject.GetTypeInfo().IsSubclassOf(baseType))
.ForCondition(!isDerivedFrom)
.BecauseOf(because, becauseArgs)
.FailWith("Expected type {0} not to be derived from {1}{reason}, but it is.", Subject, baseType);

Expand Down
60 changes: 60 additions & 0 deletions Tests/Shared.Specs/ObjectAssertionSpecs.cs
Expand Up @@ -646,6 +646,20 @@ public void When_an_implemented_interface_type_instance_it_should_succeed()
someObject.Should().BeAssignableTo(typeof(IDisposable));
}

[Fact]
public void When_an_implemented_open_generic_interface_type_instance_it_should_succeed()
{
//-----------------------------------------------------------------------------------------------------------
// Arrange
//-----------------------------------------------------------------------------------------------------------
var someObject = new System.Collections.Generic.List<string>();

//-----------------------------------------------------------------------------------------------------------
// Act / Assert
//-----------------------------------------------------------------------------------------------------------
someObject.Should().BeAssignableTo(typeof(System.Collections.Generic.IList<>));
}

[Fact]
public void When_an_unrelated_type_instance_it_should_fail_with_a_descriptive_message()
{
Expand All @@ -662,6 +676,22 @@ public void When_an_unrelated_type_instance_it_should_fail_with_a_descriptive_me
.WithMessage($"*assignable to {typeof(DateTime)}*failure message*{typeof(DummyImplementingClass)} is not*");
}

[Fact]
public void When_unrelated_to_open_generic_type_it_should_fail_with_a_descriptive_message()
{
//-----------------------------------------------------------------------------------------------------------
// Arrange
//-----------------------------------------------------------------------------------------------------------
var someObject = new DummyImplementingClass();
Action act = () => someObject.Should().BeAssignableTo(typeof(System.Collections.Generic.IList<>), "because we want to test the failure {0}", "message");

//-----------------------------------------------------------------------------------------------------------
// Act / Assert
//-----------------------------------------------------------------------------------------------------------
act.Should().Throw<XunitException>()
.WithMessage($"*assignable to {typeof(System.Collections.Generic.IList<>)}*failure message*{typeof(DummyImplementingClass)} is not*");
}

#endregion

#region NotBeAssignableTo
Expand Down Expand Up @@ -797,6 +827,22 @@ public void When_an_implemented_interface_type_instance_and_asserting_not_assign
.WithMessage($"*not be assignable to {typeof(IDisposable)}*failure message*{typeof(DummyImplementingClass)} is*");
}

[Fact]
public void When_an_implemented_open_generic_interface_type_instance_and_asserting_not_assignable_it_should_fail_with_a_useful_message()
{
//-----------------------------------------------------------------------------------------------------------
// Arrange
//-----------------------------------------------------------------------------------------------------------
var someObject = new System.Collections.Generic.List<string>();
Action act = () => someObject.Should().NotBeAssignableTo(typeof(System.Collections.Generic.IList<>), "because we want to test the failure {0}", "message");

//-----------------------------------------------------------------------------------------------------------
// Act / Assert
//-----------------------------------------------------------------------------------------------------------
act.Should().Throw<XunitException>()
.WithMessage($"*not be assignable to {typeof(System.Collections.Generic.IList<>)}*failure message*{typeof(System.Collections.Generic.List<string>)} is*");
}

[Fact]
public void When_an_unrelated_type_instance_and_asserting_not_assignable_it_should_succeed()
{
Expand All @@ -811,6 +857,20 @@ public void When_an_unrelated_type_instance_and_asserting_not_assignable_it_shou
someObject.Should().NotBeAssignableTo(typeof(DateTime), "because we want to test the failure {0}", "message");
}

[Fact]
public void When_unrelated_to_open_generic_type_and_asserting_not_assignable_it_should_succeed()
{
//-----------------------------------------------------------------------------------------------------------
// Arrange
//-----------------------------------------------------------------------------------------------------------
var someObject = new DummyImplementingClass();

//-----------------------------------------------------------------------------------------------------------
// Act / Assert
//-----------------------------------------------------------------------------------------------------------
someObject.Should().NotBeAssignableTo(typeof(System.Collections.Generic.IList<>), "because we want to test the failure {0}", "message");
}

#endregion

#region HaveFlag / NotHaveFlag
Expand Down

0 comments on commit a9abfa8

Please sign in to comment.