Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Consider types assignable to open generics (#954) #955

Merged
merged 7 commits into from Nov 8, 2018
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
30 changes: 30 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 Down Expand Up @@ -797,6 +811,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 Down
26 changes: 23 additions & 3 deletions Tests/Shared.Specs/ReferenceTypeAssertionsSpecs.cs
Expand Up @@ -118,17 +118,17 @@ public void When_object_is_of_the_expected_type_it_should_not_throw()
}

[Fact]
public void When_object_is_an_open_generic_of_the_expected_type_it_should_not_throw()
public void When_object_is_of_the_expected_open_generic_type_it_should_not_throw()
{
//-----------------------------------------------------------------------------------------------------------
// Arrange
//-----------------------------------------------------------------------------------------------------------
var openGeneric = new System.Collections.Generic.List<string>();
var aList = new System.Collections.Generic.List<string>();

//-----------------------------------------------------------------------------------------------------------
// Act
//-----------------------------------------------------------------------------------------------------------
Action action = () => openGeneric.Should().BeOfType(typeof(System.Collections.Generic.List<>));
Action action = () => aList.Should().BeOfType(typeof(System.Collections.Generic.List<>));

//-----------------------------------------------------------------------------------------------------------
// Assert
Expand Down Expand Up @@ -220,6 +220,26 @@ public void When_object_is_of_the_unexpected_generic_type_it_should_throw()
.WithMessage("Expected type not to be [" + typeof(string).AssemblyQualifiedName + "], but it is.");
}

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

//-----------------------------------------------------------------------------------------------------------
// Act
//-----------------------------------------------------------------------------------------------------------
Action action = () => aList.Should().NotBeOfType(typeof(System.Collections.Generic.List<>));

//-----------------------------------------------------------------------------------------------------------
// Assert
//-----------------------------------------------------------------------------------------------------------
action.Should().Throw<XunitException>()
.WithMessage("Expected type not to be [" + typeof(System.Collections.Generic.List<>).AssemblyQualifiedName + "], but it is.");
}

[Fact]
public void When_object_is_not_of_the_expected_type_it_should_not_throw()
{
Expand Down